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
8 changes: 8 additions & 0 deletions code/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ 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.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`.

### Changed
- **The branch-policy failure now teaches instead of asserting.** `feature_branch_selected` returned a bare `Use issue-linked feature branch matching active issue`. It now names the expected pattern, gives an example, shows the actual branch, states why a `feat/<n>-…` prefix is rejected (the name must be a single segment), and points at `iq implementation start --issue <N>` — the command that produces a valid branch in one step.
- **`inquiry-start.md` no longer teaches the model to run `git`/`mkdir` or hand-write scaffold files.** The bootstrap is owned by the CLI; the instruction simply invokes `iq implementation start --issue <N>`, honoring the principle that every mechanical process is an `iq` command.

## [0.20.0]
### Added
- **Every command declares its contract, and the help *is* that contract.** Each command declares its parameters (`CliParam`) on its `Input`, so one declaration feeds three things that were maintained by hand and drifted apart: the help text, the machine-readable catalog, and the parser.
Expand Down
94 changes: 26 additions & 68 deletions code/cli/assets/instructions/inquiry-start.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: inquiry-start
description: 'Protocol for starting work on an existing GitHub issue. Verifies the issue, prepares the branch and cleanroom, and transitions to ANALYZE state.'
description: 'Protocol for starting work on an existing GitHub issue. Verifies the issue and opens the cycle with a single command, which prepares the branch and cleanroom and transitions to ANALYZE.'
---

# inquiry-start - Operational Start Protocol
Expand All @@ -9,9 +9,7 @@ description: 'Protocol for starting work on an existing GitHub issue. Verifies t

Run iq doctor first and stop on any failed check.
Verify the issue already exists with gh issue view.
Create branch NNN-slug and cleanrooms/NNN-slug/analyze.
Create analyze/index.md for the cleanroom.
Transition with iq fsm transition --event start_analyze --issue NNN.
Open the implementation with iq implementation start --issue NNN, which derives the NNN-slug branch, checks it out, scaffolds the cleanroom, and transitions to ANALYZE.
Confirm iq fsm state reports ANALYZE for that issue.

## When to Use
Expand Down Expand Up @@ -49,91 +47,51 @@ All checks must pass. Do not proceed if any check fails.
gh issue view <NNN> --json number,title,state
```

Confirm the issue already exists and extract `number` and `title` from the JSON response.
Confirm the issue already exists. You do NOT need to read the title to build a
branch name — `iq implementation start` derives it from the issue itself.

### Step 3: Generate Slug

Transform the confirmed issue title into a slug:

1. Lowercase the title
2. Replace spaces with hyphens (`-`)
3. Remove special characters (keep only `a-z`, `0-9`, `-`)
4. Limit to 50 characters
5. Trim trailing hyphens

**Examples:**
- "Fix login timeout" -> `fix-login-timeout`
- "Add dark mode support!!!" -> `add-dark-mode-support`
- "URGENT: Database migration script" -> `urgent-database-migration-script`

### Step 4: Create Branch

Format: `<NNN>-<slug>`

```bash
git checkout -b <NNN>-<slug>
```

**Examples:**
- Issue #37 "Fix login timeout" -> `git checkout -b 037-fix-login-timeout`
- Issue #142 "Add dark mode" -> `git checkout -b 142-add-dark-mode`

Note: Pad issue numbers less than 100 with leading zeros for sort consistency.

### Step 5: Create Working Directory
### Step 3: Open the Cycle

```bash
mkdir -p cleanrooms/<NNN>-<slug>/analyze/
iq implementation start --issue <NNN>
```

This creates the analysis directory for SOCRATES to work in during ANALYZE phase.

### Step 6: Create index.md
This single command owns every mechanical step of the bootstrap — there is no
`git checkout -b`, no `mkdir`, no hand-writing of scaffold files:

