Skip to content

Commit 7485cd2

Browse files
Merge pull request #257 from Priivacy-ai/codex/branch-contract-and-log-hardening-2x-20260306
2.x: deterministic branch contract + prompt hardening for agent flows
2 parents 6a53825 + 5d9ace5 commit 7485cd2

File tree

35 files changed

+806
-264
lines changed

35 files changed

+806
-264
lines changed

src/specify_cli/cli/commands/agent/feature.py

Lines changed: 186 additions & 39 deletions
Large diffs are not rendered by default.

src/specify_cli/cli/commands/merge.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import typer
1616

17+
from specify_cli import __version__ as SPEC_KITTY_VERSION
1718
from specify_cli.cli import StepTracker
1819
from specify_cli.cli.helpers import check_version_compatibility, console, show_banner
1920
from specify_cli.core.git_preflight import (
@@ -75,7 +76,9 @@ def _enforce_git_preflight(repo_root: Path, *, json_output: bool) -> None:
7576
command_name="spec-kitty merge",
7677
)
7778
if json_output:
78-
print(json.dumps(payload))
79+
enriched = dict(payload)
80+
enriched["spec_kitty_version"] = SPEC_KITTY_VERSION
81+
print(json.dumps(enriched))
7982
else:
8083
console.print(f"[red]Error:[/red] {payload['error']}")
8184
for cmd in payload.get("remediation", []):
@@ -722,6 +725,7 @@ def merge(
722725

723726
if json_output and not dry_run:
724727
print(json.dumps({
728+
"spec_kitty_version": SPEC_KITTY_VERSION,
725729
"error": "--json is currently supported with --dry-run only.",
726730
}))
727731
raise typer.Exit(1)
@@ -730,6 +734,7 @@ def merge(
730734
_, current_branch, _ = run_command(["git", "rev-parse", "--abbrev-ref", "HEAD"], capture=True)
731735
if current_branch == target_branch and not feature:
732736
print(json.dumps({
737+
"spec_kitty_version": SPEC_KITTY_VERSION,
733738
"error": f"Already on {target_branch}; pass --feature <slug> for workspace-per-WP planning.",
734739
}))
735740
raise typer.Exit(1)
@@ -772,6 +777,7 @@ def merge(
772777
steps.append(f"git branch -d {branch}")
773778

774779
print(json.dumps({
780+
"spec_kitty_version": SPEC_KITTY_VERSION,
775781
"feature_slug": feature_slug,
776782
"target_branch": target_branch,
777783
"all_wp_branches": [branch for _, _, branch in merge_plan["all_wp_workspaces"]], # type: ignore[index]
@@ -802,6 +808,7 @@ def merge(
802808
planned_steps.append(f"git branch -d {feature_slug}")
803809

804810
print(json.dumps({
811+
"spec_kitty_version": SPEC_KITTY_VERSION,
805812
"feature_slug": feature_slug,
806813
"target_branch": target_branch,
807814
"all_wp_branches": [],

src/specify_cli/cli/commands/upgrade.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def upgrade(
172172
# Check if this is a Spec Kitty project
173173
if not kittify_dir.exists() and not specify_dir.exists():
174174
if json_output:
175-
console.print(json.dumps({"error": "Not a Spec Kitty project"}))
175+
print(json.dumps({"error": "Not a Spec Kitty project"}))
176176
else:
177177
console.print("[red]Error:[/red] Not a Spec Kitty project.")
178178
console.print(
@@ -234,7 +234,7 @@ def upgrade(
234234

235235
if json_output:
236236
warnings = [auto_commit_warning] if auto_commit_warning else []
237-
console.print(
237+
print(
238238
json.dumps(
239239
{
240240
"status": "up_to_date",
@@ -347,7 +347,7 @@ def upgrade(
347347
"auto_committed": auto_committed,
348348
"auto_commit_paths": auto_commit_paths,
349349
}
350-
console.print(json.dumps(output, indent=2))
350+
print(json.dumps(output))
351351
return
352352

353353
# Display results

src/specify_cli/cli/helpers.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,21 @@ def _format_simple_help(group: TyperGroup, ctx, formatter) -> None:
7171

7272
def _should_render_banner_for_invocation(argv: list[str] | None = None) -> bool:
7373
"""Return True only for invocations that should render ASCII art."""
74+
# Agent/tool contexts should never receive decorative banner output.
75+
# It pollutes deterministic parsing and wastes tokens.
76+
if os.environ.get("SPEC_KITTY_NO_BANNER", "").strip().lower() in {"1", "true", "yes", "on"}:
77+
return False
78+
79+
agent_env_markers = (
80+
"CLAUDECODE",
81+
"CLAUDE_CODE",
82+
"CODEX",
83+
"OPENCODE",
84+
"CURSOR_TRACE_ID",
85+
)
86+
if any(key in os.environ for key in agent_env_markers):
87+
return False
88+
7489
tokens = [token.strip().lower() for token in (argv if argv is not None else sys.argv[1:]) if token.strip()]
7590
if "--version" in tokens or "-v" in tokens:
7691
return True

src/specify_cli/core/feature_detection.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,43 @@ def _list_all_features(repo_root: Path) -> list[str]:
152152
return sorted(features)
153153

154154

155+
def _resolve_numeric_feature_slug(
156+
feature_number: str,
157+
repo_root: Path,
158+
*,
159+
mode: Literal["strict", "lenient"],
160+
) -> Optional[str]:
161+
"""Resolve a 3-digit feature number (e.g., ``019``) to full slug.
162+
163+
This is a compatibility affordance for agents that pass only the numeric
164+
feature id after parsing logs or UI text.
165+
"""
166+
all_features = _list_all_features(repo_root)
167+
matches = [slug for slug in all_features if slug.startswith(f"{feature_number}-")]
168+
169+
if len(matches) == 1:
170+
return matches[0]
171+
172+
if len(matches) > 1:
173+
error_msg = (
174+
f"Feature number '{feature_number}' matches multiple features:\n"
175+
+ "\n".join(f" - {slug}" for slug in matches)
176+
+ "\n\nUse the full slug with --feature <###-feature-name>."
177+
)
178+
if mode == "strict":
179+
raise MultipleFeaturesError(matches, error_msg)
180+
return None
181+
182+
error_msg = (
183+
f"No feature found for number '{feature_number}'.\n\n"
184+
+ "Available features:\n"
185+
+ "\n".join(f" - {slug}" for slug in all_features[:20])
186+
)
187+
if mode == "strict":
188+
raise NoFeatureFoundError(error_msg)
189+
return None
190+
191+
155192
def _detect_from_git_branch(repo_root: Path) -> Optional[str]:
156193
"""Detect feature from git branch name.
157194
@@ -413,12 +450,26 @@ def detect_feature(
413450
# Priority 1: Explicit --feature parameter
414451
if explicit_feature:
415452
detected_slug = explicit_feature.strip()
416-
detection_method = "explicit"
453+
if re.fullmatch(r"\d{3}", detected_slug):
454+
resolved = _resolve_numeric_feature_slug(detected_slug, repo_root, mode=mode)
455+
if resolved is None:
456+
return None
457+
detected_slug = resolved
458+
detection_method = "explicit_number"
459+
else:
460+
detection_method = "explicit"
417461

418462
# Priority 2: SPECIFY_FEATURE environment variable
419463
elif "SPECIFY_FEATURE" in env and env["SPECIFY_FEATURE"].strip():
420464
detected_slug = env["SPECIFY_FEATURE"].strip()
421-
detection_method = "env_var"
465+
if re.fullmatch(r"\d{3}", detected_slug):
466+
resolved = _resolve_numeric_feature_slug(detected_slug, repo_root, mode=mode)
467+
if resolved is None:
468+
return None
469+
detected_slug = resolved
470+
detection_method = "env_var_number"
471+
else:
472+
detection_method = "env_var"
422473

423474
# Priority 3: Git branch name
424475
elif (branch_slug := _detect_from_git_branch(repo_root)):

src/specify_cli/missions/documentation/command-templates/plan.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,20 @@ You **MUST** consider the user input before proceeding (if not empty).
1717

1818
## Location Pre-flight Check
1919

20-
Verify you are in the main repository (not a worktree). Planning happens in main for ALL missions.
20+
Verify you are in the primary repository checkout (not a worktree). Planning happens on the feature target branch for all missions.
21+
22+
1. Run `spec-kitty agent feature setup-plan --json` and capture:
23+
- `target_branch` / `base_branch`
24+
- `TARGET_BRANCH` / `BASE_BRANCH`
25+
- `feature_dir`
26+
27+
Treat this JSON as the canonical branch contract.
2128

2229
```bash
23-
git branch --show-current # Should show "main"
30+
git branch --show-current # Should match TARGET_BRANCH from setup-plan output
2431
```
2532

26-
**Note**: Planning in main is standard for all spec-kitty missions. Implementation happens in per-WP worktrees.
33+
**Note**: Planning runs on the feature target branch. Implementation happens later in per-WP worktrees.
2734

2835
---
2936

@@ -57,7 +64,7 @@ For documentation missions, planning interrogation is lighter than software-dev:
5764

5865
## Outline
5966

60-
1. **Setup**: Run `spec-kitty agent feature setup-plan --json` to initialize plan.md
67+
1. **Setup**: Use the pre-flight `setup-plan --json` output to initialize plan.md and keep `target_branch/base_branch` in context.
6168

6269
2. **Load context**: Read spec.md, meta.json (especially `documentation_state`)
6370

src/specify_cli/missions/documentation/command-templates/tasks.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,26 @@ You **MUST** consider the user input before proceeding (if not empty).
1717

1818
## Location Pre-flight Check
1919

20-
Verify you are in the main repository (not a worktree). Task generation happens in main for ALL missions.
20+
Verify you are in the primary repository checkout (not a worktree). Task generation happens on the feature target branch for all missions.
21+
22+
1. Run `spec-kitty agent feature check-prerequisites --json --paths-only --include-tasks` and capture:
23+
- `target_branch` / `base_branch`
24+
- `TARGET_BRANCH` / `BASE_BRANCH`
25+
- `feature_dir`
26+
27+
Treat this JSON as the canonical branch contract for this command.
2128

2229
```bash
23-
git branch --show-current # Should show "main"
30+
git branch --show-current # Should match TARGET_BRANCH from check-prerequisites JSON
2431
```
2532

26-
**Note**: Task generation in main is standard for all spec-kitty missions. Implementation happens in per-WP worktrees.
33+
**Note**: Task generation happens on the feature target branch. Implementation happens later in per-WP worktrees.
2734

2835
---
2936

3037
## Outline
3138

32-
1. **Setup**: Run `spec-kitty agent feature check-prerequisites --json --paths-only --include-tasks`
39+
1. **Setup**: Use the pre-flight `check-prerequisites` JSON and keep `feature_dir` plus `target_branch/base_branch` in context.
3340

3441
2. **Load design documents**:
3542
- spec.md (documentation goals, selected Divio types)

src/specify_cli/missions/documentation/templates/task-prompt-template.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ Use language identifiers in code blocks: ````python`, ````bash`
9595
1. Scroll to the bottom of this file (Activity Log section below "Valid lanes")
9696
2. **APPEND the new entry at the END** (do NOT prepend or insert in middle)
9797
3. Use exact format: `- YYYY-MM-DDTHH:MM:SSZ – agent_id – lane=<lane> – <action>`
98-
4. Timestamp MUST be current time in UTC (check with `date -u "+%Y-%m-%dT%H:%M:%SZ"`)
98+
4. Timestamp MUST be current time in UTC (ISO-8601 `YYYY-MM-DDTHH:MM:SSZ`) and should be generated without shell commands (prefer `NOW_UTC_ISO` from `check-prerequisites --json --paths-only`)
9999
5. Lane MUST match the frontmatter `lane:` field exactly
100100
6. Agent ID should identify who made the change (claude-sonnet-4-5, codex, etc.)
101101

src/specify_cli/missions/research/command-templates/specify.md

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ cd /path/to/project/root # Your planning repository
1616

1717
# All planning artifacts are created in the planning repo and committed:
1818
# - kitty-specs/###-feature/spec.md → Created in planning repo
19-
# - Committed to target branch (meta.json → target_branch)
19+
# - Committed to target branch (from create-feature JSON: target_branch/base_branch)
2020
# - NO worktrees created
2121
```
2222

@@ -100,7 +100,7 @@ During discovery, you MUST ask:
100100

101101
- Work in: **Planning repository** (not a worktree)
102102
- Creates: `kitty-specs/###-feature/spec.md`
103-
- Commits to: target branch (`meta.json``target_branch`)
103+
- Commits to: target branch (from `create-feature --json``target_branch`)
104104

105105
## Outline
106106

@@ -123,36 +123,19 @@ When discovery is complete, run:
123123
spec-kitty agent feature create-feature "<slug>" --json
124124
```
125125

126-
Parse the JSON output for `feature` and `feature_dir`.
126+
Parse the JSON output for `feature`, `feature_dir`, `spec_file`, and `meta_file`.
127127

128-
### 3. Create meta.json with deliverables_path
128+
### 3. Update existing meta.json with deliverables_path
129129

130-
**CRITICAL**: Include `deliverables_path` in meta.json:
130+
`create-feature` already created `meta.json`. Read it first, then update only needed fields:
131131

132-
```json
133-
{
134-
"feature_number": "<number>",
135-
"slug": "<full-slug>",
136-
"friendly_name": "<Research Title>",
137-
"mission": "research",
138-
"deliverables_path": "<confirmed-path>",
139-
"source_description": "$ARGUMENTS",
140-
"created_at": "<ISO timestamp>"
141-
}
142-
```
132+
- `mission`: set to `"research"`
133+
- `deliverables_path`: set to the confirmed path
134+
- `friendly_name`: confirmed research title
135+
- optional `source_description`
143136

144-
Example with default path:
145-
```json
146-
{
147-
"feature_number": "018",
148-
"slug": "018-market-research",
149-
"friendly_name": "Market Research Analysis",
150-
"mission": "research",
151-
"deliverables_path": "docs/research/018-market-research/",
152-
"source_description": "Research the competitive landscape",
153-
"created_at": "2025-01-25T10:00:00Z"
154-
}
155-
```
137+
Preserve existing identity and timing fields (`feature_number`, `slug`, `created_at`, `target_branch`).
138+
Do **not** recreate the file or regenerate timestamps via shell commands.
156139

157140
### 4. Load Research Spec Template
158141

@@ -168,7 +151,7 @@ Fill in:
168151

169152
### 6. Write Specification
170153

171-
Write to `<feature_dir>/spec.md`
154+
Update the existing `<feature_dir>/spec.md`
172155

173156
### 7. Validation
174157

src/specify_cli/missions/research/command-templates/tasks.md

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,15 @@ You **MUST** consider the user input before proceeding (if not empty).
1919

2020
Verify you are in the planning repository (not a worktree). Task generation happens on the target branch for ALL missions.
2121

22+
1. Run `spec-kitty agent feature check-prerequisites --json --paths-only --include-tasks` from the repository root and capture:
23+
- `target_branch` / `base_branch`
24+
- `TARGET_BRANCH` / `BASE_BRANCH`
25+
- `feature_dir`
26+
27+
Treat this JSON as canonical branch context for this command. Do not infer from `meta.json`.
28+
2229
```bash
23-
git branch --show-current # Should show the target branch (meta.json → target_branch)
30+
git branch --show-current # Should match TARGET_BRANCH from check-prerequisites JSON
2431
```
2532

2633
**Note**: Task generation in the target branch is standard for all spec-kitty missions. Implementation happens in per-WP worktrees.
@@ -29,9 +36,12 @@ git branch --show-current # Should show the target branch (meta.json → target
2936

3037
## Outline
3138

32-
1. **Setup**: Run `spec-kitty agent feature check-prerequisites --json --paths-only --include-tasks`
39+
1. **Setup**: Use the `check-prerequisites` JSON from Location Pre-flight and capture:
40+
- `feature_dir`
41+
- `target_branch` / `base_branch`
3342

34-
**CRITICAL**: The command returns JSON with `FEATURE_DIR` as an ABSOLUTE path (e.g., `/Users/robert/Code/project/kitty-specs/015-research-topic`).
43+
**CRITICAL**: The command returns JSON with `feature_dir` as an ABSOLUTE path (e.g., `/Users/robert/Code/project/kitty-specs/015-research-topic`).
44+
It also returns `runtime_vars.now_utc_iso` (`NOW_UTC_ISO`) for deterministic timestamp fields.
3545

3646
**YOU MUST USE THIS PATH** for ALL subsequent file operations.
3747

@@ -108,7 +118,7 @@ git branch --show-current # Should show the target branch (meta.json → target
108118
- **P3 (polish)**: Quality validation, external review
109119

110120
5. **Write `tasks.md`**:
111-
- Location: `FEATURE_DIR/tasks.md`
121+
- Location: `feature_dir/tasks.md`
112122
- Use `templates/tasks-template.md` from research mission
113123
- Include work packages with subtasks
114124
- Mark parallel opportunities (`[P]`)
@@ -117,12 +127,14 @@ git branch --show-current # Should show the target branch (meta.json → target
117127

118128
6. **Generate prompt files**:
119129

120-
**CRITICAL PATH RULE**: All work package files MUST be created in a FLAT `FEATURE_DIR/tasks/` directory, NOT in subdirectories!
130+
**CRITICAL PATH RULE**: All work package files MUST be created in a FLAT `feature_dir/tasks/` directory, NOT in subdirectories!
121131

122-
- Create flat `FEATURE_DIR/tasks/` directory (no subdirectories!)
132+
- Use `artifact_dirs.tasks_dir` when available.
133+
- Do **not** shell out with `mkdir -p`; `create-feature` already creates `tasks/` in normal flow.
134+
- If `tasks/` is missing unexpectedly, report the mismatch instead of improvising shell directory setup.
123135
- For each work package:
124136
- Derive a kebab-case slug from the title; filename: `WPxx-slug.md`
125-
- Full path: `FEATURE_DIR/tasks/WP01-literature-search.md`
137+
- Full path: `feature_dir/tasks/WP01-literature-search.md`
126138
- Use `templates/task-prompt-template.md` to capture:
127139
- **YAML frontmatter with `lane: "planned"`** (CRITICAL - this is how review finds WPs!)
128140
- `work_package_id`, `subtasks` array, `dependencies`, history entry

0 commit comments

Comments
 (0)