Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions code/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format loosely follows [Keep a Changelog](https://keepachangelog.com/)
and the project adheres to [Semantic Versioning](https://semver.org/).

## [0.21.1]
### Fixed
- **The analysis/plan boundary commits blocked every cycle.** Since 0.20.0 git-ignored the whole `cleanrooms/` area, the `commit_analysis_boundary` (ANALYZE→PLAN) and `commit_plan_boundary` (→EXECUTE) policies could never run — `git add -- cleanrooms/<branch>/…` refuses ignored paths, so the transition failed closed with `ERROR_BOUNDARY_COMMIT_FAILED`. The two decisions contradicted each other: a working area that is intentionally ephemeral has no boundary snapshot to commit. Both transitions are now `commit_policy: none`; their gates still validate `diagnosis.md`/`plan.md`. The gap survived because the FSM tests ran in a temp repo that did **not** git-ignore `cleanrooms/`, so the boundary commit succeeded there while failing in every real repo (where `iq init` ignores it) — the tests now assert no commit is made. Found by dogfooding a real cycle (cacsi-dev/impulsa #40).

## [0.21.0]
### Added
- **`iq implementation start --issue <N>` — one explicit command opens a cycle.** It is the whole mechanical bootstrap that used to be handed to the model step by step in `inquiry-start.md` (derive the slug, `git checkout -b`, `mkdir` the cleanroom, hand-write `index.md`, then transition): now a single deterministic command. It resolves the project root, **initializes the workspace itself if `.inquiry/` is missing** (no separate `iq init`), reads the issue title from GitHub, derives the `<NNN>-<slug>` branch and checks it out, and fires `start_analyze` — whose existing effect scaffolds `cleanrooms/<branch>/analyze/`. The result: `main` → one command → linked branch + cleanroom + ANALYZE. This is the first module built to the **module-per-stage** direction (`iq <stage> <verb>`, every argument explicit and named) recorded in `docs/roadmap.md`.
Expand Down
9 changes: 7 additions & 2 deletions code/cli/assets/fsm/transition_contract.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,10 @@ transitions:
prechecks: [issue_selected_or_created, diagnosis_exists, diagnosis_structured, diagnosis_evidence_present, diagnosis_evidence_verifiable, index_exists, confirmations_exists]
effects: [generate_plan]
artifacts: [plan.md]
commit_policy: commit_analysis_boundary
# cleanrooms/ is intentionally git-ignored (ephemeral working area; durable
# artifacts are the issue, code, and tests). A boundary commit of it can
# never succeed and blocks the transition — so there is nothing to commit.
commit_policy: none
prompt_fragment_id: analyze_to_plan

- from: ANALYZE
Expand Down Expand Up @@ -188,7 +191,9 @@ transitions:
prechecks: [issue_selected, feature_branch_selected, plan_approved, plan_executable_checks]
effects: [prepare_execute]
artifacts: [execution_log.md]
commit_policy: commit_plan_boundary
# See complete_analysis: cleanrooms/ is git-ignored, so a plan boundary
# commit can never succeed. The plan gate still validates plan.md.
commit_policy: none
prompt_fragment_id: plan_to_execute

- from: PLAN
Expand Down
2 changes: 1 addition & 1 deletion code/cli/lib/src/version.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
///
/// Update this constant when bumping version in pubspec.yaml.
/// Both are kept in sync manually to avoid build-time complexity.
const String inquiryVersion = '0.21.0';
const String inquiryVersion = '0.21.1';
2 changes: 1 addition & 1 deletion code/cli/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: inquiry_cli
description: >
CLI for Inquiry — structured development through the APE methodology.
version: 0.21.0
version: 0.21.1

environment:
sdk: ^3.8.1
Expand Down
11 changes: 3 additions & 8 deletions code/cli/test/fsm_contract_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -132,20 +132,15 @@ void main() {
analyzeTransition.operations!.prechecks,
containsAll(['diagnosis_exists', 'index_exists', 'confirmations_exists']),
);
expect(
analyzeTransition.operations!.commitPolicy,
'commit_analysis_boundary',
);
// cleanrooms/ is git-ignored, so no boundary commit is attempted.
expect(analyzeTransition.operations!.commitPolicy, 'none');
expect(analyzeTransition.operations!.promptFragmentId, 'analyze_to_plan');

expect(planTransition.allowed, isTrue);
expect(planTransition.to, FsmState.execute);
expect(planTransition.operations, isNotNull);
expect(planTransition.operations!.prechecks, contains('plan_approved'));
expect(
planTransition.operations!.commitPolicy,
'commit_plan_boundary',
);
expect(planTransition.operations!.commitPolicy, 'none');
expect(planTransition.operations!.promptFragmentId, 'plan_to_execute');
});

Expand Down
7 changes: 5 additions & 2 deletions code/cli/test/fsm_transition_integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,16 @@ void main() {
expect(t1.promptFragmentId, isNotNull);
current = t1.nextState!;

// cleanrooms/ is ephemeral and git-ignored: the analysis and plan
// boundaries no longer create commits (a boundary commit would fail on a
// real repo, where cleanrooms/ is ignored).
final analysisCommitsBefore = _commitCount(tempDir.path);
_writeDiagnosis(tempDir.path, branch, 'diagnosis ready');
final t2 = await transition('complete_analysis');
expect(t2.allowed, isTrue);
expect(t2.nextState, 'PLAN');
expect(t2.promptFragmentId, isNotNull);
expect(_commitCount(tempDir.path), analysisCommitsBefore + 1);
expect(_commitCount(tempDir.path), analysisCommitsBefore);
current = t2.nextState!;

final planCommitsBefore = _commitCount(tempDir.path);
Expand All @@ -176,7 +179,7 @@ void main() {
expect(t3.allowed, isTrue);
expect(t3.nextState, 'EXECUTE');
expect(t3.promptFragmentId, isNotNull);
expect(_commitCount(tempDir.path), planCommitsBefore + 1);
expect(_commitCount(tempDir.path), planCommitsBefore);
current = t3.nextState!;

final t4 = await transition('finish_execute');
Expand Down
168 changes: 13 additions & 155 deletions code/cli/test/fsm_transition_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ void main() {
});

test(
'returns prompt descriptor for ANALYZE -> PLAN after boundary commit',
'returns prompt descriptor for ANALYZE -> PLAN without a boundary commit',
() async {
const branch = '51-idle-execution-guardrails';
_initGitRepo(tempDir.path, branch: branch);
Expand Down Expand Up @@ -76,7 +76,9 @@ void main() {
output.toText(),
contains('Write inside the CLI-created template and keep frontmatter unchanged.'),
);
expect(_commitCount(tempDir.path), commitsBefore + 1);
// cleanrooms/ is ephemeral and git-ignored: the transition must NOT
// create a commit (a boundary commit would fail on a real repo).
expect(_commitCount(tempDir.path), commitsBefore);

// Verify state was actually updated
final stateContent = File(
Expand All @@ -91,18 +93,9 @@ void main() {
expect(runTrace.existsSync(), isTrue);
final traceContent = runTrace.readAsStringSync();
expect(traceContent, contains('event_class: sensor_run'));
expect(traceContent, contains('event_class: tool_activity'));
expect(traceContent, contains('tool_class: git'));
expect(traceContent, contains('command_family: add'));
expect(traceContent, contains('command_family: commit'));
expect(traceContent, contains('gate: commit_analysis_boundary'));
expect(traceContent, contains('verdict: APPROVED'));
expect(
traceContent,
contains(
'authority: "cleanrooms/51-idle-execution-guardrails/analyze"',
),
);
// No boundary-commit machinery runs anymore.
expect(traceContent, isNot(contains('gate: commit_analysis_boundary')));
expect(traceContent, isNot(contains('command_family: commit')));
},
);

Expand Down Expand Up @@ -372,67 +365,6 @@ void main() {
);
});

test(
'fails closed when ANALYZE -> PLAN cannot create boundary commit',
() async {
const branch = '51-idle-execution-guardrails';

_initGitRepo(tempDir.path, branch: branch);
_writeAnalyzeIndex(tempDir.path, branch);
_writeConfirmations(tempDir.path, branch);
_writeDiagnosis(tempDir.path, branch, 'diagnosis already committed');
_git(tempDir.path, [
'add',
'--',
p.posix.join('cleanrooms', branch, 'analyze'),
]);
_git(tempDir.path, [
'commit',
'-m',
'analysis ready',
'--only',
'--',
p.posix.join('cleanrooms', branch, 'analyze'),
]);
_writeState(tempDir.path, 'ANALYZE', issue: '51');
final commitsBefore = _commitCount(tempDir.path);

final output = await StateTransitionCommand(
StateTransitionInput(
currentState: null,
event: 'complete_analysis',
workingDirectory: tempDir.path,
),
branchProvider: (_) async => branch,
).execute();

expect(output.allowed, isFalse);
expect(output.nextState, isNull);
expect(output.message, contains('commit'));
expect(_commitCount(tempDir.path), commitsBefore);

final stateContent = File(
_cycleStatePath(tempDir.path, branch),
).readAsStringSync();
expect(stateContent, contains('state: ANALYZE'));

final runTrace = File(
p.join(tempDir.path, 'cleanrooms', branch, 'run_trace.yaml'),
);
expect(runTrace.existsSync(), isTrue);
final traceContent = runTrace.readAsStringSync();
expect(traceContent, contains('event_class: sensor_run'));
expect(traceContent, contains('gate: commit_analysis_boundary'));
expect(traceContent, contains('verdict: FAILED'));
expect(
traceContent,
contains(
'authority: "cleanrooms/51-idle-execution-guardrails/analyze"',
),
);
},
);

test(
'fails precheck when commitment needs issue/branch and issue missing',
() async {
Expand Down Expand Up @@ -668,7 +600,7 @@ void main() {
);

test(
'transitions PLAN -> EXECUTE only after plan boundary commit',
'transitions PLAN -> EXECUTE without a boundary commit',
() async {
const branch = '51-idle-execution-guardrails';
_initGitRepo(tempDir.path, branch: branch);
Expand All @@ -690,7 +622,8 @@ void main() {
expect(output.promptFragmentId, 'plan_to_execute');
expect(output.requiredInstructions, ['coding-manifesto-review']);
expect(output.toText(), startsWith(output.message));
expect(_commitCount(tempDir.path), commitsBefore + 1);
// cleanrooms/ is ephemeral and git-ignored: no boundary commit.
expect(_commitCount(tempDir.path), commitsBefore);

final stateContent = File(
_cycleStatePath(tempDir.path, branch),
Expand All @@ -705,89 +638,14 @@ void main() {
final traceContent = runTrace.readAsStringSync();
expect(traceContent, contains('task_id: "51"'));
expect(traceContent, contains('event_class: sensor_run'));
expect(traceContent, contains('event_class: tool_activity'));
expect(traceContent, contains('tool_class: git'));
expect(traceContent, contains('command_family: add'));
expect(traceContent, contains('command_family: commit'));
expect(traceContent, contains('gate: plan_approved'));
expect(traceContent, contains('gate: commit_plan_boundary'));
expect(traceContent, contains('sensor_category: pre_transition'));
expect(traceContent, contains('verdict: APPROVED'));
expect(
traceContent,
contains(
'authority: "cleanrooms/51-idle-execution-guardrails/plan.md"',
),
);
expect(traceContent, contains('transition_event: approve_plan'));
expect(traceContent, contains('outcome: allowed'));
},
);

test(
'fails closed when PLAN -> EXECUTE cannot create boundary commit',
() async {
const branch = '51-idle-execution-guardrails';
final planPath = p.posix.join('cleanrooms', branch, 'plan.md');

_initGitRepo(tempDir.path, branch: branch);
_writePlan(tempDir.path, branch, '# committed plan\n');
_git(tempDir.path, ['add', '--', planPath]);
_git(tempDir.path, [
'commit',
'-m',
'plan ready',
'--only',
'--',
planPath,
]);
_writeState(tempDir.path, 'PLAN', issue: '51');
final commitsBefore = _commitCount(tempDir.path);

final output = await StateTransitionCommand(
StateTransitionInput(
currentState: null,
event: 'approve_plan',
workingDirectory: tempDir.path,
),
branchProvider: (_) async => branch,
).execute();

expect(output.allowed, isFalse);
expect(output.nextState, isNull);
expect(output.message, contains('commit'));
expect(_commitCount(tempDir.path), commitsBefore);

final stateContent = File(
_cycleStatePath(tempDir.path, branch),
).readAsStringSync();
expect(stateContent, contains('state: PLAN'));

final runTrace = File(
p.join(tempDir.path, 'cleanrooms', branch, 'run_trace.yaml'),
);
expect(runTrace.existsSync(), isTrue);
final traceContent = runTrace.readAsStringSync();
expect(traceContent, contains('event_class: transition'));
expect(traceContent, contains('event_class: sensor_run'));
expect(traceContent, contains('event_class: tool_activity'));
expect(traceContent, contains('tool_class: git'));
expect(traceContent, contains('command_family: add'));
expect(traceContent, contains('command_family: commit'));
expect(traceContent, contains('outcome: failed'));
expect(traceContent, contains('transition_event: approve_plan'));
expect(traceContent, contains('gate: commit_plan_boundary'));
expect(traceContent, contains('verdict: FAILED'));
expect(traceContent, contains('to_state: EXECUTE'));
expect(traceContent, contains('outcome: blocked'));
expect(traceContent, contains('event_class: block'));
expect(traceContent, contains('blocking_boundary: boundary_commit'));
expect(
traceContent,
contains(
'authoritative_surface: "cleanrooms/51-idle-execution-guardrails/plan.md"',
),
);
// No boundary-commit machinery runs anymore.
expect(traceContent, isNot(contains('gate: commit_plan_boundary')));
expect(traceContent, isNot(contains('command_family: commit')));
},
);

Expand Down
2 changes: 1 addition & 1 deletion code/site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
<h1><span class="ape">Inquiry</span></h1>
<p class="primary-tagline">Analyze. Plan. Execute.</p>
<p class="secondary-tagline">Available for OpenCode and GitHub Copilot. More hosts coming soon.</p>
<span class="badge">v0.21.0</span>
<span class="badge">v0.21.1</span>
<p class="secondary-tagline">Public entry surface. For the canonical documentation map, start in the repository docs.</p>

<div class="hero-install" id="install">
Expand Down
Loading