Create `cleanrooms/<NNN>-<slug>/analyze/index.md` with this template:
1. Initializes the Inquiry workspace if it is missing (no separate `iq init`).
2. Reads the issue title and derives the branch `<NNN>-<slug>` (issue number
left-padded to three digits, e.g. `#37 → 037-fix-login-timeout`).
3. Creates and checks out that branch (or checks it out if it already exists).
4. Fires the `start_analyze` transition, whose effect scaffolds
`cleanrooms/<NNN>-<slug>/analyze/` (index.md, confirmations.md, diagnosis.md)
and the issue mirror.

```markdown
# Analyze Phase - Index
The command reports the branch, the cleanroom path, and the new state. Do NOT
write `.inquiry` state directly — all state mutations go through `iq` commands.

**Issue:** #<NNN> - <title>
**Branch:** <NNN>-<slug>
**Phase:** ANALYZE
**Status:** In progress
### Step 4: Verify

---

## Documents

| # | File | Description |
|---|------|-------------|
```

### Step 7: Transition to ANALYZE

Execute the CLI transition command with the issue number:

```
iq fsm transition --event start_analyze --issue <NNN>
```bash
iq fsm state
```

This transitions the FSM to ANALYZE and auto-activates the analysis sub-agent. Do NOT write `.inquiry/state.yaml` directly - all state mutations go through `iq` commands.

### Step 8: Verify Transition

Run `iq fsm state` to confirm the state is now ANALYZE with the correct issue number.
Confirm the state is now ANALYZE with the correct issue number.

## Verification

After completing all steps, verify:

- [ ] The issue already exists and `gh issue view <NNN> --json number,title,state` succeeds
- [ ] `iq implementation start --issue <NNN>` reported ANALYZE
- [ ] Branch exists: `git branch --show-current` returns `<NNN>-<slug>`
- [ ] Directory exists: `cleanrooms/<NNN>-<slug>/analyze/index.md`
- [ ] Directory exists: `cleanrooms/<NNN>-<slug>/analyze/`
- [ ] State updated: `iq fsm state` shows ANALYZE with the issue number

## Notes

- This skill is executed by the scheduler APE, not by a human
- TRIAGE owns issue creation or confirmation through `issue-create`
- The scheduler reads this document and executes commands step by step
- If any step fails, the scheduler should report the error and remain in IDLE
- Mechanical bootstrap (slug, branch, cleanroom, transition) is owned by
`iq implementation start` — the scheduler invokes it, it does not reproduce those steps
- If any step fails, the scheduler should report the error and remain in IDLE
2 changes: 2 additions & 0 deletions code/cli/lib/inquiry_cli.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'assets.dart';
import 'modules/global/global_builder.dart';
import 'modules/fsm/fsm_builder.dart';
import 'modules/ape/ape_builder.dart';
import 'modules/implementation/implementation_builder.dart';
import 'modules/host/host_builder.dart';
import 'modules/issue/issue_builder.dart';
import 'modules/specification/specification_builder.dart';
Expand Down Expand Up @@ -64,6 +65,7 @@ Future<int> runInquiry(
cli.module('host', (m) => buildHostModule(m, deployer: deployer, cleaner: cleaner));
cli.module('fsm', (m) => buildFsmModule(m, assets: assets));
cli.module('ape', (m) => buildApeModule(m, assets: assets));
cli.module('implementation', (m) => buildImplementationModule(m, assets: assets));
cli.module(
'specification',
(m) => buildSpecificationModule(m, assets: assets),
Expand Down
18 changes: 17 additions & 1 deletion code/cli/lib/modules/fsm/commands/transition.dart
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,7 @@ class StateTransitionCommand
issue: issue,
promptFragmentId: promptFragmentId,
);
return 'ERROR_PRECONDITION_BRANCH_POLICY: Use issue-linked feature branch matching active issue';
return _branchPolicyError(branch: branch, issue: issue);
}
_recordPrecheckSensor(
executor,
Expand Down Expand Up @@ -1264,6 +1264,22 @@ class StateTransitionCommand
);
}

/// The branch-policy failure, written to teach: it names the expected pattern,
/// gives a concrete example, shows the actual branch, and points at the command
/// that produces a valid branch in one step (AC2).
String _branchPolicyError({required String branch, String? issue}) {
final issueRef = (issue != null && issue.trim().isNotEmpty)
? issue.trim()
: '<issue>';
final actual = branch.trim().isEmpty ? '(no branch)' : branch.trim();
return 'ERROR_PRECONDITION_BRANCH_POLICY: the current branch "$actual" is not '
'linked to issue #$issueRef.\n'
'Expected a branch named "<NNN>-<slug>" that starts with "$issueRef-" '
'(e.g. "$issueRef-fix-login"). A branch under a prefix like '
'"feat/$issueRef-…" does NOT qualify — the name must be a single segment.\n'
'Create it and enter ANALYZE in one step with: iq implementation start --issue $issueRef';
}

bool _isIssueLinkedFeatureBranch(String branch, String? issue) {
final normalizedBranch = branch.trim();
if (normalizedBranch.isEmpty ||
Expand Down
64 changes: 64 additions & 0 deletions code/cli/lib/modules/implementation/branch_name.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/// Deterministic branch-name derivation for a cycle.
///
/// The `<NNN>-<slug>` convention is what the FSM's `feature_branch_selected`
/// precheck validates (`fsm/commands/transition.dart` → `_isIssueLinkedFeatureBranch`).
/// Historically the LLM derived this by hand following `assets/instructions/
/// inquiry-start.md`; moving it here makes it a deterministic CLI step (a
/// "mechanical process is an `iq` command", not an instruction).
library;

