@geminix/gxpm 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +148 -0
- package/CANON.md +53 -0
- package/CLAUDE.md +60 -0
- package/CONTEXT.md +49 -0
- package/DEBUG.md +59 -0
- package/ISSUE_CONTEXT.md +25 -0
- package/README.md +143 -0
- package/VERSION +1 -0
- package/agents/cleanup-auditor/cleanup-auditor.md +56 -0
- package/agents/grill-master.md +26 -0
- package/agents/implementer.md +32 -0
- package/agents/review-army/accessibility-reviewer.md +54 -0
- package/agents/review-army/code-quality-reviewer.md +54 -0
- package/agents/review-army/security-reviewer.md +56 -0
- package/agents/review-army/spec-compliance-reviewer.md +51 -0
- package/agents/review-army/test-reviewer.md +55 -0
- package/agents/reviewer.md +59 -0
- package/agents/ship-audit-army/docs-auditor.md +53 -0
- package/agents/ship-audit-army/performance-auditor.md +52 -0
- package/agents/ship-audit-army/security-auditor.md +52 -0
- package/agents/specifier.md +55 -0
- package/agents/triage-officer.md +27 -0
- package/bin/gxpm +17 -0
- package/bin/gxpm-browser +17 -0
- package/bin/gxpm-config +15 -0
- package/bin/gxpm-eval +13 -0
- package/bin/gxpm-global-discover +15 -0
- package/bin/gxpm-init +38 -0
- package/bin/gxpm-investigate +194 -0
- package/bin/gxpm-uninstall +15 -0
- package/bin/gxpm-update-check +165 -0
- package/commands/build.md +40 -0
- package/commands/help.md +53 -0
- package/commands/plan.md +34 -0
- package/commands/refine.md +46 -0
- package/commands/review.md +34 -0
- package/commands/ship.md +37 -0
- package/core/ac-check.ts +20 -0
- package/core/agent-runtime.ts +363 -0
- package/core/artifact-validator.ts +151 -0
- package/core/artifacts.ts +313 -0
- package/core/autopilot.ts +250 -0
- package/core/capabilities.ts +779 -0
- package/core/checkpoint.ts +370 -0
- package/core/cleanup.ts +32 -0
- package/core/command-probe.ts +82 -0
- package/core/config.ts +533 -0
- package/core/contracts/behavior-spec.schema.ts +38 -0
- package/core/contracts/converter.ts +61 -0
- package/core/contracts/host.ts +43 -0
- package/core/converters/converter.ts +93 -0
- package/core/converters/index.ts +8 -0
- package/core/converters/managed-artifact.ts +119 -0
- package/core/converters/parser.ts +159 -0
- package/core/converters/template-renderer.ts +35 -0
- package/core/converters/writer.ts +61 -0
- package/core/dag-executor.ts +426 -0
- package/core/dag-loader.ts +292 -0
- package/core/dag-schemas.ts +150 -0
- package/core/dispatch.ts +125 -0
- package/core/evidence.ts +148 -0
- package/core/gate.ts +269 -0
- package/core/hook-engine.ts +566 -0
- package/core/host-probe.ts +64 -0
- package/core/implement.ts +16 -0
- package/core/isolation-errors.ts +174 -0
- package/core/isolation-resolver.ts +921 -0
- package/core/issue-context.ts +381 -0
- package/core/issue-readiness.ts +457 -0
- package/core/issue-sync.ts +427 -0
- package/core/issues.ts +132 -0
- package/core/land.ts +108 -0
- package/core/orchestrator.ts +54 -0
- package/core/phase-artifact.ts +32 -0
- package/core/phase-gates.ts +130 -0
- package/core/phase-rewind.ts +94 -0
- package/core/plan-lint.ts +61 -0
- package/core/plan.ts +77 -0
- package/core/port-allocation.ts +50 -0
- package/core/pr-check.ts +15 -0
- package/core/preset-system/preset-resolver.ts +221 -0
- package/core/project-init-status.ts +127 -0
- package/core/qa.ts +15 -0
- package/core/resilience.ts +165 -0
- package/core/runs.ts +288 -0
- package/core/safe-path.test.ts +80 -0
- package/core/safe-path.ts +60 -0
- package/core/sdd-gate.test.ts +98 -0
- package/core/sdd-gate.ts +134 -0
- package/core/self-review.ts +62 -0
- package/core/session.ts +70 -0
- package/core/ship.ts +86 -0
- package/core/specify.ts +173 -0
- package/core/state.ts +1002 -0
- package/core/template-engine.ts +152 -0
- package/core/template-resolver.test.ts +70 -0
- package/core/template-resolver.ts +156 -0
- package/core/triage.ts +26 -0
- package/core/verify.ts +15 -0
- package/core/wiki-native.ts +2423 -0
- package/core/wiki.ts +27 -0
- package/core/workflow-event-emitter.ts +163 -0
- package/core/workflows/engine.ts +273 -0
- package/core/workflows/expressions.ts +76 -0
- package/core/workflows/index.ts +38 -0
- package/core/workflows/steps/command.ts +43 -0
- package/core/workflows/steps/gate.ts +47 -0
- package/core/workflows/steps/gxpm.ts +44 -0
- package/core/workflows/steps/linear.ts +31 -0
- package/core/workflows/steps/shell.ts +65 -0
- package/core/workflows/types.ts +62 -0
- package/core/workspace-runtime.ts +227 -0
- package/core/worktree-init-steps.ts +647 -0
- package/core/worktree-init.ts +330 -0
- package/core/worktree-owner.ts +143 -0
- package/docs/GXPM_VERIFY.md +98 -0
- package/docs/INSTALL_FOR_AGENTS.md +113 -0
- package/docs/README.md +57 -0
- package/docs/adr/adr-005-multi-platform-skill-converter.md +72 -0
- package/docs/agents/domain.md +30 -0
- package/docs/agents/issue-tracker.md +30 -0
- package/docs/agents/triage-labels.md +32 -0
- package/docs/architecture/gxpm-architecture-diagram.md +265 -0
- package/docs/architecture/gxpm-current-architecture.md +175 -0
- package/docs/architecture/gxpm-current-flow.md +278 -0
- package/docs/architecture/gxpm-replacement-architecture.md +211 -0
- package/docs/architecture/gxpm-target-architecture.md +449 -0
- package/docs/architecture/gxpm-v0-contract.md +311 -0
- package/docs/architecture/layered-workflow-boundaries.md +193 -0
- package/docs/architecture/preset-system.md +126 -0
- package/docs/architecture/scaffold-northstar.md +23 -0
- package/docs/brainstorms/2026-05-14-bdd-then-tdd-design.md +320 -0
- package/docs/brainstorms/README.md +22 -0
- package/docs/brainstorms/docs-knowledge-system-requirements.md +29 -0
- package/docs/governance/beta-skill-promotion.md +39 -0
- package/docs/governance/development-contract.md +144 -0
- package/docs/governance/gherkin-style.md +90 -0
- package/docs/governance/host-adapter.md +56 -0
- package/docs/governance/skill-authoring.md +87 -0
- package/docs/governance/skill-testing.md +356 -0
- package/docs/governance/template-authoring.md +53 -0
- package/docs/migrations/v0.2.md +51 -0
- package/docs/plans/README.md +23 -0
- package/docs/plans/bdd-then-tdd-plan.md +1767 -0
- package/docs/plans/docs-knowledge-system-plan.md +31 -0
- package/docs/plans/spec-kit-sdd-adoption-plan.md +305 -0
- package/docs/research/agents-md-best-practices.md +207 -0
- package/docs/research/archon-study.md +351 -0
- package/docs/research/claude-hooks-study.md +440 -0
- package/docs/research/codex-hooks-study.md +624 -0
- package/docs/research/everything-claude-code-study.md +252 -0
- package/docs/research/from-skills-to-layered-workflow.md +322 -0
- package/docs/research/gsd-study.md +69 -0
- package/docs/research/kimi-hooks-study.md +274 -0
- package/docs/research/mattpocock-skills-comparison.md +429 -0
- package/docs/research/mattpocock-skills-study.md +275 -0
- package/docs/research/oh-my-codex-study.md +279 -0
- package/docs/research/perplexity-agent-skills-design.md +168 -0
- package/docs/research/pmc-gstack-skill-study.md +122 -0
- package/docs/research/spec-kit-study.md +224 -0
- package/docs/research/superpowers-study.md +209 -0
- package/docs/roadmap/initial-roadmap.md +53 -0
- package/docs/solutions/README.md +45 -0
- package/docs/solutions/artifact-nesting-recovery.md +58 -0
- package/docs/solutions/session-context-restore-practice.md +67 -0
- package/docs/solutions/workflow/version-drift-recovery.md +49 -0
- package/docs/solutions/worktree-gate-recovery.md +62 -0
- package/docs/specs/README.md +28 -0
- package/docs/specs/claude.md +45 -0
- package/docs/specs/codex.md +44 -0
- package/docs/specs/cursor.md +44 -0
- package/hosts/adapters/claude.ts +29 -0
- package/hosts/adapters/codex.ts +27 -0
- package/hosts/adapters/cursor.ts +27 -0
- package/hosts/adapters/kimi.ts +27 -0
- package/hosts/claude.ts +23 -0
- package/hosts/codex.ts +26 -0
- package/hosts/cursor.ts +19 -0
- package/hosts/index.ts +33 -0
- package/hosts/registry.test.ts +52 -0
- package/hosts/registry.ts +57 -0
- package/hosts/schema.ts +58 -0
- package/package.json +52 -0
- package/scripts/browser.ts +185 -0
- package/scripts/cleanup.ts +142 -0
- package/scripts/commands/artifact.ts +115 -0
- package/scripts/commands/autopilot.ts +143 -0
- package/scripts/commands/capability.ts +57 -0
- package/scripts/commands/config.ts +69 -0
- package/scripts/commands/dag.ts +126 -0
- package/scripts/commands/feedback.ts +123 -0
- package/scripts/commands/gate.ts +291 -0
- package/scripts/commands/helpers.ts +126 -0
- package/scripts/commands/hook.ts +66 -0
- package/scripts/commands/init.ts +515 -0
- package/scripts/commands/issue.ts +825 -0
- package/scripts/commands/phase.ts +61 -0
- package/scripts/commands/preset.ts +159 -0
- package/scripts/commands/runtime.ts +199 -0
- package/scripts/commands/specify.ts +71 -0
- package/scripts/commands/upgrade.ts +243 -0
- package/scripts/commands/verify.ts +183 -0
- package/scripts/commands/wiki.ts +242 -0
- package/scripts/commands/workflow.ts +131 -0
- package/scripts/dev-skill.ts +55 -0
- package/scripts/discover-skills.ts +116 -0
- package/scripts/doctor.ts +410 -0
- package/scripts/dogfood-check.ts +125 -0
- package/scripts/eval-functional.ts +218 -0
- package/scripts/eval.ts +246 -0
- package/scripts/gen-skill-docs.ts +201 -0
- package/scripts/global-discover.ts +217 -0
- package/scripts/governance-check.ts +75 -0
- package/scripts/gxpm-check.ts +12 -0
- package/scripts/gxpm.ts +216 -0
- package/scripts/host-config.ts +62 -0
- package/scripts/install-claude-hooks.ts +138 -0
- package/scripts/install-codex-hooks.ts +271 -0
- package/scripts/install-hooks.ts +128 -0
- package/scripts/install-kimi-hooks.ts +92 -0
- package/scripts/install-skill.ts +184 -0
- package/scripts/phase-artifact-commands.ts +100 -0
- package/scripts/post-land-sync.ts +46 -0
- package/scripts/scaffold-check.ts +85 -0
- package/scripts/skill-naming-check.ts +78 -0
- package/scripts/skill-structure-check.ts +157 -0
- package/scripts/skills-lock-check.ts +60 -0
- package/scripts/sync-markdown-artifacts.ts +172 -0
- package/scripts/uninstall.ts +162 -0
- package/scripts/version.ts +47 -0
- package/scripts/wait-pr-ready.ts +407 -0
- package/skills/gxpm/SKILL.md +485 -0
- package/skills/gxpm/SKILL.md.tmpl +422 -0
- package/skills/gxpm/references/CANON.md +53 -0
- package/skills/gxpm/references/key-rules.md +130 -0
- package/skills/gxpm-architecture/SKILL.md +106 -0
- package/skills/gxpm-architecture/references/DEEPENING.md +37 -0
- package/skills/gxpm-architecture/references/INTERFACE-DESIGN.md +44 -0
- package/skills/gxpm-autopilot/SKILL.md +116 -0
- package/skills/gxpm-autopilot/SKILL.md.tmpl +107 -0
- package/skills/gxpm-browser/SKILL.md +105 -0
- package/skills/gxpm-browser/SKILL.md.tmpl +41 -0
- package/skills/gxpm-browser/references/commands.md +43 -0
- package/skills/gxpm-browser/references/evidence-path.md +20 -0
- package/skills/gxpm-build/SKILL.md +78 -0
- package/skills/gxpm-cleanup/SKILL.md +76 -0
- package/skills/gxpm-debug-issue/SKILL.md +39 -0
- package/skills/gxpm-diagnose/SKILL.md +220 -0
- package/skills/gxpm-diagnose/SKILL.md.tmpl +31 -0
- package/skills/gxpm-diagnose/references/feedback-loop.md +34 -0
- package/skills/gxpm-diagnose/references/feedback-loops.md +43 -0
- package/skills/gxpm-diagnose/references/phases.md +60 -0
- package/skills/gxpm-eval/SKILL.md +78 -0
- package/skills/gxpm-explore-codebase/SKILL.md +36 -0
- package/skills/gxpm-explore-codebase/scripts/summarize-communities.ts +51 -0
- package/skills/gxpm-feedback/SKILL.md +122 -0
- package/skills/gxpm-grill/SKILL.md +159 -0
- package/skills/gxpm-grill/SKILL.md.tmpl +77 -0
- package/skills/gxpm-grill/references/documentation-templates.md +56 -0
- package/skills/gxpm-grill/references/process.md +25 -0
- package/skills/gxpm-handoff/SKILL.md +112 -0
- package/skills/gxpm-hygiene/SKILL.md +69 -0
- package/skills/gxpm-implementer/SKILL.md +142 -0
- package/skills/gxpm-implementer/SKILL.md.tmpl +141 -0
- package/skills/gxpm-linear/SKILL.md +282 -0
- package/skills/gxpm-linear/SKILL.md.tmpl +86 -0
- package/skills/gxpm-linear/references/commands.md +75 -0
- package/skills/gxpm-linear/references/workflows.md +120 -0
- package/skills/gxpm-planning/SKILL.md +134 -0
- package/skills/gxpm-prototype/SKILL.md +64 -0
- package/skills/gxpm-refactor-safely/SKILL.md +62 -0
- package/skills/gxpm-review-army/SKILL.md +117 -0
- package/skills/gxpm-review-changes/SKILL.md +36 -0
- package/skills/gxpm-setup/SKILL.md +101 -0
- package/skills/gxpm-specifier/SKILL.md +135 -0
- package/skills/gxpm-tdd/SKILL.md +187 -0
- package/skills/gxpm-tdd/references/interface-design.md +23 -0
- package/skills/gxpm-tdd/references/mocking.md +27 -0
- package/skills/gxpm-tdd/references/red-green-refactor.md +61 -0
- package/skills/gxpm-tdd/references/troubleshooting.md +28 -0
- package/skills/gxpm-tdd/references/workflow.md +50 -0
- package/skills/gxpm-tdd/testing-anti-patterns.tmpl +304 -0
- package/skills/gxpm-triage/SKILL.md +160 -0
- package/skills/gxpm-verify/SKILL.md +107 -0
- package/skills/gxpm-write-skill/SKILL.md +131 -0
- package/skills/gxpm-zoom-out/SKILL.md +69 -0
- package/skills/maintain-hygiene-skills-lock/SKILL.md +54 -0
- package/skills/maintain-hygiene-skills-lock/SKILL.md.tmpl +53 -0
- package/templates/constitution-template.md +63 -0
- package/templates/hooks/gxpm-commit-msg +16 -0
- package/templates/hooks/gxpm-post-checkout +19 -0
- package/templates/hooks/gxpm-post-commit +7 -0
- package/templates/hooks/gxpm-post-merge +29 -0
- package/templates/hooks/gxpm-pre-commit +39 -0
- package/templates/hooks/gxpm-pre-push +33 -0
- package/templates/plan-template.md.tmpl +46 -0
- package/templates/spec-template.md.tmpl +63 -0
- package/templates/specify-stub.tmpl +22 -0
- package/templates/tasks-template.md.tmpl +32 -0
|
@@ -0,0 +1,1767 @@
|
|
|
1
|
+
# BDD-Then-TDD 强制流程 Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** 在 gxpm 状态机的 `dispatch → implement` 之间插入新的 `specify` phase,强制所有 issue 在进入 implement 之前产出并由用户确认 Gherkin 行为规约。
|
|
6
|
+
|
|
7
|
+
**Architecture:** Minimal Incremental — 新增 `behavior-spec` artifact type、`specify` phase、`specifier` agent、`gxpm-specifier` skill、Gherkin 规则治理文档;扩展 phase-gate 校验 `confirmedAt` 字段;升级 `gxpm-tdd` skill 强制引用 specify.json。
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript + Bun + Zod + 现有 gxpm artifact/state framework。
|
|
10
|
+
|
|
11
|
+
**Spec Reference:** [docs/brainstorms/2026-05-14-bdd-then-tdd-design.md](../brainstorms/2026-05-14-bdd-then-tdd-design.md)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## File Structure
|
|
16
|
+
|
|
17
|
+
**新建文件:**
|
|
18
|
+
- `core/specify.ts` — initializeSpecify / confirmSpecify / reviseSpecify 函数
|
|
19
|
+
- `core/contracts/behavior-spec.schema.ts` — Zod schema 单独导出
|
|
20
|
+
- `scripts/commands/specify.ts` — CLI confirm/show/revise 子命令路由
|
|
21
|
+
- `agents/specifier.md` — Specifier agent 模板
|
|
22
|
+
- `skills/gxpm-specifier/SKILL.md` — BDD 行为设计 skill
|
|
23
|
+
- `docs/governance/gherkin-style.md` — Gherkin 写作规则(吸收 AutomationPanda)
|
|
24
|
+
- `templates/specify-stub.tmpl` — test stub 文件生成模板
|
|
25
|
+
- `test/core/specify.test.ts` — initializeSpecify/confirmSpecify 单元测试
|
|
26
|
+
- `test/core/phase-gates.specify.test.ts` — specify gate 校验单元测试
|
|
27
|
+
- `test/functional/gxpm-specify/init-confirm.test.ts` — CLI 端到端集成测试
|
|
28
|
+
|
|
29
|
+
**修改文件:**
|
|
30
|
+
- `core/artifacts.ts` — 注册 `"behavior-spec"` 到 `ARTIFACT_TYPES`
|
|
31
|
+
- `core/state.ts` — 在 `GXPM_PHASES` 数组插入 `"specify"`;扩展 `assertArtifactGate` 校验 `confirmedAt`
|
|
32
|
+
- `core/phase-gates.ts` — 拆 dispatch→implement 规则为 dispatch→specify + specify→implement
|
|
33
|
+
- `scripts/phase-artifact-commands.ts` — 注册 `behavior-spec` handler
|
|
34
|
+
- `scripts/gxpm.ts` — 添加 `specify` 命令分支
|
|
35
|
+
- `skills/gxpm-tdd/SKILL.md` — 强制引用 specify.json 作为 RED 测试输入源
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Task 1: 注册 behavior-spec artifact type 与 Zod schema
|
|
40
|
+
|
|
41
|
+
**Files:**
|
|
42
|
+
- Create: `core/contracts/behavior-spec.schema.ts`
|
|
43
|
+
- Modify: `core/artifacts.ts:14-30`
|
|
44
|
+
- Test: `test/core/specify.test.ts`
|
|
45
|
+
|
|
46
|
+
- [ ] **Step 1: Write the failing test for schema validation**
|
|
47
|
+
|
|
48
|
+
Create `test/core/specify.test.ts`:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { describe, expect, it } from "bun:test";
|
|
52
|
+
import { BehaviorSpecSchema, type BehaviorSpec } from "../../core/contracts/behavior-spec.schema";
|
|
53
|
+
|
|
54
|
+
describe("BehaviorSpecSchema", () => {
|
|
55
|
+
it("accepts a minimal valid spec", () => {
|
|
56
|
+
const valid: BehaviorSpec = {
|
|
57
|
+
$schema: "behavior-spec.v1",
|
|
58
|
+
issueId: "GXPM-001",
|
|
59
|
+
createdAt: "2026-05-14T00:00:00.000Z",
|
|
60
|
+
createdBy: "specifier@test",
|
|
61
|
+
confirmedAt: null,
|
|
62
|
+
confirmedBy: null,
|
|
63
|
+
feature: { title: "T", asA: "user", iWant: "X", soThat: "Y" },
|
|
64
|
+
scenarios: [
|
|
65
|
+
{
|
|
66
|
+
id: "scn-01",
|
|
67
|
+
name: "happy",
|
|
68
|
+
given: ["a"],
|
|
69
|
+
when: "b",
|
|
70
|
+
then: ["c"],
|
|
71
|
+
examples: [],
|
|
72
|
+
stubPath: "test/foo.test.ts:test_x",
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
guidelinesRef: "docs/governance/gherkin-style.md@v1",
|
|
76
|
+
};
|
|
77
|
+
expect(() => BehaviorSpecSchema.parse(valid)).not.toThrow();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("rejects empty scenarios array", () => {
|
|
81
|
+
const invalid = { scenarios: [] };
|
|
82
|
+
expect(() => BehaviorSpecSchema.parse(invalid)).toThrow();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("rejects scenario missing given/when/then", () => {
|
|
86
|
+
const invalid = {
|
|
87
|
+
$schema: "behavior-spec.v1",
|
|
88
|
+
scenarios: [{ id: "x", name: "n", given: [], when: "", then: [] }],
|
|
89
|
+
};
|
|
90
|
+
expect(() => BehaviorSpecSchema.parse(invalid)).toThrow();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
96
|
+
|
|
97
|
+
Run: `bun test test/core/specify.test.ts`
|
|
98
|
+
Expected: FAIL with "Cannot find module '../../core/contracts/behavior-spec.schema'"
|
|
99
|
+
|
|
100
|
+
- [ ] **Step 3: Create the schema file**
|
|
101
|
+
|
|
102
|
+
Create `core/contracts/behavior-spec.schema.ts`:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { z } from "zod";
|
|
106
|
+
|
|
107
|
+
const ScenarioSchema = z.object({
|
|
108
|
+
id: z.string().min(1),
|
|
109
|
+
name: z.string().min(1),
|
|
110
|
+
given: z.array(z.string().min(1)).min(1),
|
|
111
|
+
when: z.string().min(1),
|
|
112
|
+
then: z.array(z.string().min(1)).min(1),
|
|
113
|
+
examples: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
114
|
+
stubPath: z.string().min(1),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const FeatureSchema = z.object({
|
|
118
|
+
title: z.string().min(1),
|
|
119
|
+
asA: z.string().min(1),
|
|
120
|
+
iWant: z.string().min(1),
|
|
121
|
+
soThat: z.string().min(1),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
export const BehaviorSpecSchema = z.object({
|
|
125
|
+
$schema: z.literal("behavior-spec.v1"),
|
|
126
|
+
issueId: z.string().min(1),
|
|
127
|
+
createdAt: z.string().datetime(),
|
|
128
|
+
createdBy: z.string().min(1),
|
|
129
|
+
confirmedAt: z.string().datetime().nullable(),
|
|
130
|
+
confirmedBy: z.string().nullable(),
|
|
131
|
+
feature: FeatureSchema,
|
|
132
|
+
scenarios: z.array(ScenarioSchema).min(1),
|
|
133
|
+
guidelinesRef: z.string().min(1),
|
|
134
|
+
}).refine(
|
|
135
|
+
(spec) =>
|
|
136
|
+
(spec.confirmedAt === null && spec.confirmedBy === null) ||
|
|
137
|
+
(spec.confirmedAt !== null && spec.confirmedBy !== null),
|
|
138
|
+
{ message: "confirmedAt and confirmedBy must both be null or both be set" },
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
export type BehaviorSpec = z.infer<typeof BehaviorSpecSchema>;
|
|
142
|
+
export type BehaviorSpecScenario = z.infer<typeof ScenarioSchema>;
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- [ ] **Step 4: Register artifact type**
|
|
146
|
+
|
|
147
|
+
Edit `core/artifacts.ts` lines 14-30 — extend `ARTIFACT_TYPES`:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
export const ARTIFACT_TYPES = [
|
|
151
|
+
"issue-intake",
|
|
152
|
+
"triage-report",
|
|
153
|
+
"autopilot-grant",
|
|
154
|
+
"acceptance-contract",
|
|
155
|
+
"implementation-plan",
|
|
156
|
+
"dispatch-handoff",
|
|
157
|
+
"behavior-spec",
|
|
158
|
+
"wiki-context",
|
|
159
|
+
"local-verify",
|
|
160
|
+
"acceptance-check",
|
|
161
|
+
"self-review",
|
|
162
|
+
"ship-readiness",
|
|
163
|
+
"pr-check",
|
|
164
|
+
"verify-findings",
|
|
165
|
+
"qa-findings",
|
|
166
|
+
"land-findings",
|
|
167
|
+
] as const;
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
- [ ] **Step 5: Run test to verify it passes**
|
|
171
|
+
|
|
172
|
+
Run: `bun test test/core/specify.test.ts`
|
|
173
|
+
Expected: PASS — 3 tests pass.
|
|
174
|
+
|
|
175
|
+
- [ ] **Step 6: Commit**
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
git add core/contracts/behavior-spec.schema.ts core/artifacts.ts test/core/specify.test.ts
|
|
179
|
+
git commit -m "GXPM-XXX add behavior-spec artifact type and zod schema
|
|
180
|
+
|
|
181
|
+
Registers behavior-spec as an artifact type and defines its
|
|
182
|
+
zod schema with structural constraints: scenarios non-empty,
|
|
183
|
+
given/when/then non-empty, confirmedAt/confirmedBy paired.
|
|
184
|
+
|
|
185
|
+
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Task 2: 在 GXPM_PHASES 中插入 specify
|
|
191
|
+
|
|
192
|
+
**Files:**
|
|
193
|
+
- Modify: `core/state.ts:17-30`
|
|
194
|
+
- Test: `test/core/state.specify-phase.test.ts`
|
|
195
|
+
|
|
196
|
+
- [ ] **Step 1: Write the failing test**
|
|
197
|
+
|
|
198
|
+
Create `test/core/state.specify-phase.test.ts`:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { describe, expect, it } from "bun:test";
|
|
202
|
+
import { GXPM_PHASES } from "../../core/state";
|
|
203
|
+
|
|
204
|
+
describe("GXPM_PHASES with specify", () => {
|
|
205
|
+
it("places specify between dispatch and implement", () => {
|
|
206
|
+
const phases = GXPM_PHASES as readonly string[];
|
|
207
|
+
const dispatchIdx = phases.indexOf("dispatch");
|
|
208
|
+
const specifyIdx = phases.indexOf("specify");
|
|
209
|
+
const implementIdx = phases.indexOf("implement");
|
|
210
|
+
expect(specifyIdx).toBeGreaterThan(-1);
|
|
211
|
+
expect(specifyIdx).toBe(dispatchIdx + 1);
|
|
212
|
+
expect(implementIdx).toBe(specifyIdx + 1);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
218
|
+
|
|
219
|
+
Run: `bun test test/core/state.specify-phase.test.ts`
|
|
220
|
+
Expected: FAIL — `expect(specifyIdx).toBeGreaterThan(-1)` fails.
|
|
221
|
+
|
|
222
|
+
- [ ] **Step 3: Insert specify in GXPM_PHASES**
|
|
223
|
+
|
|
224
|
+
Edit `core/state.ts` lines 17-30:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
export const GXPM_PHASES = [
|
|
228
|
+
"triage",
|
|
229
|
+
"plan",
|
|
230
|
+
"dispatch",
|
|
231
|
+
"specify",
|
|
232
|
+
"implement",
|
|
233
|
+
"local-verify",
|
|
234
|
+
"ac-check",
|
|
235
|
+
"self-review",
|
|
236
|
+
"ship",
|
|
237
|
+
"pr-check",
|
|
238
|
+
"verify",
|
|
239
|
+
"qa",
|
|
240
|
+
"land",
|
|
241
|
+
] as const;
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
245
|
+
|
|
246
|
+
Run: `bun test test/core/state.specify-phase.test.ts`
|
|
247
|
+
Expected: PASS.
|
|
248
|
+
|
|
249
|
+
- [ ] **Step 5: Commit**
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
git add core/state.ts test/core/state.specify-phase.test.ts
|
|
253
|
+
git commit -m "GXPM-XXX insert specify phase between dispatch and implement
|
|
254
|
+
|
|
255
|
+
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Task 3: 拆分 phase-gate 规则
|
|
261
|
+
|
|
262
|
+
**Files:**
|
|
263
|
+
- Modify: `core/phase-gates.ts:32-99`
|
|
264
|
+
- Test: `test/core/phase-gates.specify.test.ts`
|
|
265
|
+
|
|
266
|
+
- [ ] **Step 1: Write the failing test**
|
|
267
|
+
|
|
268
|
+
Create `test/core/phase-gates.specify.test.ts`:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { describe, expect, it } from "bun:test";
|
|
272
|
+
import { PHASE_GATE_RULES, getRequiredArtifactForTransition } from "../../core/phase-gates";
|
|
273
|
+
|
|
274
|
+
describe("phase-gates with specify", () => {
|
|
275
|
+
it("requires dispatch-handoff for dispatch→specify", () => {
|
|
276
|
+
expect(getRequiredArtifactForTransition("dispatch", "specify")).toBe("dispatch-handoff");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("requires behavior-spec for specify→implement", () => {
|
|
280
|
+
expect(getRequiredArtifactForTransition("specify", "implement")).toBe("behavior-spec");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("removes original dispatch→implement direct rule", () => {
|
|
284
|
+
const direct = PHASE_GATE_RULES.find(
|
|
285
|
+
(r) => r.fromPhase === "dispatch" && r.nextPhase === "implement",
|
|
286
|
+
);
|
|
287
|
+
expect(direct).toBeUndefined();
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
293
|
+
|
|
294
|
+
Run: `bun test test/core/phase-gates.specify.test.ts`
|
|
295
|
+
Expected: FAIL — dispatch→specify rule missing.
|
|
296
|
+
|
|
297
|
+
- [ ] **Step 3: Edit PHASE_GATE_RULES**
|
|
298
|
+
|
|
299
|
+
Edit `core/phase-gates.ts` — replace lines 45-50 (the `dispatch-handoff` block) with two entries:
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
{
|
|
303
|
+
command: "gxpm dispatch init <issue-id>",
|
|
304
|
+
fromPhase: "dispatch",
|
|
305
|
+
nextPhase: "specify",
|
|
306
|
+
requiredArtifact: "dispatch-handoff",
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
command: "gxpm specify init <issue-id>",
|
|
310
|
+
fromPhase: "specify",
|
|
311
|
+
nextPhase: "implement",
|
|
312
|
+
requiredArtifact: "behavior-spec",
|
|
313
|
+
},
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
317
|
+
|
|
318
|
+
Run: `bun test test/core/phase-gates.specify.test.ts`
|
|
319
|
+
Expected: PASS — 3 tests pass.
|
|
320
|
+
|
|
321
|
+
- [ ] **Step 5: Run full test suite to catch regressions**
|
|
322
|
+
|
|
323
|
+
Run: `bun test test/core/`
|
|
324
|
+
Expected: All pass. If pre-existing tests reference `dispatch→implement` directly, update them to `dispatch→specify→implement`.
|
|
325
|
+
|
|
326
|
+
- [ ] **Step 6: Commit**
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
git add core/phase-gates.ts test/core/phase-gates.specify.test.ts
|
|
330
|
+
git commit -m "GXPM-XXX split dispatch->implement phase gate via specify
|
|
331
|
+
|
|
332
|
+
Replaces direct dispatch->implement rule with two rules:
|
|
333
|
+
dispatch->specify (requires dispatch-handoff) and
|
|
334
|
+
specify->implement (requires behavior-spec). Test coverage
|
|
335
|
+
for both transitions added.
|
|
336
|
+
|
|
337
|
+
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## Task 4: 扩展 assertArtifactGate 校验 confirmedAt
|
|
343
|
+
|
|
344
|
+
**Files:**
|
|
345
|
+
- Modify: `core/state.ts:690-745` (around `assertArtifactGate`)
|
|
346
|
+
- Test: `test/core/phase-gates.specify.test.ts` (append)
|
|
347
|
+
|
|
348
|
+
- [ ] **Step 1: Write the failing test (append to existing file)**
|
|
349
|
+
|
|
350
|
+
Append to `test/core/phase-gates.specify.test.ts`:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
354
|
+
import { tmpdir } from "node:os";
|
|
355
|
+
import { join } from "node:path";
|
|
356
|
+
import { createIssueState, transitionIssuePhase, type IssueState } from "../../core/state";
|
|
357
|
+
|
|
358
|
+
function seedIssueAt(phase: IssueState["currentPhase"]) {
|
|
359
|
+
const root = mkdtempSync(join(tmpdir(), "gxpm-specify-"));
|
|
360
|
+
createIssueState({ root, issueId: "G-1", issueType: "feature" });
|
|
361
|
+
// walk through phases up to `phase`
|
|
362
|
+
const path = ["plan", "dispatch", "specify"] as const;
|
|
363
|
+
for (const p of path) {
|
|
364
|
+
if (p === phase) break;
|
|
365
|
+
// seed required artifacts for transition
|
|
366
|
+
const artDir = join(root, ".gxpm", "issues", "G-1", "artifacts");
|
|
367
|
+
mkdirSync(artDir, { recursive: true });
|
|
368
|
+
writeFileSync(
|
|
369
|
+
join(artDir, "acceptance-contract.json"),
|
|
370
|
+
JSON.stringify({ schemaVersion: 1, issueId: "G-1", type: "acceptance-contract", writtenAt: "now", payload: {} }),
|
|
371
|
+
);
|
|
372
|
+
// ... similar seeding for downstream artifacts
|
|
373
|
+
}
|
|
374
|
+
return root;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
describe("specify→implement gate confirmedAt check", () => {
|
|
378
|
+
it("blocks transition when behavior-spec.confirmedAt is null", () => {
|
|
379
|
+
// Setup: seed an issue in specify phase with unconfirmed behavior-spec
|
|
380
|
+
const root = mkdtempSync(join(tmpdir(), "gxpm-specify-gate-"));
|
|
381
|
+
createIssueState({ root, issueId: "G-2", issueType: "feature" });
|
|
382
|
+
// Force phase to specify via direct state write (test helper).
|
|
383
|
+
const stateFile = join(root, ".gxpm", "issues", "G-2", "state.json");
|
|
384
|
+
const raw = JSON.parse(require("node:fs").readFileSync(stateFile, "utf8"));
|
|
385
|
+
raw.currentPhase = "specify";
|
|
386
|
+
raw.phaseHistory.push({ phase: "specify", enteredAt: "2026-05-14T00:00:00Z", fromPhase: "dispatch" });
|
|
387
|
+
writeFileSync(stateFile, JSON.stringify(raw, null, 2));
|
|
388
|
+
// Seed unconfirmed behavior-spec
|
|
389
|
+
const artPath = join(root, ".gxpm", "issues", "G-2", "artifacts", "behavior-spec.json");
|
|
390
|
+
mkdirSync(join(root, ".gxpm", "issues", "G-2", "artifacts"), { recursive: true });
|
|
391
|
+
writeFileSync(
|
|
392
|
+
artPath,
|
|
393
|
+
JSON.stringify({
|
|
394
|
+
schemaVersion: 1,
|
|
395
|
+
issueId: "G-2",
|
|
396
|
+
type: "behavior-spec",
|
|
397
|
+
writtenAt: "2026-05-14T00:00:00Z",
|
|
398
|
+
payload: { $schema: "behavior-spec.v1", confirmedAt: null, scenarios: [] },
|
|
399
|
+
}),
|
|
400
|
+
);
|
|
401
|
+
expect(() =>
|
|
402
|
+
transitionIssuePhase({ root, issueId: "G-2", nextPhase: "implement" }),
|
|
403
|
+
).toThrow(/confirmedAt/);
|
|
404
|
+
rmSync(root, { recursive: true, force: true });
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
410
|
+
|
|
411
|
+
Run: `bun test test/core/phase-gates.specify.test.ts`
|
|
412
|
+
Expected: FAIL — gate currently only checks file existence, not confirmedAt.
|
|
413
|
+
|
|
414
|
+
- [ ] **Step 3: Extend assertArtifactGate**
|
|
415
|
+
|
|
416
|
+
Edit `core/state.ts` around line 705 — after the `existsSync(requiredArtifactPath)` block, add behavior-spec confirmation check:
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
if (existsSync(requiredArtifactPath)) {
|
|
420
|
+
// Extra check: behavior-spec must be confirmed before transitioning into implement.
|
|
421
|
+
if (requiredArtifact === "behavior-spec" && input.nextPhase === "implement") {
|
|
422
|
+
const raw = JSON.parse(readFileSync(requiredArtifactPath, "utf8"));
|
|
423
|
+
const confirmedAt = raw?.payload?.confirmedAt;
|
|
424
|
+
if (!confirmedAt) {
|
|
425
|
+
const now = new Date().toISOString();
|
|
426
|
+
appendIssueEvent({
|
|
427
|
+
issueDir: input.issueDir,
|
|
428
|
+
event: {
|
|
429
|
+
schemaVersion: 1,
|
|
430
|
+
type: "gate.blocked",
|
|
431
|
+
issueId: input.issueId,
|
|
432
|
+
timestamp: now,
|
|
433
|
+
sessionId: resolveSessionId(),
|
|
434
|
+
payload: {
|
|
435
|
+
fromPhase: input.fromPhase,
|
|
436
|
+
toPhase: input.nextPhase,
|
|
437
|
+
missingArtifact: "behavior-spec.confirmedAt",
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
throw new Error(
|
|
442
|
+
`behavior-spec exists but confirmedAt is null; run \`gxpm specify confirm ${input.issueId}\` to confirm`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const now = new Date().toISOString();
|
|
447
|
+
appendIssueEvent({
|
|
448
|
+
issueDir: input.issueDir,
|
|
449
|
+
event: {
|
|
450
|
+
schemaVersion: 1,
|
|
451
|
+
type: "gate.passed",
|
|
452
|
+
issueId: input.issueId,
|
|
453
|
+
timestamp: now,
|
|
454
|
+
sessionId: resolveSessionId(),
|
|
455
|
+
payload: {
|
|
456
|
+
fromPhase: input.fromPhase,
|
|
457
|
+
toPhase: input.nextPhase,
|
|
458
|
+
requiredArtifact,
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
467
|
+
|
|
468
|
+
Run: `bun test test/core/phase-gates.specify.test.ts`
|
|
469
|
+
Expected: PASS.
|
|
470
|
+
|
|
471
|
+
- [ ] **Step 5: Commit**
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
git add core/state.ts test/core/phase-gates.specify.test.ts
|
|
475
|
+
git commit -m "GXPM-XXX gate specify->implement on behavior-spec.confirmedAt
|
|
476
|
+
|
|
477
|
+
Phase-gate now reads behavior-spec.json payload and rejects
|
|
478
|
+
transition into implement if confirmedAt is null. Emits a
|
|
479
|
+
gate.blocked event with missingArtifact: behavior-spec.confirmedAt.
|
|
480
|
+
|
|
481
|
+
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## Task 4.5: 向后兼容 — 老 issue 跳过 specify gate
|
|
487
|
+
|
|
488
|
+
**Files:**
|
|
489
|
+
- Modify: `core/state.ts` `assertArtifactGate` (the block from Task 4)
|
|
490
|
+
- Test: `test/core/phase-gates.specify.test.ts` (append)
|
|
491
|
+
|
|
492
|
+
Background: spec § 2.4 / § 5.1 要求"若 issue 在 specify 引入日期前已进入 implement,跳过 specify-gate 校验"。
|
|
493
|
+
|
|
494
|
+
- [ ] **Step 1: Define the cutoff constant**
|
|
495
|
+
|
|
496
|
+
Edit `core/state.ts` — near the top (after existing constants), add:
|
|
497
|
+
|
|
498
|
+
```ts
|
|
499
|
+
// Issues whose phaseHistory was created before this cutoff are exempt from the
|
|
500
|
+
// specify-gate (introduced 2026-05-14). Set to the merge date of the specify
|
|
501
|
+
// phase feature; do NOT change retroactively.
|
|
502
|
+
export const SPECIFY_PHASE_CUTOFF = "2026-05-14T00:00:00Z";
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
- [ ] **Step 2: Write the failing test**
|
|
506
|
+
|
|
507
|
+
Append to `test/core/phase-gates.specify.test.ts`:
|
|
508
|
+
|
|
509
|
+
```ts
|
|
510
|
+
import { SPECIFY_PHASE_CUTOFF } from "../../core/state";
|
|
511
|
+
|
|
512
|
+
describe("specify-gate backward compatibility", () => {
|
|
513
|
+
it("skips specify gate for issues that entered implement before cutoff", () => {
|
|
514
|
+
const root = mkdtempSync(join(tmpdir(), "gxpm-legacy-"));
|
|
515
|
+
createIssueState({ root, issueId: "G-LEGACY", issueType: "feature" });
|
|
516
|
+
const stateFile = join(root, ".gxpm", "issues", "G-LEGACY", "state.json");
|
|
517
|
+
const raw = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
518
|
+
raw.currentPhase = "dispatch";
|
|
519
|
+
// phaseHistory shows implement was entered before cutoff
|
|
520
|
+
raw.phaseHistory = [
|
|
521
|
+
{ phase: "triage", enteredAt: "2025-12-01T00:00:00Z", fromPhase: null },
|
|
522
|
+
{ phase: "plan", enteredAt: "2025-12-02T00:00:00Z", fromPhase: "triage" },
|
|
523
|
+
{ phase: "dispatch", enteredAt: "2025-12-03T00:00:00Z", fromPhase: "plan" },
|
|
524
|
+
{ phase: "implement", enteredAt: "2025-12-04T00:00:00Z", fromPhase: "dispatch" },
|
|
525
|
+
];
|
|
526
|
+
raw.currentPhase = "implement";
|
|
527
|
+
writeFileSync(stateFile, JSON.stringify(raw, null, 2));
|
|
528
|
+
// No behavior-spec.json on disk — but legacy issue should pass
|
|
529
|
+
// We test by attempting to re-enter implement from dispatch (artificial),
|
|
530
|
+
// expecting NO behavior-spec error.
|
|
531
|
+
// For this test we directly assert the cutoff check helper:
|
|
532
|
+
expect(SPECIFY_PHASE_CUTOFF).toBe("2026-05-14T00:00:00Z");
|
|
533
|
+
rmSync(root, { recursive: true, force: true });
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
(Note: a more direct test would require exposing the helper; for MVP the constant assertion plus the manual exemption logic in step 3 is sufficient. A deeper integration test is in Task 14.)
|
|
539
|
+
|
|
540
|
+
- [ ] **Step 3: Add the exemption check in assertArtifactGate**
|
|
541
|
+
|
|
542
|
+
Edit `core/state.ts` — in the `assertArtifactGate` function (modified in Task 4), wrap the `behavior-spec.confirmedAt` check so it ALSO checks the legacy cutoff:
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
if (existsSync(requiredArtifactPath)) {
|
|
546
|
+
if (requiredArtifact === "behavior-spec" && input.nextPhase === "implement") {
|
|
547
|
+
// Legacy exemption: if this issue entered implement before cutoff, skip.
|
|
548
|
+
const state = readIssueState({ root: undefined, issueId: input.issueId });
|
|
549
|
+
const legacyEntry = state.phaseHistory?.find((h) => h.phase === "implement");
|
|
550
|
+
if (legacyEntry && legacyEntry.enteredAt < SPECIFY_PHASE_CUTOFF) {
|
|
551
|
+
// skip confirmedAt check
|
|
552
|
+
} else {
|
|
553
|
+
const raw = JSON.parse(readFileSync(requiredArtifactPath, "utf8"));
|
|
554
|
+
const confirmedAt = raw?.payload?.confirmedAt;
|
|
555
|
+
if (!confirmedAt) {
|
|
556
|
+
// ... (same as Task 4)
|
|
557
|
+
throw new Error(
|
|
558
|
+
`behavior-spec exists but confirmedAt is null; run \`gxpm specify confirm ${input.issueId}\` to confirm`,
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
// ... emit gate.passed event (same as Task 4)
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Also skip the missing-artifact branch for legacy issues:
|
|
568
|
+
if (requiredArtifact === "behavior-spec") {
|
|
569
|
+
const state = readIssueState({ root: undefined, issueId: input.issueId });
|
|
570
|
+
const legacyEntry = state.phaseHistory?.find((h) => h.phase === "implement");
|
|
571
|
+
if (legacyEntry && legacyEntry.enteredAt < SPECIFY_PHASE_CUTOFF) {
|
|
572
|
+
return; // legacy bypass
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
Place the second block BEFORE the `throw new Error("Missing required artifact: ...")` line so legacy issues exit the function cleanly.
|
|
578
|
+
|
|
579
|
+
- [ ] **Step 4: Run tests to verify**
|
|
580
|
+
|
|
581
|
+
Run: `bun test test/core/phase-gates.specify.test.ts`
|
|
582
|
+
Expected: PASS — legacy compatibility test + all earlier tests.
|
|
583
|
+
|
|
584
|
+
- [ ] **Step 5: Commit**
|
|
585
|
+
|
|
586
|
+
```bash
|
|
587
|
+
git add core/state.ts test/core/phase-gates.specify.test.ts
|
|
588
|
+
git commit -m "GXPM-XXX exempt pre-cutoff issues from specify gate
|
|
589
|
+
|
|
590
|
+
Issues whose phaseHistory shows implement entered before
|
|
591
|
+
SPECIFY_PHASE_CUTOFF (2026-05-14T00:00:00Z) bypass the new
|
|
592
|
+
specify gate, so legacy in-flight work is not blocked.
|
|
593
|
+
|
|
594
|
+
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
## Task 5: 实现 core/specify.ts — initializeSpecify
|
|
600
|
+
|
|
601
|
+
**Files:**
|
|
602
|
+
- Create: `core/specify.ts`
|
|
603
|
+
- Test: `test/core/specify.test.ts` (append)
|
|
604
|
+
|
|
605
|
+
- [ ] **Step 1: Write the failing test**
|
|
606
|
+
|
|
607
|
+
Append to `test/core/specify.test.ts`:
|
|
608
|
+
|
|
609
|
+
```ts
|
|
610
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
611
|
+
import { tmpdir } from "node:os";
|
|
612
|
+
import { join } from "node:path";
|
|
613
|
+
import { createIssueState } from "../../core/state";
|
|
614
|
+
import { initializeSpecify } from "../../core/specify";
|
|
615
|
+
|
|
616
|
+
describe("initializeSpecify", () => {
|
|
617
|
+
it("writes a draft behavior-spec.json with confirmedAt=null", () => {
|
|
618
|
+
const root = mkdtempSync(join(tmpdir(), "gxpm-init-specify-"));
|
|
619
|
+
createIssueState({ root, issueId: "G-3", issueType: "feature" });
|
|
620
|
+
// Force phase to specify
|
|
621
|
+
const stateFile = join(root, ".gxpm", "issues", "G-3", "state.json");
|
|
622
|
+
const raw = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
623
|
+
raw.currentPhase = "specify";
|
|
624
|
+
writeFileSync(stateFile, JSON.stringify(raw, null, 2));
|
|
625
|
+
|
|
626
|
+
const record = initializeSpecify({ root, issueId: "G-3" });
|
|
627
|
+
expect(record.type).toBe("behavior-spec");
|
|
628
|
+
|
|
629
|
+
const artPath = join(root, ".gxpm", "issues", "G-3", "artifacts", "behavior-spec.json");
|
|
630
|
+
const written = JSON.parse(readFileSync(artPath, "utf8"));
|
|
631
|
+
expect(written.payload.$schema).toBe("behavior-spec.v1");
|
|
632
|
+
expect(written.payload.confirmedAt).toBeNull();
|
|
633
|
+
expect(written.payload.confirmedBy).toBeNull();
|
|
634
|
+
expect(written.payload.scenarios).toHaveLength(1);
|
|
635
|
+
expect(written.payload.scenarios[0].id).toBe("scn-01");
|
|
636
|
+
rmSync(root, { recursive: true, force: true });
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it("throws when not in specify phase", () => {
|
|
640
|
+
const root = mkdtempSync(join(tmpdir(), "gxpm-init-specify-wrong-"));
|
|
641
|
+
createIssueState({ root, issueId: "G-4", issueType: "feature" });
|
|
642
|
+
expect(() => initializeSpecify({ root, issueId: "G-4" })).toThrow(/specify phase/);
|
|
643
|
+
rmSync(root, { recursive: true, force: true });
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
649
|
+
|
|
650
|
+
Run: `bun test test/core/specify.test.ts`
|
|
651
|
+
Expected: FAIL — `Cannot find module '../../core/specify'`.
|
|
652
|
+
|
|
653
|
+
- [ ] **Step 3: Create core/specify.ts (initialize only)**
|
|
654
|
+
|
|
655
|
+
Create `core/specify.ts`:
|
|
656
|
+
|
|
657
|
+
```ts
|
|
658
|
+
import { writeArtifact } from "./artifacts";
|
|
659
|
+
import { readIssueState } from "./state";
|
|
660
|
+
import { resolveAgentIdentity } from "./session";
|
|
661
|
+
|
|
662
|
+
interface SpecifyInput {
|
|
663
|
+
root?: string;
|
|
664
|
+
issueId: string;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export function initializeSpecify(input: SpecifyInput) {
|
|
668
|
+
const state = readIssueState({ root: input.root, issueId: input.issueId });
|
|
669
|
+
if (state.currentPhase !== "specify") {
|
|
670
|
+
throw new Error(
|
|
671
|
+
`Specify can only be initialized from specify phase: current phase is ${state.currentPhase}`,
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const now = new Date().toISOString();
|
|
676
|
+
const identity = resolveAgentIdentity();
|
|
677
|
+
|
|
678
|
+
const payload = {
|
|
679
|
+
$schema: "behavior-spec.v1",
|
|
680
|
+
issueId: input.issueId,
|
|
681
|
+
createdAt: now,
|
|
682
|
+
createdBy: identity,
|
|
683
|
+
confirmedAt: null,
|
|
684
|
+
confirmedBy: null,
|
|
685
|
+
feature: {
|
|
686
|
+
title: "",
|
|
687
|
+
asA: "",
|
|
688
|
+
iWant: "",
|
|
689
|
+
soThat: "",
|
|
690
|
+
},
|
|
691
|
+
scenarios: [
|
|
692
|
+
{
|
|
693
|
+
id: "scn-01",
|
|
694
|
+
name: "",
|
|
695
|
+
given: [""],
|
|
696
|
+
when: "",
|
|
697
|
+
then: [""],
|
|
698
|
+
examples: [],
|
|
699
|
+
stubPath: "",
|
|
700
|
+
},
|
|
701
|
+
],
|
|
702
|
+
guidelinesRef: "docs/governance/gherkin-style.md@v1",
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
return writeArtifact({
|
|
706
|
+
root: input.root,
|
|
707
|
+
issueId: input.issueId,
|
|
708
|
+
type: "behavior-spec",
|
|
709
|
+
payload,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
715
|
+
|
|
716
|
+
Run: `bun test test/core/specify.test.ts`
|
|
717
|
+
Expected: PASS (3 schema tests + 2 init tests = 5 tests pass).
|
|
718
|
+
|
|
719
|
+
- [ ] **Step 5: Commit**
|
|
720
|
+
|
|
721
|
+
```bash
|
|
722
|
+
git add core/specify.ts test/core/specify.test.ts
|
|
723
|
+
git commit -m "GXPM-XXX implement initializeSpecify to draft behavior-spec
|
|
724
|
+
|
|
725
|
+
Mirrors core/plan.ts shape: writes a draft behavior-spec.json
|
|
726
|
+
with one empty scenario placeholder; rejects if issue not in
|
|
727
|
+
specify phase.
|
|
728
|
+
|
|
729
|
+
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
---
|
|
733
|
+
|
|
734
|
+
## Task 6: 实现 confirmSpecify / reviseSpecify
|
|
735
|
+
|
|
736
|
+
**Files:**
|
|
737
|
+
- Modify: `core/specify.ts` (append functions)
|
|
738
|
+
- Test: `test/core/specify.test.ts` (append)
|
|
739
|
+
|
|
740
|
+
- [ ] **Step 1: Write failing tests for confirm and revise**
|
|
741
|
+
|
|
742
|
+
Append to `test/core/specify.test.ts`:
|
|
743
|
+
|
|
744
|
+
```ts
|
|
745
|
+
import { confirmSpecify, reviseSpecify } from "../../core/specify";
|
|
746
|
+
import { BehaviorSpecSchema } from "../../core/contracts/behavior-spec.schema";
|
|
747
|
+
|
|
748
|
+
function seedValidSpec(root: string, issueId: string, stubFile: string) {
|
|
749
|
+
const artDir = join(root, ".gxpm", "issues", issueId, "artifacts");
|
|
750
|
+
mkdirSync(artDir, { recursive: true });
|
|
751
|
+
writeFileSync(
|
|
752
|
+
join(artDir, "behavior-spec.json"),
|
|
753
|
+
JSON.stringify({
|
|
754
|
+
schemaVersion: 1,
|
|
755
|
+
issueId,
|
|
756
|
+
type: "behavior-spec",
|
|
757
|
+
writtenAt: "2026-05-14T00:00:00Z",
|
|
758
|
+
payload: {
|
|
759
|
+
$schema: "behavior-spec.v1",
|
|
760
|
+
issueId,
|
|
761
|
+
createdAt: "2026-05-14T00:00:00Z",
|
|
762
|
+
createdBy: "specifier",
|
|
763
|
+
confirmedAt: null,
|
|
764
|
+
confirmedBy: null,
|
|
765
|
+
feature: { title: "T", asA: "a", iWant: "b", soThat: "c" },
|
|
766
|
+
scenarios: [
|
|
767
|
+
{
|
|
768
|
+
id: "scn-01",
|
|
769
|
+
name: "happy",
|
|
770
|
+
given: ["g"],
|
|
771
|
+
when: "w",
|
|
772
|
+
then: ["t"],
|
|
773
|
+
examples: [],
|
|
774
|
+
stubPath: stubFile,
|
|
775
|
+
},
|
|
776
|
+
],
|
|
777
|
+
guidelinesRef: "docs/governance/gherkin-style.md@v1",
|
|
778
|
+
},
|
|
779
|
+
}),
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
describe("confirmSpecify", () => {
|
|
784
|
+
it("writes confirmedAt and confirmedBy when valid", () => {
|
|
785
|
+
const root = mkdtempSync(join(tmpdir(), "gxpm-confirm-"));
|
|
786
|
+
createIssueState({ root, issueId: "G-5", issueType: "feature" });
|
|
787
|
+
// Force phase to specify
|
|
788
|
+
const stateFile = join(root, ".gxpm", "issues", "G-5", "state.json");
|
|
789
|
+
const raw = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
790
|
+
raw.currentPhase = "specify";
|
|
791
|
+
writeFileSync(stateFile, JSON.stringify(raw, null, 2));
|
|
792
|
+
// Create stub file referenced by scenario
|
|
793
|
+
const stubPath = join(root, "test/foo.test.ts");
|
|
794
|
+
mkdirSync(join(root, "test"), { recursive: true });
|
|
795
|
+
writeFileSync(stubPath, "// stub");
|
|
796
|
+
seedValidSpec(root, "G-5", "test/foo.test.ts");
|
|
797
|
+
|
|
798
|
+
confirmSpecify({ root, issueId: "G-5", confirmedBy: "alice@example.com" });
|
|
799
|
+
|
|
800
|
+
const artPath = join(root, ".gxpm", "issues", "G-5", "artifacts", "behavior-spec.json");
|
|
801
|
+
const after = JSON.parse(readFileSync(artPath, "utf8"));
|
|
802
|
+
expect(after.payload.confirmedAt).not.toBeNull();
|
|
803
|
+
expect(after.payload.confirmedBy).toBe("alice@example.com");
|
|
804
|
+
rmSync(root, { recursive: true, force: true });
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
it("rejects confirm when stubPath does not exist", () => {
|
|
808
|
+
const root = mkdtempSync(join(tmpdir(), "gxpm-confirm-missing-"));
|
|
809
|
+
createIssueState({ root, issueId: "G-6", issueType: "feature" });
|
|
810
|
+
seedValidSpec(root, "G-6", "test/does-not-exist.test.ts");
|
|
811
|
+
expect(() =>
|
|
812
|
+
confirmSpecify({ root, issueId: "G-6", confirmedBy: "alice@example.com" }),
|
|
813
|
+
).toThrow(/Stub file missing/);
|
|
814
|
+
rmSync(root, { recursive: true, force: true });
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
describe("reviseSpecify", () => {
|
|
819
|
+
it("clears confirmedAt and confirmedBy", () => {
|
|
820
|
+
const root = mkdtempSync(join(tmpdir(), "gxpm-revise-"));
|
|
821
|
+
createIssueState({ root, issueId: "G-7", issueType: "feature" });
|
|
822
|
+
const stubPath = join(root, "test/foo.test.ts");
|
|
823
|
+
mkdirSync(join(root, "test"), { recursive: true });
|
|
824
|
+
writeFileSync(stubPath, "// stub");
|
|
825
|
+
seedValidSpec(root, "G-7", "test/foo.test.ts");
|
|
826
|
+
// Manually set confirmedAt
|
|
827
|
+
const artPath = join(root, ".gxpm", "issues", "G-7", "artifacts", "behavior-spec.json");
|
|
828
|
+
const before = JSON.parse(readFileSync(artPath, "utf8"));
|
|
829
|
+
before.payload.confirmedAt = "2026-05-14T01:00:00Z";
|
|
830
|
+
before.payload.confirmedBy = "alice@example.com";
|
|
831
|
+
writeFileSync(artPath, JSON.stringify(before));
|
|
832
|
+
|
|
833
|
+
reviseSpecify({ root, issueId: "G-7" });
|
|
834
|
+
|
|
835
|
+
const after = JSON.parse(readFileSync(artPath, "utf8"));
|
|
836
|
+
expect(after.payload.confirmedAt).toBeNull();
|
|
837
|
+
expect(after.payload.confirmedBy).toBeNull();
|
|
838
|
+
rmSync(root, { recursive: true, force: true });
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
- [ ] **Step 2: Run test to verify it fails**
|
|
844
|
+
|
|
845
|
+
Run: `bun test test/core/specify.test.ts`
|
|
846
|
+
Expected: FAIL — `confirmSpecify` and `reviseSpecify` not exported.
|
|
847
|
+
|
|
848
|
+
- [ ] **Step 3: Implement confirmSpecify and reviseSpecify**
|
|
849
|
+
|
|
850
|
+
Append to `core/specify.ts`:
|
|
851
|
+
|
|
852
|
+
```ts
|
|
853
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
854
|
+
import { join } from "node:path";
|
|
855
|
+
import { execSync } from "node:child_process";
|
|
856
|
+
import { BehaviorSpecSchema, type BehaviorSpec } from "./contracts/behavior-spec.schema";
|
|
857
|
+
|
|
858
|
+
interface ConfirmInput {
|
|
859
|
+
root?: string;
|
|
860
|
+
issueId: string;
|
|
861
|
+
confirmedBy?: string;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function gitUserEmail(): string {
|
|
865
|
+
try {
|
|
866
|
+
return execSync("git config user.email", { encoding: "utf8" }).trim();
|
|
867
|
+
} catch {
|
|
868
|
+
return "unknown@local";
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function specPath(root: string, issueId: string): string {
|
|
873
|
+
return join(root, ".gxpm", "issues", issueId, "artifacts", "behavior-spec.json");
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function readSpec(root: string, issueId: string): { stored: any; spec: BehaviorSpec } {
|
|
877
|
+
const path = specPath(root, issueId);
|
|
878
|
+
if (!existsSync(path)) {
|
|
879
|
+
throw new Error(`behavior-spec.json not found for ${issueId}; run \`gxpm specify init ${issueId}\` first`);
|
|
880
|
+
}
|
|
881
|
+
const stored = JSON.parse(readFileSync(path, "utf8"));
|
|
882
|
+
const spec = BehaviorSpecSchema.parse(stored.payload);
|
|
883
|
+
return { stored, spec };
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
export function confirmSpecify(input: ConfirmInput) {
|
|
887
|
+
const root = input.root ?? process.cwd();
|
|
888
|
+
const { stored, spec } = readSpec(root, input.issueId);
|
|
889
|
+
|
|
890
|
+
for (const scn of spec.scenarios) {
|
|
891
|
+
const [file] = scn.stubPath.split(":");
|
|
892
|
+
const abs = join(root, file);
|
|
893
|
+
if (!existsSync(abs)) {
|
|
894
|
+
throw new Error(`Stub file missing: ${scn.stubPath} (scenario ${scn.id})`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const now = new Date().toISOString();
|
|
899
|
+
const confirmedBy = input.confirmedBy ?? gitUserEmail();
|
|
900
|
+
stored.payload.confirmedAt = now;
|
|
901
|
+
stored.payload.confirmedBy = confirmedBy;
|
|
902
|
+
writeFileSync(specPath(root, input.issueId), `${JSON.stringify(stored, null, 2)}\n`);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
export function reviseSpecify(input: { root?: string; issueId: string }) {
|
|
906
|
+
const root = input.root ?? process.cwd();
|
|
907
|
+
const { stored } = readSpec(root, input.issueId);
|
|
908
|
+
stored.payload.confirmedAt = null;
|
|
909
|
+
stored.payload.confirmedBy = null;
|
|
910
|
+
writeFileSync(specPath(root, input.issueId), `${JSON.stringify(stored, null, 2)}\n`);
|
|
911
|
+
}
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
- [ ] **Step 4: Run test to verify it passes**
|
|
915
|
+
|
|
916
|
+
Run: `bun test test/core/specify.test.ts`
|
|
917
|
+
Expected: PASS — all 8 tests pass.
|
|
918
|
+
|
|
919
|
+
- [ ] **Step 5: Commit**
|
|
920
|
+
|
|
921
|
+
```bash
|
|
922
|
+
git add core/specify.ts test/core/specify.test.ts
|
|
923
|
+
git commit -m "GXPM-XXX add confirmSpecify and reviseSpecify
|
|
924
|
+
|
|
925
|
+
confirmSpecify validates schema, checks every scenario.stubPath
|
|
926
|
+
file exists, then writes confirmedAt/confirmedBy. reviseSpecify
|
|
927
|
+
clears both fields. Both use BehaviorSpecSchema for shape safety.
|
|
928
|
+
|
|
929
|
+
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
---
|
|
933
|
+
|
|
934
|
+
## Task 7: 注册 specify init 到 PHASE_ARTIFACT_HANDLERS
|
|
935
|
+
|
|
936
|
+
**Files:**
|
|
937
|
+
- Modify: `scripts/phase-artifact-commands.ts:21-72`
|
|
938
|
+
|
|
939
|
+
- [ ] **Step 1: Edit PHASE_ARTIFACT_HANDLERS**
|
|
940
|
+
|
|
941
|
+
Edit `scripts/phase-artifact-commands.ts` — at the top, add import:
|
|
942
|
+
|
|
943
|
+
```ts
|
|
944
|
+
import { initializeSpecify } from "../core/specify";
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
In `PHASE_ARTIFACT_HANDLERS`, add entry between `"dispatch-handoff"` and `"local-verify"`:
|
|
948
|
+
|
|
949
|
+
```ts
|
|
950
|
+
"behavior-spec": {
|
|
951
|
+
initialize: initializeSpecify,
|
|
952
|
+
successMessage: (issueId) => `initialized behavior-spec artifact for ${issueId}`,
|
|
953
|
+
},
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
- [ ] **Step 2: Verify command discovery**
|
|
957
|
+
|
|
958
|
+
Run:
|
|
959
|
+
```bash
|
|
960
|
+
bun run scripts/gxpm.ts specify init G-001 2>&1 | head -5
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
Expected: Either "initialized behavior-spec artifact" success message (if there is an issue G-001 in specify phase) or a meaningful error like "Issue state not found: G-001". NOT "Unknown command".
|
|
964
|
+
|
|
965
|
+
- [ ] **Step 3: Commit**
|
|
966
|
+
|
|
967
|
+
```bash
|
|
968
|
+
git add scripts/phase-artifact-commands.ts
|
|
969
|
+
git commit -m "GXPM-XXX register behavior-spec init handler
|
|
970
|
+
|
|
971
|
+
Hooks initializeSpecify into the phase-artifact framework so
|
|
972
|
+
that 'gxpm specify init <issue-id>' is auto-discovered via
|
|
973
|
+
PHASE_GATE_RULES like all other phase init commands.
|
|
974
|
+
|
|
975
|
+
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
---
|
|
979
|
+
|
|
980
|
+
## Task 8: 实现 scripts/commands/specify.ts CLI 子命令
|
|
981
|
+
|
|
982
|
+
**Files:**
|
|
983
|
+
- Create: `scripts/commands/specify.ts`
|
|
984
|
+
- Modify: `scripts/gxpm.ts` (add routing branch)
|
|
985
|
+
|
|
986
|
+
- [ ] **Step 1: Create scripts/commands/specify.ts**
|
|
987
|
+
|
|
988
|
+
```ts
|
|
989
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
990
|
+
import { join } from "node:path";
|
|
991
|
+
import { confirmSpecify, reviseSpecify } from "../../core/specify";
|
|
992
|
+
|
|
993
|
+
function specPath(issueId: string): string {
|
|
994
|
+
return join(process.cwd(), ".gxpm", "issues", issueId, "artifacts", "behavior-spec.json");
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function runConfirm(issueId: string) {
|
|
998
|
+
if (!issueId) {
|
|
999
|
+
throw new Error("usage: gxpm specify confirm <issue-id>");
|
|
1000
|
+
}
|
|
1001
|
+
confirmSpecify({ issueId });
|
|
1002
|
+
console.log(`confirmed behavior-spec for ${issueId}`);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function runRevise(issueId: string) {
|
|
1006
|
+
if (!issueId) {
|
|
1007
|
+
throw new Error("usage: gxpm specify revise <issue-id>");
|
|
1008
|
+
}
|
|
1009
|
+
reviseSpecify({ issueId });
|
|
1010
|
+
console.log(`revised behavior-spec for ${issueId} (confirmedAt cleared)`);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function runShow(issueId: string) {
|
|
1014
|
+
if (!issueId) {
|
|
1015
|
+
throw new Error("usage: gxpm specify show <issue-id>");
|
|
1016
|
+
}
|
|
1017
|
+
const path = specPath(issueId);
|
|
1018
|
+
if (!existsSync(path)) {
|
|
1019
|
+
throw new Error(`behavior-spec.json not found for ${issueId}`);
|
|
1020
|
+
}
|
|
1021
|
+
const stored = JSON.parse(readFileSync(path, "utf8"));
|
|
1022
|
+
const spec = stored.payload;
|
|
1023
|
+
console.log(`Feature: ${spec.feature.title}`);
|
|
1024
|
+
console.log(` As a ${spec.feature.asA}`);
|
|
1025
|
+
console.log(` I want ${spec.feature.iWant}`);
|
|
1026
|
+
console.log(` So that ${spec.feature.soThat}`);
|
|
1027
|
+
console.log("");
|
|
1028
|
+
for (const scn of spec.scenarios) {
|
|
1029
|
+
console.log(`Scenario (${scn.id}): ${scn.name}`);
|
|
1030
|
+
for (const g of scn.given) console.log(` Given ${g}`);
|
|
1031
|
+
console.log(` When ${scn.when}`);
|
|
1032
|
+
for (const t of scn.then) console.log(` Then ${t}`);
|
|
1033
|
+
console.log(` Stub: ${scn.stubPath}`);
|
|
1034
|
+
console.log("");
|
|
1035
|
+
}
|
|
1036
|
+
console.log(`confirmedAt: ${spec.confirmedAt ?? "(not confirmed)"}`);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
export function runSpecifyCommand(_argv: string[], subcommand: string | undefined, issueId: string | undefined) {
|
|
1040
|
+
switch (subcommand) {
|
|
1041
|
+
case "confirm":
|
|
1042
|
+
runConfirm(issueId ?? "");
|
|
1043
|
+
return;
|
|
1044
|
+
case "revise":
|
|
1045
|
+
runRevise(issueId ?? "");
|
|
1046
|
+
return;
|
|
1047
|
+
case "show":
|
|
1048
|
+
runShow(issueId ?? "");
|
|
1049
|
+
return;
|
|
1050
|
+
default:
|
|
1051
|
+
throw new Error(`unknown specify subcommand: ${subcommand ?? "<none>"}; expected confirm|revise|show`);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
- [ ] **Step 2: Add routing in scripts/gxpm.ts**
|
|
1057
|
+
|
|
1058
|
+
Edit `scripts/gxpm.ts` — after the existing import block, add:
|
|
1059
|
+
|
|
1060
|
+
```ts
|
|
1061
|
+
import { runSpecifyCommand } from "./commands/specify";
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
In the command routing chain (after similar `if (command === "...")` blocks, before the phase-artifact lookup at line 180), add:
|
|
1065
|
+
|
|
1066
|
+
```ts
|
|
1067
|
+
if (command === "specify" && subcommand !== "init") {
|
|
1068
|
+
runSpecifyCommand(argv, subcommand, issueId);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
```
|
|
1072
|
+
|
|
1073
|
+
Note: `specify init` is handled by the auto-discovered phase-artifact framework (Task 7); only confirm/revise/show go through this routing.
|
|
1074
|
+
|
|
1075
|
+
- [ ] **Step 3: Smoke test the CLI**
|
|
1076
|
+
|
|
1077
|
+
Create a temporary issue and test the path:
|
|
1078
|
+
|
|
1079
|
+
```bash
|
|
1080
|
+
# Setup
|
|
1081
|
+
TMP=$(mktemp -d)
|
|
1082
|
+
cd "$TMP"
|
|
1083
|
+
bun run /Users/x/Desktop/Project/gxpm/scripts/gxpm.ts init
|
|
1084
|
+
# (Continue setup using gxpm CLI to reach specify phase, then:)
|
|
1085
|
+
# bun run scripts/gxpm.ts specify confirm G-001
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
Expected: Either success message or a meaningful schema/stub error. NOT "Unknown command".
|
|
1089
|
+
|
|
1090
|
+
- [ ] **Step 4: Commit**
|
|
1091
|
+
|
|
1092
|
+
```bash
|
|
1093
|
+
git add scripts/commands/specify.ts scripts/gxpm.ts
|
|
1094
|
+
git commit -m "GXPM-XXX add gxpm specify confirm/revise/show CLI
|
|
1095
|
+
|
|
1096
|
+
Wires the three subcommands through scripts/commands/specify.ts.
|
|
1097
|
+
The 'init' subcommand is intentionally left to the phase-artifact
|
|
1098
|
+
auto-discovery framework (see Task 7).
|
|
1099
|
+
|
|
1100
|
+
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
---
|
|
1104
|
+
|
|
1105
|
+
## Task 9: 创建 Gherkin 风格治理文档
|
|
1106
|
+
|
|
1107
|
+
**Files:**
|
|
1108
|
+
- Create: `docs/governance/gherkin-style.md`
|
|
1109
|
+
|
|
1110
|
+
- [ ] **Step 1: Write the governance document**
|
|
1111
|
+
|
|
1112
|
+
Create `docs/governance/gherkin-style.md`:
|
|
1113
|
+
|
|
1114
|
+
```markdown
|
|
1115
|
+
# Gherkin 写作风格指南 (v1)
|
|
1116
|
+
|
|
1117
|
+
> 本文档源自 AutomationPanda/gherkin-guidelines-for-ai (MIT),加 gxpm 本地补充。
|
|
1118
|
+
> 所有 gxpm-driven issue 在 specify 阶段必须遵循本规则集。
|
|
1119
|
+
|
|
1120
|
+
## 核心原则
|
|
1121
|
+
|
|
1122
|
+
- **行为驱动**:描述系统*做什么*而非*怎么做*
|
|
1123
|
+
- **领域语言**:使用 CONTEXT.md 中的术语,不出现 UI/HTTP/SQL 等技术词
|
|
1124
|
+
- **示例规约**:scenario 用具体例子展示行为
|
|
1125
|
+
- **每个 scenario 一个行为**
|
|
1126
|
+
- **可独立执行**:scenario 之间无顺序依赖
|
|
1127
|
+
|
|
1128
|
+
## Feature 结构
|
|
1129
|
+
|
|
1130
|
+
- 单一 `Feature:` 标题与文件名一致
|
|
1131
|
+
- User Story 紧跟标题:
|
|
1132
|
+
- `As a <role>`
|
|
1133
|
+
- `I want <goal>`
|
|
1134
|
+
- `So that <reason>`
|
|
1135
|
+
|
|
1136
|
+
## Scenario 设计规则
|
|
1137
|
+
|
|
1138
|
+
- 单行、行为聚焦的标题
|
|
1139
|
+
- 步骤可按时间顺序执行
|
|
1140
|
+
- 声明式语言,非命令式
|
|
1141
|
+
- 不混合多个无关关注点(功能 + 性能 + 可访问性必须拆分)
|
|
1142
|
+
- 步骤数 < 10,超过用 data table
|
|
1143
|
+
|
|
1144
|
+
## Given / When / Then 语义
|
|
1145
|
+
|
|
1146
|
+
- `Given` 建立上下文(Arrange)
|
|
1147
|
+
- `When` 触发动作(Act)
|
|
1148
|
+
- `Then` 验证可观察结果(Assert)
|
|
1149
|
+
|
|
1150
|
+
**严格顺序**:`Given → When → Then`,禁止重复阶段。
|
|
1151
|
+
**关键词**:`And` 续相同类型(OK);`But` 用于对比(少用);**禁止 `Or`**。
|
|
1152
|
+
**可观察结果**:`Then` 必须可从场景文本验证——禁止 "it works"、"it succeeds"。
|
|
1153
|
+
|
|
1154
|
+
## 词汇与命名
|
|
1155
|
+
|
|
1156
|
+
- 整个 issue 内使用稳定词汇,禁止同义词替换
|
|
1157
|
+
- 第三人称、现在时、主谓结构
|
|
1158
|
+
- 字符串参数用双引号
|
|
1159
|
+
- 步骤数据用 doc string (`"""`) 或 data table,禁止用 `And` 串联
|
|
1160
|
+
|
|
1161
|
+
## 反模式(禁止)
|
|
1162
|
+
|
|
1163
|
+
- ❌ 在 Given/When/Then 写入 UI 选择器、XPath、`click #id`
|
|
1164
|
+
- ❌ 在 Then 写 SQL 断言、HTTP 状态码(除非这就是被测的接口)
|
|
1165
|
+
- ❌ 占位符数据:`foo`、`bar`、`test`、`123`(数字若有业务含义可用)
|
|
1166
|
+
- ❌ 一个 scenario 多个行为
|
|
1167
|
+
- ❌ 步骤超过 10 个
|
|
1168
|
+
- ❌ "用户登录" 描述成 10 步点击;改用状态:"用户已以 Editor 身份登录"
|
|
1169
|
+
|
|
1170
|
+
## gxpm 本地补充
|
|
1171
|
+
|
|
1172
|
+
- **中文允许**:领域词允许中文,但同一 issue 内保持中英一致
|
|
1173
|
+
- **必须引用 CONTEXT.md 术语**:scenario 中提及的实体名必须在 CONTEXT.md 中有定义
|
|
1174
|
+
- **stubPath 必须真实**:每个 scenario 的 `stubPath` 字段指向的测试文件必须存在
|
|
1175
|
+
- **scenario.id 命名**:`scn-NN`(两位数字补零)
|
|
1176
|
+
|
|
1177
|
+
## Pre-confirm 自查清单
|
|
1178
|
+
|
|
1179
|
+
specifier agent 在调用 `gxpm specify confirm` 前必须自查:
|
|
1180
|
+
|
|
1181
|
+
- [ ] 单一行为,可独立执行
|
|
1182
|
+
- [ ] 无混合无关关注点
|
|
1183
|
+
- [ ] 词汇稳定,引用 CONTEXT.md 术语
|
|
1184
|
+
- [ ] 领域级抽象,无 UI/HTTP/SQL 管道术语
|
|
1185
|
+
- [ ] 最小但充分的 Given
|
|
1186
|
+
- [ ] 真实示例数据(无 foo/bar/test)
|
|
1187
|
+
- [ ] 第三人称、现在时、主谓结构
|
|
1188
|
+
- [ ] 严格 Given → When → Then,Then 可观察
|
|
1189
|
+
- [ ] 步骤数 < 10
|
|
1190
|
+
- [ ] 每个 scenario.stubPath 真实存在
|
|
1191
|
+
|
|
1192
|
+
## 参考
|
|
1193
|
+
|
|
1194
|
+
- AutomationPanda/gherkin-guidelines-for-ai (https://github.com/AutomationPanda/gherkin-guidelines-for-ai)
|
|
1195
|
+
- gxpm CONTEXT.md(领域词典)
|
|
1196
|
+
```
|
|
1197
|
+
|
|
1198
|
+
- [ ] **Step 2: Commit**
|
|
1199
|
+
|
|
1200
|
+
```bash
|
|
1201
|
+
git add docs/governance/gherkin-style.md
|
|
1202
|
+
git commit -m "GXPM-XXX add Gherkin style governance for specify phase
|
|
1203
|
+
|
|
1204
|
+
Codifies AutomationPanda's open-source Gherkin guidelines plus
|
|
1205
|
+
gxpm-local supplements (Chinese OK, must reference CONTEXT.md,
|
|
1206
|
+
scn-NN ID format, stubPath must be real). The pre-confirm
|
|
1207
|
+
self-check list is the specifier agent's gate before calling
|
|
1208
|
+
'gxpm specify confirm'.
|
|
1209
|
+
|
|
1210
|
+
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
---
|
|
1214
|
+
|
|
1215
|
+
## Task 10: 创建 specifier agent 模板
|
|
1216
|
+
|
|
1217
|
+
**Files:**
|
|
1218
|
+
- Create: `agents/specifier.md`
|
|
1219
|
+
|
|
1220
|
+
- [ ] **Step 1: Write the agent template**
|
|
1221
|
+
|
|
1222
|
+
Create `agents/specifier.md`:
|
|
1223
|
+
|
|
1224
|
+
```markdown
|
|
1225
|
+
# Specifier Agent
|
|
1226
|
+
|
|
1227
|
+
## Role
|
|
1228
|
+
|
|
1229
|
+
Specifier 是 gxpm `specify` 阶段的唯一 owner。其职责是接收 dispatch-handoff,产出可被用户确认的 Gherkin 行为规约(behavior-spec artifact)。
|
|
1230
|
+
|
|
1231
|
+
**Specifier 不写实现代码,不写测试逻辑代码。仅产出行为注释 + 空测试 stub。**
|
|
1232
|
+
|
|
1233
|
+
## Inputs
|
|
1234
|
+
|
|
1235
|
+
- `.gxpm/issues/<id>/artifacts/acceptance-contract.json`(来自 triage)
|
|
1236
|
+
- `.gxpm/issues/<id>/artifacts/implementation-plan.json`(来自 plan)
|
|
1237
|
+
- `.gxpm/issues/<id>/artifacts/dispatch-handoff.json`(来自 dispatch)
|
|
1238
|
+
- `docs/governance/gherkin-style.md`(必读)
|
|
1239
|
+
- `CONTEXT.md`(领域词典)
|
|
1240
|
+
- `test/**` 下既有测试文件(few-shot 范本)
|
|
1241
|
+
|
|
1242
|
+
## Outputs
|
|
1243
|
+
|
|
1244
|
+
- `.gxpm/issues/<id>/artifacts/behavior-spec.json`(结构化 Gherkin 规约,confirmedAt=null)
|
|
1245
|
+
- `test/**/<area>/<name>.test.ts`(空 stub 文件,每个 scenario 一个空测试函数 + Gherkin 注释)
|
|
1246
|
+
|
|
1247
|
+
## Operating Procedure
|
|
1248
|
+
|
|
1249
|
+
1. **加载 skill**:`skills/gxpm-specifier/SKILL.md`
|
|
1250
|
+
2. **读取上游 artifact**:理解需求范围
|
|
1251
|
+
3. **加载 Gherkin 规则**:`docs/governance/gherkin-style.md`
|
|
1252
|
+
4. **查询 few-shot 范本**:在 test/ 下选择 1-2 个既有测试做为风格参考
|
|
1253
|
+
5. **草拟行为规约**:每个用户故事 → 1 Feature + N Scenarios(N≥1)
|
|
1254
|
+
6. **生成 stub 文件**:为每个 scenario 产出空测试函数 + Gherkin 注释
|
|
1255
|
+
7. **运行 `gxpm specify init <id>`**:写入 behavior-spec.json
|
|
1256
|
+
8. **向用户呈现**:调用 AskUserQuestion 工具(若 host 支持)或终端输出场景摘要
|
|
1257
|
+
9. **根据反馈迭代**:调整后重新生成 stub 文件(保持 scenario.id 稳定)
|
|
1258
|
+
10. **用户确认后**:运行 `gxpm specify confirm <id>`
|
|
1259
|
+
|
|
1260
|
+
## Hard Rules(不可违反)
|
|
1261
|
+
|
|
1262
|
+
- **禁止** 在 specify 阶段写任何测试逻辑代码(函数体必须为空 / pass)
|
|
1263
|
+
- **禁止** 在用户 confirm 之前推进到 implement 阶段
|
|
1264
|
+
- **禁止** 跳过 `docs/governance/gherkin-style.md` 自查清单
|
|
1265
|
+
- **禁止** scenario 步骤超过 10 个;超过必须拆分 scenario
|
|
1266
|
+
- **禁止** 使用 foo/bar/test 等占位符数据
|
|
1267
|
+
|
|
1268
|
+
## Handoff to Implementer
|
|
1269
|
+
|
|
1270
|
+
confirmedAt 写入后,implementer agent 接管。implementer 从 behavior-spec.json 读取 scenario,按 RED→GREEN→REFACTOR 在每个 stub 文件中实现测试逻辑与产品代码。
|
|
1271
|
+
|
|
1272
|
+
## 相关文档
|
|
1273
|
+
|
|
1274
|
+
- `skills/gxpm-specifier/SKILL.md`
|
|
1275
|
+
- `skills/gxpm-tdd/SKILL.md`(下游)
|
|
1276
|
+
- `docs/governance/gherkin-style.md`
|
|
1277
|
+
- `docs/brainstorms/2026-05-14-bdd-then-tdd-design.md`
|
|
1278
|
+
```
|
|
1279
|
+
|
|
1280
|
+
- [ ] **Step 2: Commit**
|
|
1281
|
+
|
|
1282
|
+
```bash
|
|
1283
|
+
git add agents/specifier.md
|
|
1284
|
+
git commit -m "GXPM-XXX add specifier agent template
|
|
1285
|
+
|
|
1286
|
+
Defines the specify-phase owner: inputs, outputs, operating
|
|
1287
|
+
procedure, hard rules, and the handoff contract to implementer.
|
|
1288
|
+
The hard rules forbid writing any test logic during specify
|
|
1289
|
+
and ban placeholders.
|
|
1290
|
+
|
|
1291
|
+
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
|
|
1292
|
+
```
|
|
1293
|
+
|
|
1294
|
+
---
|
|
1295
|
+
|
|
1296
|
+
## Task 11: 创建 gxpm-specifier skill
|
|
1297
|
+
|
|
1298
|
+
**Files:**
|
|
1299
|
+
- Create: `skills/gxpm-specifier/SKILL.md`
|
|
1300
|
+
- Modify: `skills-lock.json` (via maintain-hygiene-skills-lock skill)
|
|
1301
|
+
|
|
1302
|
+
- [ ] **Step 1: Write the skill**
|
|
1303
|
+
|
|
1304
|
+
Create `skills/gxpm-specifier/SKILL.md`:
|
|
1305
|
+
|
|
1306
|
+
```markdown
|
|
1307
|
+
---
|
|
1308
|
+
name: gxpm-specifier
|
|
1309
|
+
description: BDD 行为规约设计 skill。在 gxpm specify 阶段使用,强制先产出 Gherkin 行为注释 + 空测试 stub,由用户确认后才能进入 TDD。触发场景:用户在 specify 阶段、用户提到 BDD、Gherkin、Given-When-Then、行为规约、行为先行。
|
|
1310
|
+
---
|
|
1311
|
+
|
|
1312
|
+
# gxpm-specifier
|
|
1313
|
+
|
|
1314
|
+
## Core Principle
|
|
1315
|
+
|
|
1316
|
+
**Specify is BDD. Implement is TDD. The two must be separated by a user confirmation.**
|
|
1317
|
+
|
|
1318
|
+
在 specify 阶段,**不写一行测试逻辑代码**。产出的仅是 Gherkin 行为注释 + 空函数 stub + structured artifact。
|
|
1319
|
+
|
|
1320
|
+
## 入口条件
|
|
1321
|
+
|
|
1322
|
+
- gxpm issue 处于 `specify` phase
|
|
1323
|
+
- 用户要求"先写行为再写代码"、"BDD 先行"、"Given-When-Then"
|
|
1324
|
+
- dispatch-handoff.json 已存在
|
|
1325
|
+
|
|
1326
|
+
## Hard Rules
|
|
1327
|
+
|
|
1328
|
+
```
|
|
1329
|
+
NO TEST LOGIC IN SPECIFY PHASE
|
|
1330
|
+
NO IMPLEMENTATION CODE IN SPECIFY PHASE
|
|
1331
|
+
NO PLACEHOLDER DATA (foo/bar/test/123)
|
|
1332
|
+
NO SCENARIO WITH > 10 STEPS
|
|
1333
|
+
NO MIXED CONCERNS IN ONE SCENARIO
|
|
1334
|
+
```
|
|
1335
|
+
|
|
1336
|
+
违反任一条 = 删除产出,从 `gxpm specify init` 重新开始。
|
|
1337
|
+
|
|
1338
|
+
## 可操作流程
|
|
1339
|
+
|
|
1340
|
+
1. 读取上游 artifact:acceptance-contract、implementation-plan、dispatch-handoff
|
|
1341
|
+
2. 读取治理文档:`docs/governance/gherkin-style.md`
|
|
1342
|
+
3. 在 `test/` 下找 1-2 个既有测试文件作为风格参照
|
|
1343
|
+
4. 草拟 Feature + Scenarios(每 scenario `given`/`when`/`then` 各 ≥1 项)
|
|
1344
|
+
5. 为每个 scenario 生成空 stub:
|
|
1345
|
+
```ts
|
|
1346
|
+
// Feature: <title>
|
|
1347
|
+
//
|
|
1348
|
+
// Scenario (scn-01): <name>
|
|
1349
|
+
// Given <given[0]>
|
|
1350
|
+
// And <given[1]>
|
|
1351
|
+
// When <when>
|
|
1352
|
+
// Then <then[0]>
|
|
1353
|
+
// And <then[1]>
|
|
1354
|
+
|
|
1355
|
+
test("test_<scenario_name_in_snake_case>", () => {
|
|
1356
|
+
// intentionally empty — awaiting user confirmation
|
|
1357
|
+
});
|
|
1358
|
+
```
|
|
1359
|
+
6. 运行 `gxpm specify init <issue-id>` 写入 behavior-spec.json
|
|
1360
|
+
7. 编辑 behavior-spec.json,填充 feature/scenarios/stubPath 真实值(`gxpm specify edit` 或直接编辑)
|
|
1361
|
+
8. 调用 AskUserQuestion 呈现三选项:
|
|
1362
|
+
- 行为正确,继续
|
|
1363
|
+
- 需要调整:用户反馈 → 回到步骤 4
|
|
1364
|
+
- 补充边界场景:增加 scenario → 回到步骤 4
|
|
1365
|
+
9. 用户确认后运行 `gxpm specify confirm <issue-id>`
|
|
1366
|
+
|
|
1367
|
+
## 红旗清单
|
|
1368
|
+
|
|
1369
|
+
立即停止并重新开始:
|
|
1370
|
+
|
|
1371
|
+
- 在 specify 阶段写了 expect/assert 语句
|
|
1372
|
+
- 用 foo/bar/test 等占位符
|
|
1373
|
+
- scenario 步骤 > 10
|
|
1374
|
+
- 一个 scenario 同时测功能 + 性能
|
|
1375
|
+
- 在 Then 写 UI 选择器、HTTP 状态码(除非接口本身被测)
|
|
1376
|
+
- 跳过用户确认直接 `gxpm specify confirm`
|
|
1377
|
+
- 跳过 specify 直接 implement(phase-gate 会拒绝)
|
|
1378
|
+
|
|
1379
|
+
## 验证清单
|
|
1380
|
+
|
|
1381
|
+
每次 specify confirm 前自查:
|
|
1382
|
+
|
|
1383
|
+
- [ ] 单一行为,可独立执行
|
|
1384
|
+
- [ ] 无混合关注点
|
|
1385
|
+
- [ ] 词汇稳定,CONTEXT.md 术语对齐
|
|
1386
|
+
- [ ] 领域级抽象,无 UI/HTTP/SQL 管道术语
|
|
1387
|
+
- [ ] 最小但充分的 Given
|
|
1388
|
+
- [ ] 真实示例数据
|
|
1389
|
+
- [ ] 第三人称、现在时、主谓结构
|
|
1390
|
+
- [ ] 严格 Given→When→Then,Then 可观察
|
|
1391
|
+
- [ ] 步骤数 < 10
|
|
1392
|
+
- [ ] 每个 scenario.stubPath 真实存在
|
|
1393
|
+
- [ ] 用户已通过 AskUserQuestion 或终端确认
|
|
1394
|
+
|
|
1395
|
+
## Handoff
|
|
1396
|
+
|
|
1397
|
+
confirmedAt 写入 → phase 可转 implement → gxpm-tdd skill 接管。
|
|
1398
|
+
```
|
|
1399
|
+
|
|
1400
|
+
- [ ] **Step 2: Update skills-lock.json**
|
|
1401
|
+
|
|
1402
|
+
Use the `maintain-hygiene-skills-lock` skill or run:
|
|
1403
|
+
```bash
|
|
1404
|
+
bun run scripts/update-skills-lock.ts 2>&1 | tail -5
|
|
1405
|
+
```
|
|
1406
|
+
|
|
1407
|
+
If no such script exists, manually compute SHA256 and append to `skills-lock.json`. Verify with:
|
|
1408
|
+
```bash
|
|
1409
|
+
bun run scripts/scaffold-check.ts | grep specifier
|
|
1410
|
+
```
|
|
1411
|
+
|
|
1412
|
+
Expected: no integrity error related to gxpm-specifier.
|
|
1413
|
+
|
|
1414
|
+
- [ ] **Step 3: Commit**
|
|
1415
|
+
|
|
1416
|
+
```bash
|
|
1417
|
+
git add skills/gxpm-specifier/SKILL.md skills-lock.json
|
|
1418
|
+
git commit -m "GXPM-XXX add gxpm-specifier BDD skill
|
|
1419
|
+
|
|
1420
|
+
Discipline skill enforcing the rule that specify phase produces
|
|
1421
|
+
only Gherkin comments + empty test stubs; no test logic, no
|
|
1422
|
+
placeholders, no scenario > 10 steps. Includes hard rules,
|
|
1423
|
+
operating procedure, and a pre-confirm self-check list.
|
|
1424
|
+
|
|
1425
|
+
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
|
|
1426
|
+
```
|
|
1427
|
+
|
|
1428
|
+
---
|
|
1429
|
+
|
|
1430
|
+
## Task 12: 升级 gxpm-tdd skill 强制引用 specify.json
|
|
1431
|
+
|
|
1432
|
+
**Files:**
|
|
1433
|
+
- Modify: `skills/gxpm-tdd/SKILL.md`
|
|
1434
|
+
- Modify: `skills-lock.json`
|
|
1435
|
+
|
|
1436
|
+
- [ ] **Step 1: Add a "Specify-First Iron Law" section**
|
|
1437
|
+
|
|
1438
|
+
Edit `skills/gxpm-tdd/SKILL.md` — after the existing "### The Iron Law" section, insert a new section:
|
|
1439
|
+
|
|
1440
|
+
```markdown
|
|
1441
|
+
### The Specify-First Iron Law
|
|
1442
|
+
|
|
1443
|
+
**Before writing ANY test logic, the test scenario MUST already exist in `.gxpm/issues/<id>/artifacts/behavior-spec.json`.**
|
|
1444
|
+
|
|
1445
|
+
If you find yourself writing a test without a corresponding entry in behavior-spec.json:
|
|
1446
|
+
- STOP
|
|
1447
|
+
- Delete the test code you wrote
|
|
1448
|
+
- Return to specify phase: `gxpm phase rewind <id> --to specify --reason "missing scenario"`
|
|
1449
|
+
- Add the scenario to behavior-spec.json
|
|
1450
|
+
- Re-confirm with `gxpm specify confirm <id>`
|
|
1451
|
+
- Then resume TDD
|
|
1452
|
+
|
|
1453
|
+
**Why:** The two flows are non-negotiable serial: BDD describes WHAT behavior we want; TDD enforces THAT behavior incrementally. Jumping to TDD without a confirmed BDD spec means agent is inventing test cases, which is the precise failure mode this discipline prevents.
|
|
1454
|
+
|
|
1455
|
+
**The test stub file at `scenario.stubPath` is your contract.** Open it; the Gherkin comment block at the top is the only legitimate source of assertions you may translate into code.
|
|
1456
|
+
```
|
|
1457
|
+
|
|
1458
|
+
- [ ] **Step 2: Add a red-flag entry**
|
|
1459
|
+
|
|
1460
|
+
In the existing "### 必须立即停止并重新开始的情况" section, add to the bullet list:
|
|
1461
|
+
|
|
1462
|
+
```markdown
|
|
1463
|
+
- Writing a test without a matching scenario in behavior-spec.json
|
|
1464
|
+
- Adding assertions that do not appear in the scenario's Then clauses
|
|
1465
|
+
```
|
|
1466
|
+
|
|
1467
|
+
- [ ] **Step 3: Update skills-lock.json**
|
|
1468
|
+
|
|
1469
|
+
```bash
|
|
1470
|
+
bun run scripts/update-skills-lock.ts 2>&1 | tail -5
|
|
1471
|
+
# or manually update SHA256
|
|
1472
|
+
bun run scripts/scaffold-check.ts | grep gxpm-tdd
|
|
1473
|
+
```
|
|
1474
|
+
|
|
1475
|
+
Expected: no integrity error.
|
|
1476
|
+
|
|
1477
|
+
- [ ] **Step 4: Commit**
|
|
1478
|
+
|
|
1479
|
+
```bash
|
|
1480
|
+
git add skills/gxpm-tdd/SKILL.md skills-lock.json
|
|
1481
|
+
git commit -m "GXPM-XXX gate gxpm-tdd on behavior-spec scenarios
|
|
1482
|
+
|
|
1483
|
+
Adds Specify-First Iron Law: no test logic can be written
|
|
1484
|
+
unless a matching scenario exists in behavior-spec.json. The
|
|
1485
|
+
scenario.stubPath file is now the only legitimate source of
|
|
1486
|
+
assertions. Two new red flags added to the rationalization
|
|
1487
|
+
table.
|
|
1488
|
+
|
|
1489
|
+
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
|
|
1490
|
+
```
|
|
1491
|
+
|
|
1492
|
+
---
|
|
1493
|
+
|
|
1494
|
+
## Task 13: 创建 stub 文件模板
|
|
1495
|
+
|
|
1496
|
+
**Files:**
|
|
1497
|
+
- Create: `templates/specify-stub.tmpl`
|
|
1498
|
+
|
|
1499
|
+
- [ ] **Step 1: Write template**
|
|
1500
|
+
|
|
1501
|
+
Create `templates/specify-stub.tmpl`:
|
|
1502
|
+
|
|
1503
|
+
```
|
|
1504
|
+
// Feature: {{feature.title}}
|
|
1505
|
+
//
|
|
1506
|
+
// As a {{feature.asA}}
|
|
1507
|
+
// I want {{feature.iWant}}
|
|
1508
|
+
// So that {{feature.soThat}}
|
|
1509
|
+
//
|
|
1510
|
+
{{#each scenarios}}
|
|
1511
|
+
// Scenario ({{this.id}}): {{this.name}}
|
|
1512
|
+
{{#each this.given}}
|
|
1513
|
+
// {{#if @first}}Given{{else}}And{{/if}} {{this}}
|
|
1514
|
+
{{/each}}
|
|
1515
|
+
// When {{this.when}}
|
|
1516
|
+
{{#each this.then}}
|
|
1517
|
+
// {{#if @first}}Then{{else}}And{{/if}} {{this}}
|
|
1518
|
+
{{/each}}
|
|
1519
|
+
|
|
1520
|
+
test("{{this.testName}}", () => {
|
|
1521
|
+
// intentionally empty — awaiting user confirmation
|
|
1522
|
+
// implement only after `gxpm specify confirm <issue-id>`
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
{{/each}}
|
|
1526
|
+
```
|
|
1527
|
+
|
|
1528
|
+
Note: This template uses a handlebars-like syntax. Actual rendering is the specifier agent's responsibility for now (MVP: agent writes the file directly, template is reference). A renderer can be added later if needed.
|
|
1529
|
+
|
|
1530
|
+
- [ ] **Step 2: Commit**
|
|
1531
|
+
|
|
1532
|
+
```bash
|
|
1533
|
+
git add templates/specify-stub.tmpl
|
|
1534
|
+
git commit -m "GXPM-XXX add specify-stub template for reference
|
|
1535
|
+
|
|
1536
|
+
Reference template for what an empty BDD stub file should look
|
|
1537
|
+
like. The specifier agent writes the file directly from this
|
|
1538
|
+
shape; an automated renderer can be added later.
|
|
1539
|
+
|
|
1540
|
+
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
|
|
1541
|
+
```
|
|
1542
|
+
|
|
1543
|
+
---
|
|
1544
|
+
|
|
1545
|
+
## Task 14: 集成测试 — 端到端 CLI 流程
|
|
1546
|
+
|
|
1547
|
+
**Files:**
|
|
1548
|
+
- Create: `test/functional/gxpm-specify/init-confirm.test.ts`
|
|
1549
|
+
|
|
1550
|
+
- [ ] **Step 1: Write end-to-end test**
|
|
1551
|
+
|
|
1552
|
+
Create `test/functional/gxpm-specify/init-confirm.test.ts`:
|
|
1553
|
+
|
|
1554
|
+
```ts
|
|
1555
|
+
import { describe, expect, it, beforeEach, afterEach } from "bun:test";
|
|
1556
|
+
import { mkdtempSync, rmSync, writeFileSync, readFileSync, mkdirSync, existsSync } from "node:fs";
|
|
1557
|
+
import { tmpdir } from "node:os";
|
|
1558
|
+
import { join } from "node:path";
|
|
1559
|
+
import { spawnSync } from "node:child_process";
|
|
1560
|
+
|
|
1561
|
+
const GXPM_CLI = join(import.meta.dir, "..", "..", "..", "scripts", "gxpm.ts");
|
|
1562
|
+
|
|
1563
|
+
function runCli(cwd: string, args: string[]) {
|
|
1564
|
+
return spawnSync("bun", ["run", GXPM_CLI, ...args], { cwd, encoding: "utf8" });
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
describe("gxpm specify CLI end-to-end", () => {
|
|
1568
|
+
let root: string;
|
|
1569
|
+
|
|
1570
|
+
beforeEach(() => {
|
|
1571
|
+
root = mkdtempSync(join(tmpdir(), "gxpm-specify-e2e-"));
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
afterEach(() => {
|
|
1575
|
+
rmSync(root, { recursive: true, force: true });
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
it("init produces a draft behavior-spec with confirmedAt=null", () => {
|
|
1579
|
+
// Manually create issue in specify phase (skipping triage/plan/dispatch to focus on specify)
|
|
1580
|
+
const issueDir = join(root, ".gxpm", "issues", "G-100");
|
|
1581
|
+
mkdirSync(join(issueDir, "artifacts"), { recursive: true });
|
|
1582
|
+
writeFileSync(
|
|
1583
|
+
join(issueDir, "state.json"),
|
|
1584
|
+
JSON.stringify({
|
|
1585
|
+
schemaVersion: 1,
|
|
1586
|
+
issueId: "G-100",
|
|
1587
|
+
currentPhase: "specify",
|
|
1588
|
+
createdAt: "2026-05-14T00:00:00Z",
|
|
1589
|
+
updatedAt: "2026-05-14T00:00:00Z",
|
|
1590
|
+
stateRoot: ".gxpm/issues/G-100",
|
|
1591
|
+
artifactRoot: ".gxpm/issues/G-100/artifacts",
|
|
1592
|
+
phaseHistory: [],
|
|
1593
|
+
}, null, 2),
|
|
1594
|
+
);
|
|
1595
|
+
writeFileSync(join(issueDir, "events.jsonl"), "");
|
|
1596
|
+
writeFileSync(
|
|
1597
|
+
join(issueDir, "graph.json"),
|
|
1598
|
+
JSON.stringify({ phases: [], currentPhase: "specify", transitions: [] }),
|
|
1599
|
+
);
|
|
1600
|
+
|
|
1601
|
+
const r = runCli(root, ["specify", "init", "G-100"]);
|
|
1602
|
+
expect(r.status).toBe(0);
|
|
1603
|
+
|
|
1604
|
+
const specPath = join(issueDir, "artifacts", "behavior-spec.json");
|
|
1605
|
+
expect(existsSync(specPath)).toBe(true);
|
|
1606
|
+
const stored = JSON.parse(readFileSync(specPath, "utf8"));
|
|
1607
|
+
expect(stored.payload.confirmedAt).toBeNull();
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
it("confirm fails when stubPath does not exist", () => {
|
|
1611
|
+
const issueDir = join(root, ".gxpm", "issues", "G-101");
|
|
1612
|
+
mkdirSync(join(issueDir, "artifacts"), { recursive: true });
|
|
1613
|
+
writeFileSync(
|
|
1614
|
+
join(issueDir, "state.json"),
|
|
1615
|
+
JSON.stringify({
|
|
1616
|
+
schemaVersion: 1,
|
|
1617
|
+
issueId: "G-101",
|
|
1618
|
+
currentPhase: "specify",
|
|
1619
|
+
createdAt: "2026-05-14T00:00:00Z",
|
|
1620
|
+
updatedAt: "2026-05-14T00:00:00Z",
|
|
1621
|
+
stateRoot: ".gxpm/issues/G-101",
|
|
1622
|
+
artifactRoot: ".gxpm/issues/G-101/artifacts",
|
|
1623
|
+
phaseHistory: [],
|
|
1624
|
+
}, null, 2),
|
|
1625
|
+
);
|
|
1626
|
+
writeFileSync(join(issueDir, "events.jsonl"), "");
|
|
1627
|
+
writeFileSync(join(issueDir, "graph.json"), JSON.stringify({}));
|
|
1628
|
+
|
|
1629
|
+
writeFileSync(
|
|
1630
|
+
join(issueDir, "artifacts", "behavior-spec.json"),
|
|
1631
|
+
JSON.stringify({
|
|
1632
|
+
schemaVersion: 1,
|
|
1633
|
+
issueId: "G-101",
|
|
1634
|
+
type: "behavior-spec",
|
|
1635
|
+
writtenAt: "2026-05-14T00:00:00Z",
|
|
1636
|
+
payload: {
|
|
1637
|
+
$schema: "behavior-spec.v1",
|
|
1638
|
+
issueId: "G-101",
|
|
1639
|
+
createdAt: "2026-05-14T00:00:00Z",
|
|
1640
|
+
createdBy: "specifier",
|
|
1641
|
+
confirmedAt: null,
|
|
1642
|
+
confirmedBy: null,
|
|
1643
|
+
feature: { title: "T", asA: "a", iWant: "b", soThat: "c" },
|
|
1644
|
+
scenarios: [
|
|
1645
|
+
{
|
|
1646
|
+
id: "scn-01",
|
|
1647
|
+
name: "n",
|
|
1648
|
+
given: ["g"],
|
|
1649
|
+
when: "w",
|
|
1650
|
+
then: ["t"],
|
|
1651
|
+
examples: [],
|
|
1652
|
+
stubPath: "test/does-not-exist.test.ts",
|
|
1653
|
+
},
|
|
1654
|
+
],
|
|
1655
|
+
guidelinesRef: "docs/governance/gherkin-style.md@v1",
|
|
1656
|
+
},
|
|
1657
|
+
}, null, 2),
|
|
1658
|
+
);
|
|
1659
|
+
|
|
1660
|
+
const r = runCli(root, ["specify", "confirm", "G-101"]);
|
|
1661
|
+
expect(r.status).not.toBe(0);
|
|
1662
|
+
expect(r.stderr).toMatch(/Stub file missing/);
|
|
1663
|
+
});
|
|
1664
|
+
});
|
|
1665
|
+
```
|
|
1666
|
+
|
|
1667
|
+
- [ ] **Step 2: Run the test**
|
|
1668
|
+
|
|
1669
|
+
Run: `bun test test/functional/gxpm-specify/init-confirm.test.ts`
|
|
1670
|
+
Expected: PASS — both end-to-end tests pass.
|
|
1671
|
+
|
|
1672
|
+
- [ ] **Step 3: Run full test suite**
|
|
1673
|
+
|
|
1674
|
+
Run: `bun test`
|
|
1675
|
+
Expected: All previously-passing tests still pass. New tests pass.
|
|
1676
|
+
|
|
1677
|
+
- [ ] **Step 4: Commit**
|
|
1678
|
+
|
|
1679
|
+
```bash
|
|
1680
|
+
git add test/functional/gxpm-specify/init-confirm.test.ts
|
|
1681
|
+
git commit -m "GXPM-XXX e2e tests for gxpm specify init/confirm CLI
|
|
1682
|
+
|
|
1683
|
+
Two end-to-end scenarios: (1) init produces draft behavior-spec
|
|
1684
|
+
with confirmedAt=null; (2) confirm fails with 'Stub file missing'
|
|
1685
|
+
when scenario.stubPath does not exist on disk.
|
|
1686
|
+
|
|
1687
|
+
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
|
|
1688
|
+
```
|
|
1689
|
+
|
|
1690
|
+
---
|
|
1691
|
+
|
|
1692
|
+
## Task 15: 升级 docs/AGENTS.md / CANON.md 引用
|
|
1693
|
+
|
|
1694
|
+
**Files:**
|
|
1695
|
+
- Modify: `AGENTS.md`
|
|
1696
|
+
|
|
1697
|
+
- [ ] **Step 1: Add specify to phase reference**
|
|
1698
|
+
|
|
1699
|
+
Read `AGENTS.md` — locate any section listing phases (e.g., the "Command 阶段协议" table referenced in the file). Add `specify` to the phase list, with a one-line description.
|
|
1700
|
+
|
|
1701
|
+
Find a section similar to:
|
|
1702
|
+
```
|
|
1703
|
+
| Command 阶段协议(12 命令) | CLI phase commands + `core/phase-gates.ts` | ✅ 已转译为状态机 |
|
|
1704
|
+
```
|
|
1705
|
+
|
|
1706
|
+
Append a new row below the existing four-dimension table, or in a phases sub-section if one exists:
|
|
1707
|
+
|
|
1708
|
+
```markdown
|
|
1709
|
+
**Phase 顺序(13 阶段)**:
|
|
1710
|
+
triage → plan → dispatch → **specify** → implement → local-verify → ac-check → self-review → ship → pr-check → verify → qa → land
|
|
1711
|
+
|
|
1712
|
+
- **specify**: BDD 行为规约阶段。产出 `behavior-spec.json` artifact + 空测试 stub;必须由用户通过 `gxpm specify confirm <id>` 显式确认后才能进入 implement。Owner: `specifier`. Skill: `gxpm-specifier`. 规则: `docs/governance/gherkin-style.md`.
|
|
1713
|
+
```
|
|
1714
|
+
|
|
1715
|
+
- [ ] **Step 2: Commit**
|
|
1716
|
+
|
|
1717
|
+
```bash
|
|
1718
|
+
git add AGENTS.md
|
|
1719
|
+
git commit -m "GXPM-XXX document specify phase in AGENTS.md
|
|
1720
|
+
|
|
1721
|
+
Records the new 13-phase order and a one-line summary of the
|
|
1722
|
+
specify phase: owner, skill, governance doc, and the hard
|
|
1723
|
+
requirement of user confirmation via `gxpm specify confirm`.
|
|
1724
|
+
|
|
1725
|
+
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
|
|
1726
|
+
```
|
|
1727
|
+
|
|
1728
|
+
---
|
|
1729
|
+
|
|
1730
|
+
## Risks
|
|
1731
|
+
|
|
1732
|
+
1. **现有 in-flight issues 的兼容性**:spec 第 5.1 节通过 `phaseHistory` 跳过校验,需在 Task 4 的 `assertArtifactGate` 中实现这一豁免(已在 spec 描述,本 plan 未单独建任务——若发现现有 issue 在测试中受影响,追加 Task 4.5)。
|
|
1733
|
+
|
|
1734
|
+
2. **skills-lock.json 完整性**:Task 11、12 新增/修改 skill 必须同步更新 skills-lock,否则 CI 会失败。若 `scripts/update-skills-lock.ts` 不存在,需查阅 `maintain-hygiene-skills-lock` skill 的指引。
|
|
1735
|
+
|
|
1736
|
+
3. **AskUserQuestion 在 CLI 环境不可用**:MVP 通过显式 CLI confirm 命令规避——但 specifier skill 中提及"调用 AskUserQuestion",在裸 CLI 下需 fallback 到终端 prompt,已在 spec 第 4 节明确,不影响门控正确性。
|
|
1737
|
+
|
|
1738
|
+
4. **`gxpm phase rewind` 命令未在本 plan 实现**:spec § 5.2 提及但 MVP 不实现,留作 follow-up plan。当前流程通过 `gxpm specify revise` 已能满足"未跨阶段时回退"的核心需求;跨阶段回退仅在 implement 阶段发现 scenario 漏缺时才需要,属于低频路径。
|
|
1739
|
+
|
|
1740
|
+
5. **Skill 压力测试未在本 plan 实现**:spec § 6.3 描述的 Meta-TDD 压力测试("用户说赶时间,跳过 BDD"、"凭直觉写测试代码"两个场景)属于 skill 治理工程,应由 `gxpm-eval` skill 在独立 follow-up plan 中实现。本 plan 已包含基础单元 + 集成测试足以验证流程正确性。
|
|
1741
|
+
|
|
1742
|
+
6. **集成测试对 state.json schema 假设**:Task 14 中手动 seed state.json,若 IssueState schema 变化需同步更新。建议未来抽出 test helper。
|
|
1743
|
+
|
|
1744
|
+
7. **Dogfood 自合规检查脚本未实现**:spec § 6.2 提及 `scripts/dogfood-check.ts` + pre-push hook,spec 已明确为"可推迟到 MVP 后"。留作 follow-up。
|
|
1745
|
+
|
|
1746
|
+
---
|
|
1747
|
+
|
|
1748
|
+
## Validation
|
|
1749
|
+
|
|
1750
|
+
完成 Task 1-15 后整体验证:
|
|
1751
|
+
|
|
1752
|
+
1. `bun test` — 所有测试通过
|
|
1753
|
+
2. `bun run scripts/gxpm.ts check` — scaffold 检查通过
|
|
1754
|
+
3. `bun run scripts/gxpm.ts specify init G-XXX` 在一个新建 specify 阶段 issue 上能产出 behavior-spec.json
|
|
1755
|
+
4. `bun run scripts/gxpm.ts specify confirm G-XXX` 在 stubPath 真实存在时能成功
|
|
1756
|
+
5. `bun run scripts/gxpm.ts implement init G-XXX` 在 confirmedAt=null 时被 phase-gate 拒绝,错误信息明确
|
|
1757
|
+
|
|
1758
|
+
---
|
|
1759
|
+
|
|
1760
|
+
## Rollback Plan
|
|
1761
|
+
|
|
1762
|
+
若整体回滚:
|
|
1763
|
+
1. `git revert` 所有 Task 提交(按反序)
|
|
1764
|
+
2. 已创建的 specify phase issue 需手动迁移:编辑 `.gxpm/issues/<id>/state.json` 将 `currentPhase` 从 `"specify"` 改回 `"dispatch"`
|
|
1765
|
+
3. 删除 `.gxpm/issues/<id>/artifacts/behavior-spec.json`
|
|
1766
|
+
|
|
1767
|
+
若分段回滚:每个 commit 独立,可单独 revert,但 Task 3(phase-gate 拆分)和 Task 4(confirmedAt 校验)必须一起回滚以保持 state.ts 与 phase-gates.ts 的一致性。
|