intentdna 1.5.13 → 1.5.15

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.
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Intent DNA — DNAStateManager
3
+ *
4
+ * Class-based facade over state.ts module functions. Bundles projectDir +
5
+ * sessionId into a single object so call sites don't repeat those args.
6
+ * Adds experience chain persistence (markdown + structured JSON) and
7
+ * lifecycle operations (init / cleanup / isStale) that state.ts lacks.
8
+ *
9
+ * Existing state.ts functions remain the canonical I/O primitives — this
10
+ * class delegates to them. Prefer this class in new code; state.ts exports
11
+ * are @deprecated for direct use.
12
+ */
13
+ import { readFile, mkdir, unlink, stat } from "node:fs/promises";
14
+ import { join } from "node:path";
15
+ import { resolveStateDir, readWorkflowState as rwState, writeWorkflowState as wwState, clearWorkflowState as cwState, appendAudit as aAudit, readSurgeonAttempts as rSurg, writeSurgeonAttempts as wSurg, readSessionReads as rReads, appendSessionRead as aRead, atomicWrite, } from "./state.js";
16
+ const EXPERIENCE_JSON = "workflow/experience.json";
17
+ const EXPERIENCE_MD = "workflow/experience.md";
18
+ const DEFAULT_STALENESS_MS = 2 * 60 * 60 * 1000; // 2h — matches state.ts
19
+ // ── Manager ────────────────────────────────────────────────
20
+ export class DNAStateManager {
21
+ projectDir;
22
+ sessionId;
23
+ constructor(projectDir, sessionId) {
24
+ this.projectDir = projectDir;
25
+ this.sessionId = sessionId;
26
+ }
27
+ // ── Workflow state ──
28
+ readWorkflowState(stalenessMs = DEFAULT_STALENESS_MS) {
29
+ return rwState(this.projectDir, this.sessionId, stalenessMs);
30
+ }
31
+ writeWorkflowState(state) {
32
+ return wwState(this.projectDir, state, this.sessionId);
33
+ }
34
+ clearWorkflowState() {
35
+ return cwState(this.projectDir, this.sessionId);
36
+ }
37
+ // ── Audit log ──
38
+ appendAudit(entry) {
39
+ return aAudit(this.projectDir, entry);
40
+ }
41
+ // ── Surgeon attempts ──
42
+ readSurgeonAttempts() {
43
+ return rSurg(this.projectDir, this.sessionId);
44
+ }
45
+ writeSurgeonAttempts(state) {
46
+ return wSurg(this.projectDir, state, this.sessionId);
47
+ }
48
+ // ── Session reads ──
49
+ readSessionReads() {
50
+ return rReads(this.projectDir, this.sessionId);
51
+ }
52
+ appendSessionRead(filePath) {
53
+ return aRead(this.projectDir, filePath, this.sessionId);
54
+ }
55
+ // ── Experience chain (new) ──
56
+ async readExperienceChain() {
57
+ const path = this.path(EXPERIENCE_JSON);
58
+ try {
59
+ const raw = await readFile(path, "utf-8");
60
+ const parsed = JSON.parse(raw);
61
+ return Array.isArray(parsed) ? parsed : [];
62
+ }
63
+ catch {
64
+ return [];
65
+ }
66
+ }
67
+ async writeExperienceChain(chain) {
68
+ await atomicWrite(this.path(EXPERIENCE_JSON), JSON.stringify(chain, null, 2));
69
+ await atomicWrite(this.path(EXPERIENCE_MD), renderExperienceMarkdown(chain));
70
+ }
71
+ async appendExperience(entry) {
72
+ const chain = await this.readExperienceChain();
73
+ chain.push(entry);
74
+ await this.writeExperienceChain(chain);
75
+ }
76
+ // ── Lifecycle ──
77
+ /** Ensure session directory structure exists. */
78
+ async init() {
79
+ await mkdir(resolveStateDir(this.projectDir, this.sessionId), { recursive: true });
80
+ }
81
+ /**
82
+ * Remove this manager's state files.
83
+ * Callers that need trace rotation should invoke rotateTraces() from state.ts separately.
84
+ */
85
+ async cleanup() {
86
+ const dir = resolveStateDir(this.projectDir, this.sessionId);
87
+ for (const rel of ["workflow.json", EXPERIENCE_JSON, EXPERIENCE_MD, "workflow/surgeon-attempts.json", "workflow/session-reads.json"]) {
88
+ await unlink(join(dir, rel)).catch(() => { });
89
+ }
90
+ }
91
+ /** True when workflow.json started_at exceeds the staleness threshold. */
92
+ async isStale(stalenessMs = DEFAULT_STALENESS_MS) {
93
+ const wfPath = join(resolveStateDir(this.projectDir, this.sessionId), "workflow.json");
94
+ try {
95
+ const raw = await readFile(wfPath, "utf-8");
96
+ const state = JSON.parse(raw);
97
+ if (!state.started_at)
98
+ return false;
99
+ return Date.now() - new Date(state.started_at).getTime() > stalenessMs;
100
+ }
101
+ catch {
102
+ // No workflow.json — fall back to directory mtime for unparseable state
103
+ try {
104
+ const s = await stat(wfPath);
105
+ return Date.now() - s.mtimeMs > stalenessMs;
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ }
111
+ }
112
+ path(rel) {
113
+ return join(resolveStateDir(this.projectDir, this.sessionId), rel);
114
+ }
115
+ }
116
+ // ── Markdown rendering ────────────────────────────────────
117
+ function renderExperienceMarkdown(chain) {
118
+ if (chain.length === 0) {
119
+ return "# Experience chain\n\n_Empty — no attempts yet._\n";
120
+ }
121
+ const lines = ["# Experience chain", ""];
122
+ for (const e of chain) {
123
+ lines.push(`## Round ${e.round} — ${e.timestamp}`);
124
+ lines.push("");
125
+ lines.push(`**Analysis:** ${e.analysis}`);
126
+ lines.push("");
127
+ lines.push(`**Attempt:** ${e.attempt}`);
128
+ lines.push("");
129
+ lines.push(`**Result:** ${e.result}`);
130
+ lines.push("");
131
+ lines.push(`**Lesson:** ${e.lesson}`);
132
+ lines.push("");
133
+ }
134
+ return lines.join("\n");
135
+ }
@@ -56,20 +56,24 @@ export interface SurgeonAttemptState {
56
56
  export declare function resolveStateDir(projectDir: string, sessionId?: string): string;
57
57
  /**
58
58
  * Read current workflow state. Returns null if not found or stale.
59
+ * @deprecated Prefer `new DNAStateManager(projectDir, sessionId).readWorkflowState()`.
59
60
  */
60
61
  export declare function readWorkflowState(projectDir: string, sessionId?: string, stalenessMs?: number): Promise<DNAWorkflowState | null>;
61
62
  /**
62
63
  * Write workflow state atomically.
64
+ * @deprecated Prefer `DNAStateManager.writeWorkflowState()`.
63
65
  */
64
66
  export declare function writeWorkflowState(projectDir: string, state: DNAWorkflowState, sessionId?: string): Promise<void>;
65
67
  /**
66
68
  * Clear workflow state (workflow complete).
69
+ * @deprecated Prefer `DNAStateManager.clearWorkflowState()`.
67
70
  */
68
71
  export declare function clearWorkflowState(projectDir: string, sessionId?: string): Promise<void>;
69
72
  /**
70
73
  * Append an entry to the audit log with dedup protection.
71
74
  * Dedup key: event + tool_name + timestamp (second-level).
72
75
  * Log file: `.dna/audit/violations-YYYY-MM-DD.log` (JSON Lines format)
76
+ * @deprecated Prefer `DNAStateManager.appendAudit()`.
73
77
  */
74
78
  export declare function appendAudit(projectDir: string, entry: AuditEntry): Promise<void>;
75
79
  /**
@@ -84,10 +88,12 @@ export declare function appendCompletedArtifact(projectDir: string, stepId: stri
84
88
  export declare function atomicWrite(filePath: string, data: string): Promise<void>;
85
89
  /**
86
90
  * Read surgeon attempt state. Returns default state if not found.
91
+ * @deprecated Prefer `DNAStateManager.readSurgeonAttempts()`.
87
92
  */
88
93
  export declare function readSurgeonAttempts(projectDir: string, sessionId?: string): Promise<SurgeonAttemptState>;
89
94
  /**
90
95
  * Write surgeon attempt state atomically.
96
+ * @deprecated Prefer `DNAStateManager.writeSurgeonAttempts()`.
91
97
  */
92
98
  export declare function writeSurgeonAttempts(projectDir: string, state: SurgeonAttemptState, sessionId?: string): Promise<void>;
93
99
  /** Tracks which files a session has Read — for context gate enforcement */
@@ -97,6 +103,7 @@ export interface SessionReadsState {
97
103
  }
98
104
  /**
99
105
  * Read session reads state. Returns empty reads if not found.
106
+ * @deprecated Prefer `DNAStateManager.readSessionReads()`.
100
107
  */
101
108
  export declare function readSessionReads(projectDir: string, sessionId?: string): Promise<SessionReadsState>;
102
109
  /**
@@ -105,6 +112,7 @@ export declare function readSessionReads(projectDir: string, sessionId?: string)
105
112
  export declare function writeSessionReads(projectDir: string, state: SessionReadsState, sessionId?: string): Promise<void>;
106
113
  /**
107
114
  * Append a file path to session reads (dedup).
115
+ * @deprecated Prefer `DNAStateManager.appendSessionRead()`.
108
116
  */
109
117
  export declare function appendSessionRead(projectDir: string, filePath: string, sessionId?: string): Promise<void>;
110
118
  /** Trace entry for hook call observability */
@@ -30,6 +30,7 @@ export function resolveStateDir(projectDir, sessionId) {
30
30
  // ── Workflow State ─────────────────────────────────────────
31
31
  /**
32
32
  * Read current workflow state. Returns null if not found or stale.
33
+ * @deprecated Prefer `new DNAStateManager(projectDir, sessionId).readWorkflowState()`.
33
34
  */
34
35
  export async function readWorkflowState(projectDir, sessionId, stalenessMs = DEFAULT_STALENESS_MS) {
35
36
  const stateDir = resolveStateDir(projectDir, sessionId);
@@ -51,6 +52,7 @@ export async function readWorkflowState(projectDir, sessionId, stalenessMs = DEF
51
52
  }
52
53
  /**
53
54
  * Write workflow state atomically.
55
+ * @deprecated Prefer `DNAStateManager.writeWorkflowState()`.
54
56
  */
55
57
  export async function writeWorkflowState(projectDir, state, sessionId) {
56
58
  const stateDir = resolveStateDir(projectDir, sessionId);
@@ -60,6 +62,7 @@ export async function writeWorkflowState(projectDir, state, sessionId) {
60
62
  }
61
63
  /**
62
64
  * Clear workflow state (workflow complete).
65
+ * @deprecated Prefer `DNAStateManager.clearWorkflowState()`.
63
66
  */
64
67
  export async function clearWorkflowState(projectDir, sessionId) {
65
68
  const stateDir = resolveStateDir(projectDir, sessionId);
@@ -76,6 +79,7 @@ export async function clearWorkflowState(projectDir, sessionId) {
76
79
  * Append an entry to the audit log with dedup protection.
77
80
  * Dedup key: event + tool_name + timestamp (second-level).
78
81
  * Log file: `.dna/audit/violations-YYYY-MM-DD.log` (JSON Lines format)
82
+ * @deprecated Prefer `DNAStateManager.appendAudit()`.
79
83
  */
80
84
  export async function appendAudit(projectDir, entry) {
81
85
  const auditDir = join(projectDir, ".dna", "audit");
@@ -153,6 +157,7 @@ function defaultSurgeonState() {
153
157
  }
154
158
  /**
155
159
  * Read surgeon attempt state. Returns default state if not found.
160
+ * @deprecated Prefer `DNAStateManager.readSurgeonAttempts()`.
156
161
  */
157
162
  export async function readSurgeonAttempts(projectDir, sessionId) {
158
163
  const stateDir = resolveStateDir(projectDir, sessionId);
@@ -167,6 +172,7 @@ export async function readSurgeonAttempts(projectDir, sessionId) {
167
172
  }
168
173
  /**
169
174
  * Write surgeon attempt state atomically.
175
+ * @deprecated Prefer `DNAStateManager.writeSurgeonAttempts()`.
170
176
  */
171
177
  export async function writeSurgeonAttempts(projectDir, state, sessionId) {
172
178
  const stateDir = resolveStateDir(projectDir, sessionId);
@@ -176,6 +182,7 @@ export async function writeSurgeonAttempts(projectDir, state, sessionId) {
176
182
  const SESSION_READS_FILE = "workflow/session-reads.json";
177
183
  /**
178
184
  * Read session reads state. Returns empty reads if not found.
185
+ * @deprecated Prefer `DNAStateManager.readSessionReads()`.
179
186
  */
180
187
  export async function readSessionReads(projectDir, sessionId) {
181
188
  const stateDir = resolveStateDir(projectDir, sessionId);
@@ -198,6 +205,7 @@ export async function writeSessionReads(projectDir, state, sessionId) {
198
205
  }
199
206
  /**
200
207
  * Append a file path to session reads (dedup).
208
+ * @deprecated Prefer `DNAStateManager.appendSessionRead()`.
201
209
  */
202
210
  export async function appendSessionRead(projectDir, filePath, sessionId) {
203
211
  const state = await readSessionReads(projectDir, sessionId);
@@ -66,6 +66,10 @@ const VALID_EVENTS = new Set([
66
66
  "SubagentStop", "PreCompact", "Notification", "SessionStart",
67
67
  ]);
68
68
  function dispatchEnforce(event, ir, input, roles) {
69
+ const result = dispatchEnforceInner(event, ir, input, roles);
70
+ return result?.output ?? { continue: true, suppressOutput: true };
71
+ }
72
+ function dispatchEnforceInner(event, ir, input, roles) {
69
73
  switch (event) {
70
74
  case "PreToolUse":
71
75
  return enforcePreToolUse(ir, {
@@ -100,10 +104,10 @@ function dispatchEnforce(event, ir, input, roles) {
100
104
  case "SessionStart":
101
105
  return enforceSessionStart(ir, {
102
106
  cwd: typeof input.cwd === "string" ? input.cwd : undefined,
103
- session_id: typeof input.session_id === "string" ? input.session_id : undefined,
107
+ sessionId: typeof input.session_id === "string" ? input.session_id : undefined,
104
108
  });
105
109
  default:
106
- return { continue: true, suppressOutput: true };
110
+ return null;
107
111
  }
108
112
  }
109
113
  // ── Tool Registration ───────────────────────────────────────
@@ -131,9 +131,9 @@ context_files:
131
131
  - "docs/behavior/{{ARGUMENTS}}.md"
132
132
  analysis_reviewer:
133
133
  - "docs/behavior/{{ARGUMENTS}}.md"
134
- - ".omc/specs/diagnosis-{{ARGUMENTS}}.md"
134
+ - ".dna/specs/diagnosis-{{ARGUMENTS}}.md"
135
135
  surgeon:
136
- - ".omc/specs/diagnosis-{{ARGUMENTS}}.md"
136
+ - ".dna/specs/diagnosis-{{ARGUMENTS}}.md"
137
137
  test_runner:
138
138
  - "docs/behavior/{{ARGUMENTS}}.md"
139
139
 
@@ -201,7 +201,7 @@ roles:
201
201
  read: ["**/*"]
202
202
  write: ["lib/**", "v2/**", "test/**"]
203
203
  instructions:
204
- - "REQUIRED FIRST: Read context files — CLAUDE.md, docs/refactoring-workflow-v2.md, v2/docs/PROVIDER_DESIGN.md, and the diagnosis spec (.omc/specs/diagnosis-$ARGUMENTS.md). Then read v1 corresponding file before any Edit. Hook will block Edit if context files are not read."
204
+ - "REQUIRED FIRST: Read context files — CLAUDE.md, docs/refactoring-workflow-v2.md, v2/docs/PROVIDER_DESIGN.md, and the diagnosis spec (.dna/specs/diagnosis-$ARGUMENTS.md). Then read v1 corresponding file before any Edit. Hook will block Edit if context files are not read."
205
205
  - Fix only the identified breakpoint or missing implementation
206
206
  - Read v1 to understand intent, rewrite in v2 framework style
207
207
  - Do not copy v1 code verbatim — adapt to v2 architecture
@@ -235,17 +235,17 @@ roles:
235
235
 
236
236
  # ── v2 角色: diagnosis + fix workflow ──
237
237
  analyzer:
238
- description: "Reads code and classifies failing tests. Read-only."
238
+ description: "Reads code and classifies failing tests. Writes diagnosis spec only."
239
239
  tool_permissions:
240
- allow: [Read, Grep, Glob]
241
- deny: [Bash, Edit, Write, NotebookEdit]
240
+ allow: [Read, Grep, Glob, Write]
241
+ deny: [Bash, Edit, NotebookEdit]
242
242
  scope:
243
243
  read: ["**/*"]
244
- write: []
244
+ write: [".dna/specs/**"]
245
245
  instructions:
246
246
  - "REQUIRED FIRST: Read all context files listed in SKILL.md"
247
- - "For each failing test: read test code + v1 impl + v2 impl"
248
- - "Classify each failure as BUG / INFRA / REMOVED / TEST_BUG"
247
+ - "For each failing, skipped, and hung test: read test code + v1 impl + v2 impl"
248
+ - "Classify each as BUG / UNIMPLEMENTED / INFRA / REMOVED / TEST_BUG (skipped tests are usually UNIMPLEMENTED)"
249
249
  - "Output diagnosis spec with v1 code snippets + v2 current state"
250
250
  - "DO NOT run tests. DO NOT edit code. Analysis only."
251
251
 
@@ -324,6 +324,10 @@ workflows:
324
324
  - Do NOT use `sleep N && check` polling — run commands in foreground
325
325
  - Do NOT use `timeout Nm flutter test` — let tests run to completion
326
326
  - Do NOT rewrite existing test files from scratch — update incrementally
327
+
328
+ Report results and STOP.
329
+
330
+ Next step for user: Run /dna-frw-diagnosis $ARGUMENTS to analyze failing tests.
327
331
  handoff:
328
332
  consumes:
329
333
  - type: file
@@ -514,19 +518,19 @@ workflows:
514
518
  For module $ARGUMENTS, analyze failing tests in baseline:
515
519
  1. Read behavior doc: docs/behavior/$ARGUMENTS.md
516
520
  2. Read test results from last behavior-lock run
517
- 3. For each failing test:
521
+ 3. For each failing, skipped, and hung test:
518
522
  a. Read test code (what behavior does it expect?)
519
523
  b. Read v1 implementation (how did v1 do this?)
520
524
  c. Read v2 current state (what's missing/wrong?)
521
- d. Classify: BUG / INFRA / REMOVED / TEST_BUG
522
- 4. Write .omc/specs/diagnosis-$ARGUMENTS.md with:
525
+ d. Classify: BUG / UNIMPLEMENTED / INFRA / REMOVED / TEST_BUG
526
+ 4. Write .dna/specs/diagnosis-$ARGUMENTS.md with:
523
527
  - For each failure: v1 code snippet + v2 current state + classification
524
528
  - NO fix prescriptions (surgeon decides how to implement)
525
529
  - NO running tests (analysis only)
526
530
  handoff:
527
531
  produces:
528
532
  - type: file
529
- path: ".omc/specs/diagnosis-$ARGUMENTS.md"
533
+ path: ".dna/specs/diagnosis-$ARGUMENTS.md"
530
534
  description: "Diagnosis spec for module"
531
535
 
532
536
  - id: review
@@ -535,7 +539,7 @@ workflows:
535
539
  max_attempts: 2
536
540
  description: "Review analyzer output quality"
537
541
  prompt: |
538
- Read context files + .omc/specs/diagnosis-$ARGUMENTS.md
542
+ Read context files + .dna/specs/diagnosis-$ARGUMENTS.md
539
543
 
540
544
  Verify:
541
545
  1. All failing tests from baseline covered?
@@ -548,7 +552,7 @@ workflows:
548
552
  handoff:
549
553
  consumes:
550
554
  - type: file
551
- path: ".omc/specs/diagnosis-$ARGUMENTS.md"
555
+ path: ".dna/specs/diagnosis-$ARGUMENTS.md"
552
556
  description: "Diagnosis spec from analyzer"
553
557
  produces:
554
558
  - type: summary
@@ -572,11 +576,12 @@ workflows:
572
576
 
573
577
  Read context + diagnosis spec first.
574
578
 
575
- Process issues in priority order: INFRA → BUG → TEST_BUG
579
+ Process issues in priority order: INFRA → BUG → UNIMPLEMENTED → TEST_BUG
576
580
  (REMOVED skipped — needs user confirmation)
577
581
 
578
582
  For each issue:
579
583
  - BUG: read v1 file, edit v2 (use Riverpod per refactoring-workflow-v2.md)
584
+ - UNIMPLEMENTED: read v1 impl, implement in v2 style, remove skip marker from test
580
585
  - INFRA: edit test_helpers only, do NOT touch v2/lib
581
586
  - TEST_BUG: re-read v1, edit test to match v1 behavior
582
587
 
@@ -588,7 +593,7 @@ workflows:
588
593
  handoff:
589
594
  consumes:
590
595
  - type: file
591
- path: ".omc/specs/diagnosis-$ARGUMENTS.md"
596
+ path: ".dna/specs/diagnosis-$ARGUMENTS.md"
592
597
  description: "Diagnosis spec"
593
598
  produces:
594
599
  - type: git_commit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intentdna",
3
- "version": "1.5.13",
3
+ "version": "1.5.15",
4
4
  "description": "Intent DNA — Declarative policy layer for AI agent behavior",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -20,7 +20,7 @@
20
20
  "license": "Apache-2.0",
21
21
  "repository": {
22
22
  "type": "git",
23
- "url": "https://github.com/lushan1314/intentdna.git"
23
+ "url": "git+https://github.com/lushan1314/intentdna-repo.git"
24
24
  },
25
25
  "keywords": [
26
26
  "ai",
@@ -41,6 +41,7 @@
41
41
  ],
42
42
  "devDependencies": {
43
43
  "@types/node": "^25.3.3",
44
+ "fast-check": "^4.7.0",
44
45
  "typescript": "^5.9.3",
45
46
  "vitest": "^4.0.18"
46
47
  }
@@ -93,6 +93,8 @@ npm test
93
93
  **测试**: +20
94
94
  **破坏性**: 无(纯增量)
95
95
 
96
+ **实施状态(2026-04-20 更新)**: 与 Phase 1B 合并完成,产出 +108 测试(schema+contract ≈18 属 1A,其余属 1B 断言重写)。评审 GO with reservations。未提交,3 个 blocker 待修(见本 spec 末尾"Phase 1A+1B 合并 Followup"章节)。
97
+
96
98
  ### 1A.1 新建 src/hooks/schema.ts
97
99
 
98
100
  ```typescript
@@ -163,6 +165,8 @@ npm test # 全绿,+20 新测试
163
165
  **破坏性**: 是(限定在此 phase)
164
166
  **依赖**: Phase 1A
165
167
 
168
+ **实施状态(2026-04-20 更新)**: 与 Phase 1A 一起完成(autopilot 超范围)。类型/签名/断言重写都到位,未提交。
169
+
166
170
  ### 1B.1 protocol.ts 新增类型
167
171
 
168
172
  ```typescript
@@ -261,13 +265,15 @@ npm test # 全绿,+15 新测试
261
265
 
262
266
  ---
263
267
 
264
- ## Phase 2A: DNAStateManager
268
+ ## Phase 2A: DNAStateManager ✓ (2026-04-21)
265
269
 
266
270
  **目标**: 统一所有 state 操作,支持 session reads + surgeon attempts + experience chain
267
- **测试**: +20
271
+ **测试**: +21 (state-manager.test.ts)
268
272
  **破坏性**: 无(旧函数 @deprecated 保留)
269
273
  **可与 Phase 1A 并行**
270
274
 
275
+ **实施**: `src/hooks/state-manager.ts` — 类 facade 封装 workflow / audit / surgeon / session reads,新增 experience chain (json + markdown dual-write) 和 init/cleanup/isStale 生命周期。state.ts 全部被 manager 替代的函数加 `@deprecated`。1076 tests pass (1055 existing + 21 new)。
276
+
271
277
  ### 2A.1 新建 src/hooks/state-manager.ts
272
278
 
273
279
  ```typescript
@@ -343,13 +349,15 @@ npm test # 全绿,+20 新测试
343
349
 
344
350
  ---
345
351
 
346
- ## Phase 2B: Context Gate + Reflection Gate + Workflow Boundary
352
+ ## Phase 2B: Context Gate + Reflection Gate + Workflow Boundary ✓ (2026-04-21)
347
353
 
348
354
  **目标**: 3 个核心 Harness 加固机制
349
- **测试**: +15
355
+ **测试**: +18 (hooks-harness.test.ts)
350
356
  **破坏性**: 无
351
357
  **依赖**: Phase 1B (EnforceResult) + Phase 2A (StateManager)
352
358
 
359
+ **实施**: `src/hooks/cli.ts:handlePreToolGates()` — PreToolUse 前置门,按优先级评估 workflow_boundary 和 context_gate。Context gate 已从 v1.5.12 的 PostToolUse warn 模式迁移到 PreToolUse block 模式(spec 已确认去掉 warn 过渡期)。Reflection gate 保持 v1.5.10 PostToolUse 实现。Session-read 追踪由 PostToolUse Read 路径写入 `.dna/state/sessions/<id>/workflow/session-reads.json`。1094 tests pass (1076 + 18 new)。
360
+
353
361
  ### 2B.1 Context Gate — src/hooks/cli.ts
354
362
 
355
363
  在 PreToolUse 处理中新增:
@@ -508,13 +516,20 @@ npm test # 全绿,+15 新测试
508
516
 
509
517
  ---
510
518
 
511
- ## Phase 3: Stop Hook 智能编排 + Integration Tests
519
+ ## Phase 3: Stop Hook 智能编排 + Property Tests ✓ (2026-04-21)
512
520
 
513
- **目标**: Stop hook 从"继续工作"升级为"输出下一步 Agent 调用";全链路集成测试
514
- **测试**: +25
521
+ **目标**: Stop hook 从"继续工作"升级为"输出下一步 Agent 调用";property-based 不变量验证
522
+ **测试**: +22 (9 stop-guidance + 13 property)
515
523
  **破坏性**: 无
516
524
  **依赖**: Phase 2B
517
525
 
526
+ **实施**:
527
+ - `src/hooks/cli.ts:buildWorkflowGuidance()` — Stop hook 读 workflow state + reflection-config sidecar + surgeon attempts,按 (完成 / surgeon 停滞 / 正常) 三种情况输出 STOP 指令或 `Agent(subagent_type=..., prompt=...)` 调用模板。
528
+ - `test/property/enforce-invariants.test.ts` — fast-check 属性测试 13 条,覆盖 checkContextReadiness 幂等性、checkReflectionLimit 三种分支边界、checkWorkflowBoundary completed=true/false 对偶、extractBashWritePaths / validateHookInput 不抛错 + 归一化一致性。
529
+ - **跳过**: subprocess integration tests(`spawn dist/hooks/cli.js` 全链路)— 需要预先 `npm run build`,留待后续补充。不影响 gate 逻辑验证,因为 buildWorkflowGuidance / handlePreToolGates 都是可直接导入的纯/半纯函数,已有完整单测。
530
+
531
+ 1116 tests pass.
532
+
518
533
  ### 3.1 Stop Hook 智能编排 — src/hooks/cli.ts
519
534
 
520
535
  在 enforceStop 中,读 workflow state 后输出明确的下一步指令:
@@ -669,27 +684,58 @@ dna sync # 新 agent/skill 生成
669
684
  - [ ] trace metadata 不进入 HookOutput(CC 协议边界)
670
685
  - [ ] mcp/tools-enforce.ts 同步更新
671
686
 
672
- ### Phase 2A
673
- - [ ] DNAStateManager class 实现
674
- - [ ] Session reads 追踪
675
- - [ ] Surgeon attempts 追踪
676
- - [ ] Experience chain 读写
677
- - [ ] init/cleanup 生命周期
678
- - [ ] state.ts 旧函数 @deprecated 保留
679
-
680
- ### Phase 2B
681
- - [ ] Context gate: block Edit/Bash/Write if context files not read
682
- - [ ] Reflection gate: detect no-progress, warn/handoff/skip
683
- - [ ] Workflow boundary: block Skill() after workflow completed
684
- - [ ] PostToolUse Read → appendSessionRead
685
- - [ ] parseGreenCount 支持 3 种格式 + null 安全
686
-
687
- ### Phase 3
688
- - [ ] Stop hook 读 workflow state 输出 Agent() 调用指令
689
- - [ ] Integration tests: spawn dna-hook 全链路
690
- - [ ] Property tests: 7 个不变量
687
+ ### Phase 2A
688
+ - [x] DNAStateManager class 实现
689
+ - [x] Session reads 追踪
690
+ - [x] Surgeon attempts 追踪
691
+ - [x] Experience chain 读写(json + markdown dual-write)
692
+ - [x] init/cleanup/isStale 生命周期
693
+ - [x] state.ts 旧函数 @deprecated 保留
694
+
695
+ ### Phase 2B
696
+ - [x] Context gate: block Edit/Bash/Write if context files not read (PreToolUse, block mode)
697
+ - [x] Reflection gate: detect no-progress, warn/handoff/skip (PostToolUse, v1.5.10+)
698
+ - [x] Workflow boundary: block Skill() after workflow completed (wfState.active=false)
699
+ - [x] PostToolUse Read → appendSessionRead
700
+ - [x] parseGreenCount 支持 3 种格式 + null 安全 (v1.5.10+ TEST_PASSED_RE)
701
+
702
+ ### Phase 3 ✓ (partial — integration tests deferred)
703
+ - [x] Stop hook 读 workflow state 输出 Agent() 调用指令 (buildWorkflowGuidance)
704
+ - [ ] Integration tests: spawn dna-hook 全链路 (deferred — 需 build 先行)
705
+ - [x] Property tests: 13 个不变量 (fast-check)
691
706
 
692
707
  ### Phase 4
693
708
  - [ ] dna verify --health 通过
694
709
  - [ ] lwk_flutter diagnosis → fix 流程验证
695
710
  - [ ] 发版
711
+
712
+ ---
713
+
714
+ ## Phase 1A+1B 合并 Followup(2026-04-20 评审产出 → 2026-04-21 已修)
715
+
716
+ autopilot 超范围将 1A+1B 一并完成,3 个 blocker 均已修复:
717
+
718
+ ### Blocker 1: schema.ts 接入 cli.ts ✓
719
+
720
+ **实施**: `src/hooks/cli.ts:24,125-137` — 在 `readStdin` 后、任何业务逻辑前调用 `validateHookInput(event, rawStdin)`;无效输入 fail-open(stderr 警告 + silent exit),有效输入使用 `validation.normalized` 作为后续 `rawInput`(session_id 已归一化为 sessionId)。
721
+
722
+ ### Blocker 2: matched_rule 语义修正 ✓
723
+
724
+ **实施**: `src/hooks/protocol.ts:100` — MatchedRule 枚举新增 `"validator"`;`src/hooks/enforce.ts:155-160` — audit-only 路径使用 `"validator"`,bash scope 违规仍用 `"scope"`(真实路径匹配)。
725
+
726
+ ### Blocker 3: commit 信息如实 ✓
727
+
728
+ **实施**: commit message 明确标注 "Phase 1A+1B 合并(autopilot 超范围)+ 3 blocker 修复"。
729
+
730
+ ### Phase 1A/1B 验收全 PASS
731
+
732
+ - [x] schema.ts validateHookInput() 实现 + 接入 cli.ts
733
+ - [x] 8 个真实 CC fixture 文件 + contract tests 全链路通过
734
+ - [x] SessionStartInput snake_case bug 已修
735
+ - [x] EnforceResult + TraceMetadata 类型定义
736
+ - [x] matched_rule 包含 context_gate / reflection_gate / workflow_boundary / validator
737
+ - [x] 8 个 enforce 函数返回 EnforceResult
738
+ - [x] cli.ts 不再猜 decision(读 `result.output`)
739
+ - [x] trace metadata 不进入 HookOutput(CC 协议边界)
740
+ - [x] mcp/tools-enforce.ts 同步更新
741
+ - [x] 全量测试 1055 passed