/// Common Spanish/Latin accented letters folded to ASCII, so a title like
/// "incluir código" yields `incluir-codigo`, not `incluir-c-digo`.
const Map<String, String> _accentFolds = {
'á': 'a', 'à': 'a', 'â': 'a', 'ä': 'a', 'ã': 'a',
'é': 'e', 'è': 'e', 'ê': 'e', 'ë': 'e',
'í': 'i', 'ì': 'i', 'î': 'i', 'ï': 'i',
'ó': 'o', 'ò': 'o', 'ô': 'o', 'ö': 'o', 'õ': 'o',
'ú': 'u', 'ù': 'u', 'û': 'u', 'ü': 'u',
'ñ': 'n', 'ç': 'c',
};

/// Derives a kebab-case slug from an issue [title].
///
/// Rules (matching the documented convention, now enforced in code): lowercase,
/// fold accents, replace every run of non-alphanumerics with a single hyphen,
/// trim leading/trailing hyphens, cap at 50 characters (re-trimming any hyphen
/// the cut leaves dangling). Returns an empty string when the title has no
/// alphanumeric content.
String deriveSlug(String title) {
final lowered = title.toLowerCase();
final folded = StringBuffer();
for (final ch in lowered.split('')) {
folded.write(_accentFolds[ch] ?? ch);
}

var slug = folded
.toString()
.replaceAll(RegExp(r'[^a-z0-9]+'), '-')
.replaceAll(RegExp(r'^-+|-+$'), '');

if (slug.length > 50) {
slug = slug.substring(0, 50).replaceAll(RegExp(r'-+$'), '');
}
return slug;
}

/// Left-pads an issue number to at least three digits, matching the convention
/// (`#37 → 037`, `#142 → 142`, `#295 → 295`).
String padIssueNumber(String issue) {
final n = int.tryParse(issue.trim());
if (n == null) return issue.trim();
return n.toString().padLeft(3, '0');
}

/// The full branch name for an [issue] with the given [title]:
/// `<NNN>-<slug>`. Throws [ArgumentError] when the title yields no slug.
String branchNameFor({required String issue, required String title}) {
final slug = deriveSlug(title);
if (slug.isEmpty) {
throw ArgumentError(
'issue title "$title" has no alphanumeric content to build a branch slug from',
);
}
return '${padIssueNumber(issue)}-$slug';
}
Loading
Loading