intentdna 1.5.11 → 1.5.13
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/dist/compiler/cascade.d.ts +5 -0
- package/dist/compiler/cascade.js +8 -0
- package/dist/compiler/compile.js +1 -0
- package/dist/hooks/cli.js +57 -2
- package/dist/hooks/enforce.d.ts +9 -0
- package/dist/hooks/enforce.js +9 -0
- package/dist/hooks/state.d.ts +17 -0
- package/dist/hooks/state.js +33 -0
- package/dist/runtime/skill-adapter.js +35 -0
- package/dist/schema/types.d.ts +10 -0
- package/dist/schema/validate.js +27 -0
- package/dist/templates/flutter-rewrite.dna.yaml +182 -3
- package/package.json +1 -1
- package/spec/hooks-infra-harness-hardening.md +695 -0
|
@@ -20,6 +20,11 @@ export interface CascadedDNA {
|
|
|
20
20
|
source_ids: string[];
|
|
21
21
|
/** Warnings from cascade process (e.g., cross-namespace gene collisions) */
|
|
22
22
|
warnings?: string[];
|
|
23
|
+
context_files?: {
|
|
24
|
+
mandatory?: string[];
|
|
25
|
+
per_role?: Record<string, string[]>;
|
|
26
|
+
per_workflow?: Record<string, string[]>;
|
|
27
|
+
};
|
|
23
28
|
}
|
|
24
29
|
/**
|
|
25
30
|
* Cascade multiple DNA layers into a single merged DNA.
|
package/dist/compiler/cascade.js
CHANGED
|
@@ -156,6 +156,13 @@ export function cascadeDNA(layers) {
|
|
|
156
156
|
allMarkers.push(...dna.epigenetic.markers);
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
|
+
// context_files: highest-priority DNA wins (last in sorted layers)
|
|
160
|
+
let contextFiles;
|
|
161
|
+
for (const dna of sorted) {
|
|
162
|
+
if (dna.context_files) {
|
|
163
|
+
contextFiles = dna.context_files;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
159
166
|
return {
|
|
160
167
|
genes: mergedGenes,
|
|
161
168
|
contexts: mergedContexts,
|
|
@@ -164,5 +171,6 @@ export function cascadeDNA(layers) {
|
|
|
164
171
|
epigenetic_markers: allMarkers,
|
|
165
172
|
source_ids: sourceIds,
|
|
166
173
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
174
|
+
context_files: contextFiles,
|
|
167
175
|
};
|
|
168
176
|
}
|
package/dist/compiler/compile.js
CHANGED
|
@@ -290,5 +290,6 @@ export function compileDNA(activated, cascaded) {
|
|
|
290
290
|
roles_scope_map: rolesScopeMap.length > 0 ? rolesScopeMap : undefined,
|
|
291
291
|
step_checkpoints: stepCheckpoints.length > 0 ? stepCheckpoints : undefined,
|
|
292
292
|
workflows_ir: workflowsIR.length > 0 ? workflowsIR : undefined,
|
|
293
|
+
context_files: cascaded?.context_files,
|
|
293
294
|
};
|
|
294
295
|
}
|
package/dist/hooks/cli.js
CHANGED
|
@@ -18,8 +18,8 @@ import { readFile, stat } from "node:fs/promises";
|
|
|
18
18
|
import { join, resolve } from "node:path";
|
|
19
19
|
import { randomUUID } from "node:crypto";
|
|
20
20
|
import { readStdin, writeOutput, silentOutput, allowOutput } from "./protocol.js";
|
|
21
|
-
import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, extractBashWritePaths, checkReflectionLimit, } from "./enforce.js";
|
|
22
|
-
import { appendAudit, readWorkflowState, appendTrace, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, writeSurgeonAttempts } from "./state.js";
|
|
21
|
+
import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, extractBashWritePaths, checkReflectionLimit, checkContextReadiness, } from "./enforce.js";
|
|
22
|
+
import { appendAudit, readWorkflowState, appendTrace, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, writeSurgeonAttempts, appendSessionRead, readSessionReads } from "./state.js";
|
|
23
23
|
// ── Constants ──────────────────────────────────────────────
|
|
24
24
|
const DEFAULT_IR_PATH = ".dna/compiled/ir.json";
|
|
25
25
|
const VALID_EVENTS = new Set([
|
|
@@ -164,6 +164,34 @@ async function main() {
|
|
|
164
164
|
const durationMs = Date.now() - start;
|
|
165
165
|
// Surgeon reflection gate (PostToolUse only, async)
|
|
166
166
|
if (event === "PostToolUse") {
|
|
167
|
+
// 1. Track session reads
|
|
168
|
+
try {
|
|
169
|
+
const toolName = String(rawInput.tool_name ?? "");
|
|
170
|
+
if (toolName === "Read") {
|
|
171
|
+
const readPath = typeof rawInput.tool_input?.file_path === "string"
|
|
172
|
+
? rawInput.tool_input.file_path : undefined;
|
|
173
|
+
if (readPath) {
|
|
174
|
+
await appendSessionRead(projectDir, readPath, sessionId);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// 2. Context gate (Phase 1: warn mode)
|
|
178
|
+
const writeTools = new Set(["Edit", "Write", "Bash"]);
|
|
179
|
+
if (writeTools.has(toolName)) {
|
|
180
|
+
const wfStateForCtx = await readWorkflowState(projectDir, sessionId);
|
|
181
|
+
if (wfStateForCtx?.active && ir.source_dna_ids.length > 0) {
|
|
182
|
+
const contextGateOutput = await handleContextGate(ir, wfStateForCtx, projectDir, sessionId);
|
|
183
|
+
if (contextGateOutput) {
|
|
184
|
+
if (output.suppressOutput) {
|
|
185
|
+
output = contextGateOutput;
|
|
186
|
+
}
|
|
187
|
+
else if (output.reason) {
|
|
188
|
+
output = { ...output, reason: output.reason + "\n" + (contextGateOutput.reason ?? "") };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch { /* fail-open */ }
|
|
167
195
|
try {
|
|
168
196
|
const reflectionOutput = await handleSurgeonReflection(ir, rawInput, projectDir, sessionId);
|
|
169
197
|
if (reflectionOutput) {
|
|
@@ -476,6 +504,33 @@ async function loadReflectionConfig(projectDir, _workflowName, _stepId) {
|
|
|
476
504
|
return null;
|
|
477
505
|
}
|
|
478
506
|
}
|
|
507
|
+
// ── Context Gate ─────────────────────────────────────────
|
|
508
|
+
/**
|
|
509
|
+
* Check if required context files have been read before allowing writes.
|
|
510
|
+
* Phase 1: warn mode (allow + inject warning).
|
|
511
|
+
* Returns null if all context files read or no context_files configured.
|
|
512
|
+
*/
|
|
513
|
+
async function handleContextGate(ir, wfState, projectDir, sessionId) {
|
|
514
|
+
if (!ir.context_files)
|
|
515
|
+
return null;
|
|
516
|
+
const required = [];
|
|
517
|
+
if (ir.context_files.mandatory)
|
|
518
|
+
required.push(...ir.context_files.mandatory);
|
|
519
|
+
if (ir.context_files.per_role?.[wfState.current_role]) {
|
|
520
|
+
required.push(...ir.context_files.per_role[wfState.current_role]);
|
|
521
|
+
}
|
|
522
|
+
if (ir.context_files.per_workflow?.[wfState.workflow]) {
|
|
523
|
+
required.push(...ir.context_files.per_workflow[wfState.workflow]);
|
|
524
|
+
}
|
|
525
|
+
if (required.length === 0)
|
|
526
|
+
return null;
|
|
527
|
+
const sessionReadsState = await readSessionReads(projectDir, sessionId);
|
|
528
|
+
const result = checkContextReadiness(sessionReadsState.read_files, required);
|
|
529
|
+
if (!result.ready) {
|
|
530
|
+
return allowOutput(`WARN [Intent DNA] Context Gate: Required files not yet read: ${result.missing.join(", ")}. Read them before continuing.`);
|
|
531
|
+
}
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
479
534
|
// ── Entry Point ────────────────────────────────────────────
|
|
480
535
|
main().catch(() => {
|
|
481
536
|
// Fail-open: never block Claude Code on unexpected errors
|
package/dist/hooks/enforce.d.ts
CHANGED
|
@@ -148,3 +148,12 @@ export interface ReflectionResult {
|
|
|
148
148
|
* Pure function — no I/O.
|
|
149
149
|
*/
|
|
150
150
|
export declare function checkReflectionLimit(failCount: number, maxAttempts: number, handoffCount: number, maxHandoffs: number): ReflectionResult;
|
|
151
|
+
export interface ContextReadinessResult {
|
|
152
|
+
ready: boolean;
|
|
153
|
+
missing: string[];
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Check if all required context files have been read in this session.
|
|
157
|
+
* Pure function — no I/O.
|
|
158
|
+
*/
|
|
159
|
+
export declare function checkContextReadiness(sessionReads: string[], required: string[]): ContextReadinessResult;
|
package/dist/hooks/enforce.js
CHANGED
|
@@ -703,3 +703,12 @@ export function checkReflectionLimit(failCount, maxAttempts, handoffCount, maxHa
|
|
|
703
703
|
}
|
|
704
704
|
return { action: "skip", reason: `No progress after ${maxAttempts} attempts and ${maxHandoffs} handoffs exhausted — skipping` };
|
|
705
705
|
}
|
|
706
|
+
/**
|
|
707
|
+
* Check if all required context files have been read in this session.
|
|
708
|
+
* Pure function — no I/O.
|
|
709
|
+
*/
|
|
710
|
+
export function checkContextReadiness(sessionReads, required) {
|
|
711
|
+
const readSet = new Set(sessionReads);
|
|
712
|
+
const missing = required.filter(f => !readSet.has(f));
|
|
713
|
+
return { ready: missing.length === 0, missing };
|
|
714
|
+
}
|
package/dist/hooks/state.d.ts
CHANGED
|
@@ -90,6 +90,23 @@ export declare function readSurgeonAttempts(projectDir: string, sessionId?: stri
|
|
|
90
90
|
* Write surgeon attempt state atomically.
|
|
91
91
|
*/
|
|
92
92
|
export declare function writeSurgeonAttempts(projectDir: string, state: SurgeonAttemptState, sessionId?: string): Promise<void>;
|
|
93
|
+
/** Tracks which files a session has Read — for context gate enforcement */
|
|
94
|
+
export interface SessionReadsState {
|
|
95
|
+
session_id: string;
|
|
96
|
+
read_files: string[];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Read session reads state. Returns empty reads if not found.
|
|
100
|
+
*/
|
|
101
|
+
export declare function readSessionReads(projectDir: string, sessionId?: string): Promise<SessionReadsState>;
|
|
102
|
+
/**
|
|
103
|
+
* Write session reads state atomically.
|
|
104
|
+
*/
|
|
105
|
+
export declare function writeSessionReads(projectDir: string, state: SessionReadsState, sessionId?: string): Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Append a file path to session reads (dedup).
|
|
108
|
+
*/
|
|
109
|
+
export declare function appendSessionRead(projectDir: string, filePath: string, sessionId?: string): Promise<void>;
|
|
93
110
|
/** Trace entry for hook call observability */
|
|
94
111
|
export interface TraceEntry {
|
|
95
112
|
trace_id: string;
|
package/dist/hooks/state.js
CHANGED
|
@@ -173,6 +173,39 @@ export async function writeSurgeonAttempts(projectDir, state, sessionId) {
|
|
|
173
173
|
const filePath = join(stateDir, SURGEON_ATTEMPTS_FILE);
|
|
174
174
|
await atomicWrite(filePath, JSON.stringify(state, null, 2));
|
|
175
175
|
}
|
|
176
|
+
const SESSION_READS_FILE = "workflow/session-reads.json";
|
|
177
|
+
/**
|
|
178
|
+
* Read session reads state. Returns empty reads if not found.
|
|
179
|
+
*/
|
|
180
|
+
export async function readSessionReads(projectDir, sessionId) {
|
|
181
|
+
const stateDir = resolveStateDir(projectDir, sessionId);
|
|
182
|
+
const filePath = join(stateDir, SESSION_READS_FILE);
|
|
183
|
+
try {
|
|
184
|
+
const raw = await readFile(filePath, "utf-8");
|
|
185
|
+
return JSON.parse(raw);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return { session_id: sessionId ?? "", read_files: [] };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Write session reads state atomically.
|
|
193
|
+
*/
|
|
194
|
+
export async function writeSessionReads(projectDir, state, sessionId) {
|
|
195
|
+
const stateDir = resolveStateDir(projectDir, sessionId);
|
|
196
|
+
const filePath = join(stateDir, SESSION_READS_FILE);
|
|
197
|
+
await atomicWrite(filePath, JSON.stringify(state, null, 2));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Append a file path to session reads (dedup).
|
|
201
|
+
*/
|
|
202
|
+
export async function appendSessionRead(projectDir, filePath, sessionId) {
|
|
203
|
+
const state = await readSessionReads(projectDir, sessionId);
|
|
204
|
+
if (!state.read_files.includes(filePath)) {
|
|
205
|
+
state.read_files.push(filePath);
|
|
206
|
+
await writeSessionReads(projectDir, state, sessionId);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
176
209
|
const TRACE_DIR = "trace";
|
|
177
210
|
const MAX_TRACE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
178
211
|
const TRACE_RETENTION_DAYS = 7;
|
|
@@ -42,6 +42,41 @@ export function compileWorkflowToSkill(plan, roles, ir, variables) {
|
|
|
42
42
|
lines.push("</Purpose>");
|
|
43
43
|
lines.push("");
|
|
44
44
|
}
|
|
45
|
+
// Required Context — context files that must be read before any work
|
|
46
|
+
if (ir?.context_files) {
|
|
47
|
+
const contextFiles = [];
|
|
48
|
+
if (ir.context_files.mandatory)
|
|
49
|
+
contextFiles.push(...ir.context_files.mandatory);
|
|
50
|
+
// per_workflow for this workflow
|
|
51
|
+
if (ir.context_files.per_workflow?.[plan.source_workflow]) {
|
|
52
|
+
contextFiles.push(...ir.context_files.per_workflow[plan.source_workflow]);
|
|
53
|
+
}
|
|
54
|
+
// per_role for all roles used in this workflow
|
|
55
|
+
if (ir.context_files.per_role) {
|
|
56
|
+
for (const step of plan.steps) {
|
|
57
|
+
const roleFiles = ir.context_files.per_role[step.role];
|
|
58
|
+
if (roleFiles) {
|
|
59
|
+
for (const f of roleFiles) {
|
|
60
|
+
if (!contextFiles.includes(f))
|
|
61
|
+
contextFiles.push(f);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (contextFiles.length > 0) {
|
|
67
|
+
lines.push("<Required_Context>");
|
|
68
|
+
lines.push("Your context is EMPTY at startup. You MUST read these files first:");
|
|
69
|
+
lines.push("");
|
|
70
|
+
for (let i = 0; i < contextFiles.length; i++) {
|
|
71
|
+
lines.push(`${i + 1}. ${contextFiles[i]}`);
|
|
72
|
+
}
|
|
73
|
+
lines.push("");
|
|
74
|
+
lines.push("Without reading these, you cannot perform your role.");
|
|
75
|
+
lines.push("Hook will warn on first Edit/Bash/Write until these are read.");
|
|
76
|
+
lines.push("</Required_Context>");
|
|
77
|
+
lines.push("");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
45
80
|
// Steps — each step MUST be executed via Agent() tool call
|
|
46
81
|
const usedRoles = new Set(plan.steps.map((s) => s.role));
|
|
47
82
|
// Pre-compute handoff targets: steps that are pointed to by another step's handoff_to
|
package/dist/schema/types.d.ts
CHANGED
|
@@ -232,6 +232,11 @@ export interface IntentDNA {
|
|
|
232
232
|
workflows?: Record<string, WorkflowDef>;
|
|
233
233
|
variables?: Record<string, string | VariableDef>;
|
|
234
234
|
mcp?: Record<string, MCPServerDef>;
|
|
235
|
+
context_files?: {
|
|
236
|
+
mandatory?: string[];
|
|
237
|
+
per_role?: Record<string, string[]>;
|
|
238
|
+
per_workflow?: Record<string, string[]>;
|
|
239
|
+
};
|
|
235
240
|
epigenetic: {
|
|
236
241
|
markers: EpigeneticMarker[];
|
|
237
242
|
};
|
|
@@ -338,6 +343,11 @@ export interface ConstraintIR {
|
|
|
338
343
|
roles_scope_map?: RoleScopeEntry[];
|
|
339
344
|
step_checkpoints?: StepCheckpointIR[];
|
|
340
345
|
workflows_ir?: WorkflowIR[];
|
|
346
|
+
context_files?: {
|
|
347
|
+
mandatory?: string[];
|
|
348
|
+
per_role?: Record<string, string[]>;
|
|
349
|
+
per_workflow?: Record<string, string[]>;
|
|
350
|
+
};
|
|
341
351
|
}
|
|
342
352
|
/** A compiled workflow step with resolved metadata */
|
|
343
353
|
export interface WorkflowStep {
|
package/dist/schema/validate.js
CHANGED
|
@@ -478,6 +478,33 @@ export function validateDNA(dna) {
|
|
|
478
478
|
}
|
|
479
479
|
}
|
|
480
480
|
}
|
|
481
|
+
// Validate context_files
|
|
482
|
+
if (dna.context_files) {
|
|
483
|
+
const cf = dna.context_files;
|
|
484
|
+
if (cf.per_role) {
|
|
485
|
+
for (const [roleName, paths] of Object.entries(cf.per_role)) {
|
|
486
|
+
if (roleNamesSet.size > 0 && !roleNamesSet.has(roleName)) {
|
|
487
|
+
warnings.push({ path: `context_files.per_role.${roleName}`, message: `references unknown role '${roleName}'` });
|
|
488
|
+
}
|
|
489
|
+
if (!Array.isArray(paths)) {
|
|
490
|
+
errors.push({ path: `context_files.per_role.${roleName}`, message: "must be an array of strings" });
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (cf.per_workflow) {
|
|
495
|
+
const wfNames = new Set(Object.keys(dna.workflows ?? {}));
|
|
496
|
+
if (dna.workflow)
|
|
497
|
+
wfNames.add(dna.workflow.name);
|
|
498
|
+
for (const [wfName, paths] of Object.entries(cf.per_workflow)) {
|
|
499
|
+
if (wfNames.size > 0 && !wfNames.has(wfName)) {
|
|
500
|
+
warnings.push({ path: `context_files.per_workflow.${wfName}`, message: `references unknown workflow '${wfName}'` });
|
|
501
|
+
}
|
|
502
|
+
if (!Array.isArray(paths)) {
|
|
503
|
+
errors.push({ path: `context_files.per_workflow.${wfName}`, message: "must be an array of strings" });
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
481
508
|
// Validate epigenetic markers
|
|
482
509
|
for (let i = 0; i < (dna.epigenetic?.markers?.length ?? 0); i++) {
|
|
483
510
|
const marker = dna.epigenetic.markers[i];
|
|
@@ -119,6 +119,24 @@ genes:
|
|
|
119
119
|
|
|
120
120
|
contexts: {}
|
|
121
121
|
|
|
122
|
+
# 上下文文件 — subagent 必读清单
|
|
123
|
+
context_files:
|
|
124
|
+
mandatory:
|
|
125
|
+
- CLAUDE.md
|
|
126
|
+
- docs/refactoring-workflow-v2.md
|
|
127
|
+
- v2/docs/PROVIDER_DESIGN.md
|
|
128
|
+
- v2/docs/memory/flutter_architecture_guide.md
|
|
129
|
+
per_role:
|
|
130
|
+
analyzer:
|
|
131
|
+
- "docs/behavior/{{ARGUMENTS}}.md"
|
|
132
|
+
analysis_reviewer:
|
|
133
|
+
- "docs/behavior/{{ARGUMENTS}}.md"
|
|
134
|
+
- ".omc/specs/diagnosis-{{ARGUMENTS}}.md"
|
|
135
|
+
surgeon:
|
|
136
|
+
- ".omc/specs/diagnosis-{{ARGUMENTS}}.md"
|
|
137
|
+
test_runner:
|
|
138
|
+
- "docs/behavior/{{ARGUMENTS}}.md"
|
|
139
|
+
|
|
122
140
|
|
|
123
141
|
roles:
|
|
124
142
|
# ── Phase 0/3: behavior-lock 角色 ──
|
|
@@ -183,6 +201,7 @@ roles:
|
|
|
183
201
|
read: ["**/*"]
|
|
184
202
|
write: ["lib/**", "v2/**", "test/**"]
|
|
185
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."
|
|
186
205
|
- Fix only the identified breakpoint or missing implementation
|
|
187
206
|
- Read v1 to understand intent, rewrite in v2 framework style
|
|
188
207
|
- Do not copy v1 code verbatim — adapt to v2 architecture
|
|
@@ -214,6 +233,51 @@ roles:
|
|
|
214
233
|
- Do not add methods v1 doesn't have
|
|
215
234
|
- Do not remove methods v1 has
|
|
216
235
|
|
|
236
|
+
# ── v2 角色: diagnosis + fix workflow ──
|
|
237
|
+
analyzer:
|
|
238
|
+
description: "Reads code and classifies failing tests. Read-only."
|
|
239
|
+
tool_permissions:
|
|
240
|
+
allow: [Read, Grep, Glob]
|
|
241
|
+
deny: [Bash, Edit, Write, NotebookEdit]
|
|
242
|
+
scope:
|
|
243
|
+
read: ["**/*"]
|
|
244
|
+
write: []
|
|
245
|
+
instructions:
|
|
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"
|
|
249
|
+
- "Output diagnosis spec with v1 code snippets + v2 current state"
|
|
250
|
+
- "DO NOT run tests. DO NOT edit code. Analysis only."
|
|
251
|
+
|
|
252
|
+
analysis_reviewer:
|
|
253
|
+
description: "Reviews analyzer output quality. Read-only."
|
|
254
|
+
tool_permissions:
|
|
255
|
+
allow: [Read, Grep, Glob]
|
|
256
|
+
deny: [Bash, Edit, Write, NotebookEdit]
|
|
257
|
+
scope:
|
|
258
|
+
read: ["**/*"]
|
|
259
|
+
write: []
|
|
260
|
+
instructions:
|
|
261
|
+
- "REQUIRED FIRST: Read all context files listed in SKILL.md"
|
|
262
|
+
- "Verify analyzer spec: v1 references exist? Classifications sound?"
|
|
263
|
+
- "Check missing: any failing test not covered?"
|
|
264
|
+
- "Output: APPROVE or REQUEST_REANALYSIS with specific feedback"
|
|
265
|
+
|
|
266
|
+
test_runner:
|
|
267
|
+
description: "Runs tests and reports progress delta."
|
|
268
|
+
tool_permissions:
|
|
269
|
+
allow: [Read, Grep, Glob, Bash]
|
|
270
|
+
deny: [Edit, Write, NotebookEdit]
|
|
271
|
+
scope:
|
|
272
|
+
read: ["**/*"]
|
|
273
|
+
write: []
|
|
274
|
+
instructions:
|
|
275
|
+
- "Run flutter test for the target module"
|
|
276
|
+
- "Parse output: green/red/skip counts"
|
|
277
|
+
- "Compare with baseline: fixed / new_red / stable"
|
|
278
|
+
- "Classify red tests by type (compile/logic/widget/hung)"
|
|
279
|
+
- "Output progress delta, not test content"
|
|
280
|
+
|
|
217
281
|
workflows:
|
|
218
282
|
behavior-lock:
|
|
219
283
|
name: Behavior Lock
|
|
@@ -253,9 +317,7 @@ workflows:
|
|
|
253
317
|
Scenario 2 (re-run/incremental):
|
|
254
318
|
- Read the incremental diff from behavior doc
|
|
255
319
|
- Update existing tests incrementally — do NOT rewrite all tests (preserves rescue progress)
|
|
256
|
-
- Run
|
|
257
|
-
- Classify results: passed / failed (behavior mismatch) / hung (timed out = mock infrastructure issue, NOT behavior failure)
|
|
258
|
-
- Append baseline to behavior doc with hung/failed distinction
|
|
320
|
+
- Run ALL tests (do NOT skip previously green tests). Compare with previous baseline: GREEN→RED = REGRESSION (mark HIGH_PRIORITY), RED→GREEN = FIXED, NEW = new coverage. Update baseline with complete results.
|
|
259
321
|
- Git commit: "behavior-lock($ARGUMENTS): N tests (X green, Y red from behavior change)"
|
|
260
322
|
|
|
261
323
|
BANNED patterns:
|
|
@@ -436,3 +498,120 @@ workflows:
|
|
|
436
498
|
role: core_aligner
|
|
437
499
|
description: "Compare {{v1_path}}/ and {{v2_path}}/core/ module by module. Fix mismatches."
|
|
438
500
|
prompt: "Compare v1 ({{v1_path}}/) and v2 core ({{v2_path}}/core/lib/). List mismatches. Fix by copying v1 logic, only changing DI (GetX→Riverpod). Run flutter analyze after each fix."
|
|
501
|
+
|
|
502
|
+
diagnosis:
|
|
503
|
+
name: Diagnosis
|
|
504
|
+
description: "Analyze failing tests, classify root causes, output diagnosis spec for module $ARGUMENTS"
|
|
505
|
+
steps:
|
|
506
|
+
- id: analyze
|
|
507
|
+
role: analyzer
|
|
508
|
+
max_attempts: 2
|
|
509
|
+
on_fail: retry_with_feedback
|
|
510
|
+
description: "Classify failing tests and output diagnosis spec"
|
|
511
|
+
prompt: |
|
|
512
|
+
Read all required context files first (enforced by hook).
|
|
513
|
+
|
|
514
|
+
For module $ARGUMENTS, analyze failing tests in baseline:
|
|
515
|
+
1. Read behavior doc: docs/behavior/$ARGUMENTS.md
|
|
516
|
+
2. Read test results from last behavior-lock run
|
|
517
|
+
3. For each failing test:
|
|
518
|
+
a. Read test code (what behavior does it expect?)
|
|
519
|
+
b. Read v1 implementation (how did v1 do this?)
|
|
520
|
+
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:
|
|
523
|
+
- For each failure: v1 code snippet + v2 current state + classification
|
|
524
|
+
- NO fix prescriptions (surgeon decides how to implement)
|
|
525
|
+
- NO running tests (analysis only)
|
|
526
|
+
handoff:
|
|
527
|
+
produces:
|
|
528
|
+
- type: file
|
|
529
|
+
path: ".omc/specs/diagnosis-$ARGUMENTS.md"
|
|
530
|
+
description: "Diagnosis spec for module"
|
|
531
|
+
|
|
532
|
+
- id: review
|
|
533
|
+
role: analysis_reviewer
|
|
534
|
+
depends_on: [analyze]
|
|
535
|
+
max_attempts: 2
|
|
536
|
+
description: "Review analyzer output quality"
|
|
537
|
+
prompt: |
|
|
538
|
+
Read context files + .omc/specs/diagnosis-$ARGUMENTS.md
|
|
539
|
+
|
|
540
|
+
Verify:
|
|
541
|
+
1. All failing tests from baseline covered?
|
|
542
|
+
2. v1 code references actually exist at claimed locations?
|
|
543
|
+
3. Classifications reasonable? (INFRA not mistaken for BUG)
|
|
544
|
+
4. Enough detail for surgeon to act?
|
|
545
|
+
|
|
546
|
+
APPROVE → diagnosis complete
|
|
547
|
+
REQUEST_REANALYSIS → list specific gaps, back to analyze
|
|
548
|
+
handoff:
|
|
549
|
+
consumes:
|
|
550
|
+
- type: file
|
|
551
|
+
path: ".omc/specs/diagnosis-$ARGUMENTS.md"
|
|
552
|
+
description: "Diagnosis spec from analyzer"
|
|
553
|
+
produces:
|
|
554
|
+
- type: summary
|
|
555
|
+
description: "Review verdict (APPROVE or REQUEST_REANALYSIS)"
|
|
556
|
+
|
|
557
|
+
fix:
|
|
558
|
+
name: Fix
|
|
559
|
+
description: "Read diagnosis spec and fix v2 code for module $ARGUMENTS with reflection gate"
|
|
560
|
+
steps:
|
|
561
|
+
- id: fix_bugs
|
|
562
|
+
role: surgeon
|
|
563
|
+
max_attempts: 3
|
|
564
|
+
on_fail: handoff
|
|
565
|
+
handoff_to: fix_bugs
|
|
566
|
+
max_handoffs: 3
|
|
567
|
+
on_handoff_exhausted: skip
|
|
568
|
+
blocked_items_path: "docs/behavior/blocked_items.md"
|
|
569
|
+
description: "Fix issues according to diagnosis spec classifications"
|
|
570
|
+
prompt: |
|
|
571
|
+
Read .dna/state/workflow/experience.md first if it exists. It contains previous failed approaches — do NOT repeat them.
|
|
572
|
+
|
|
573
|
+
Read context + diagnosis spec first.
|
|
574
|
+
|
|
575
|
+
Process issues in priority order: INFRA → BUG → TEST_BUG
|
|
576
|
+
(REMOVED skipped — needs user confirmation)
|
|
577
|
+
|
|
578
|
+
For each issue:
|
|
579
|
+
- BUG: read v1 file, edit v2 (use Riverpod per refactoring-workflow-v2.md)
|
|
580
|
+
- INFRA: edit test_helpers only, do NOT touch v2/lib
|
|
581
|
+
- TEST_BUG: re-read v1, edit test to match v1 behavior
|
|
582
|
+
|
|
583
|
+
After each fix: run tests ONCE for that specific file.
|
|
584
|
+
|
|
585
|
+
If experience_chain is non-empty:
|
|
586
|
+
- You have tried approaches that failed
|
|
587
|
+
- Read them, use a DIFFERENT approach
|
|
588
|
+
handoff:
|
|
589
|
+
consumes:
|
|
590
|
+
- type: file
|
|
591
|
+
path: ".omc/specs/diagnosis-$ARGUMENTS.md"
|
|
592
|
+
description: "Diagnosis spec"
|
|
593
|
+
produces:
|
|
594
|
+
- type: git_commit
|
|
595
|
+
description: "Fix commit"
|
|
596
|
+
|
|
597
|
+
- id: progress_check
|
|
598
|
+
role: test_runner
|
|
599
|
+
depends_on: [fix_bugs]
|
|
600
|
+
description: "Verify progress and output report"
|
|
601
|
+
prompt: |
|
|
602
|
+
Run full module test suite for $ARGUMENTS.
|
|
603
|
+
Compare with diagnosis spec baseline.
|
|
604
|
+
|
|
605
|
+
Output report:
|
|
606
|
+
- Fixed: N tests now green
|
|
607
|
+
- New red: M tests that regressed
|
|
608
|
+
- Blocked: K tests skipped (see blocked_items.md)
|
|
609
|
+
- Remaining: R tests still failing
|
|
610
|
+
handoff:
|
|
611
|
+
consumes:
|
|
612
|
+
- type: git_commit
|
|
613
|
+
from: fix_bugs
|
|
614
|
+
description: "Fix commit from surgeon"
|
|
615
|
+
produces:
|
|
616
|
+
- type: summary
|
|
617
|
+
description: "Progress report with test delta"
|
package/package.json
CHANGED
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
# Hooks 基础设施优化 + Harness 加固 — 整合实施方案
|
|
2
|
+
|
|
3
|
+
## Metadata
|
|
4
|
+
- Generated: 2026-04-18
|
|
5
|
+
- Status: APPROVED
|
|
6
|
+
- 预计测试增量: +100
|
|
7
|
+
- 预计工期: 1 周
|
|
8
|
+
- 前置版本: v1.5.12
|
|
9
|
+
|
|
10
|
+
## 背景
|
|
11
|
+
|
|
12
|
+
两个独立方案的合并:
|
|
13
|
+
1. **Hooks 基础设施优化**(上次三方评审通过)— Schema validation, EnforceResult, StateManager, Integration tests
|
|
14
|
+
2. **Harness 加固**(本次 deep interview 产出)— Context gate, Reflection gate, Stop hook 智能编排, Workflow boundary
|
|
15
|
+
|
|
16
|
+
上层(Harness 加固)依赖底层(Hooks 基础设施)。合并后减少重复工作。
|
|
17
|
+
|
|
18
|
+
## 依赖图
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Phase 0 (模板修复) ← 立即可做,无依赖
|
|
22
|
+
|
|
23
|
+
Phase 1A (Schema) ──→ Phase 1B (EnforceResult) ──→ Phase 2B (Gates)
|
|
24
|
+
↓
|
|
25
|
+
Phase 2A (StateManager) ────────────────────────→ Phase 2B (Gates)
|
|
26
|
+
↓
|
|
27
|
+
Phase 3 (Stop hook + Integration)
|
|
28
|
+
↓
|
|
29
|
+
Phase 4 (Dogfood)
|
|
30
|
+
|
|
31
|
+
可并行:1A + 2A
|
|
32
|
+
必须串行:1B 依赖 1A;2B 依赖 1B + 2A;3 依赖 2B;4 依赖 3
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Phase 0: 模板即时修复
|
|
38
|
+
|
|
39
|
+
**目标**: 修复当前模板的 3 个已知缺陷,零风险
|
|
40
|
+
**文件**: `src/templates/flutter-rewrite.dna.yaml`
|
|
41
|
+
**测试**: +0(模板内容变更不影响单测,dna validate 验证)
|
|
42
|
+
**破坏性**: 无
|
|
43
|
+
|
|
44
|
+
### 0.1 Surgeon instructions 加必读清单
|
|
45
|
+
|
|
46
|
+
找到 `roles.surgeon.instructions`,在现有 instructions 列表**最前面**插入一行:
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
instructions:
|
|
50
|
+
- "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."
|
|
51
|
+
# ... 保留所有现有 instructions
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 0.2 Fix workflow experience chain 自读
|
|
55
|
+
|
|
56
|
+
找到 `workflows.fix.steps` 中 `id: fix_bugs` 的 step,在 prompt 最前面加:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
Read .dna/state/workflow/experience.md first if it exists.
|
|
60
|
+
It contains previous failed approaches — do NOT repeat them.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 0.3 Behavior-lock 全量回归
|
|
64
|
+
|
|
65
|
+
找到 `workflows.behavior-lock.steps` 中 `id: write_tests` 的 step prompt,场景 2 (re-run) 部分,替换测试执行逻辑为:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Scenario 2 (re-run/incremental):
|
|
69
|
+
- Read the incremental diff from behavior doc
|
|
70
|
+
- Update existing tests incrementally — do NOT rewrite all tests
|
|
71
|
+
- Run ALL tests (do NOT skip previously green tests)
|
|
72
|
+
- Compare with previous baseline:
|
|
73
|
+
GREEN→RED = REGRESSION (mark HIGH_PRIORITY — v2 broke something)
|
|
74
|
+
RED→GREEN = FIXED
|
|
75
|
+
NEW = new coverage
|
|
76
|
+
REMOVED = coverage shrunk (verify intentional)
|
|
77
|
+
- Update baseline with complete results
|
|
78
|
+
- Git commit includes regression count if any
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 0.4 验证
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
dna validate src/templates/flutter-rewrite.dna.yaml
|
|
85
|
+
npm test
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Phase 1A: Boundary Schema + Contract Tests
|
|
91
|
+
|
|
92
|
+
**目标**: CC ↔ dna-hook 边界的显式 schema 验证,防止 session_id 类 boundary bug
|
|
93
|
+
**测试**: +20
|
|
94
|
+
**破坏性**: 无(纯增量)
|
|
95
|
+
|
|
96
|
+
### 1A.1 新建 src/hooks/schema.ts
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
/**
|
|
100
|
+
* Hook input validation — CC protocol boundary.
|
|
101
|
+
* Zero external dependencies. Hand-written validators.
|
|
102
|
+
* Fail-open: invalid input → log warning + return { valid: false, ... }
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
export interface ValidationResult {
|
|
106
|
+
valid: boolean;
|
|
107
|
+
errors: string[];
|
|
108
|
+
normalized?: Record<string, unknown>; // camelCase 归一化后的输入
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function validateHookInput(event: string, raw: unknown): ValidationResult
|
|
112
|
+
// 对每种 event 类型验证必填字段:
|
|
113
|
+
// PreToolUse: tool_name, tool_input 必须存在
|
|
114
|
+
// PostToolUse: tool_name, tool_output 必须存在
|
|
115
|
+
// SessionStart: session_id (camelCase 归一化)
|
|
116
|
+
// SubagentStop: agent_type
|
|
117
|
+
// UserPromptSubmit: (minimal)
|
|
118
|
+
// Notification: title, message
|
|
119
|
+
// PreCompact: (minimal)
|
|
120
|
+
// Stop: (minimal)
|
|
121
|
+
|
|
122
|
+
export function normalizeSessionId(input: Record<string, unknown>): string | undefined
|
|
123
|
+
// 处理 session_id / sessionId / CLAUDE_SESSION_ID 的归一化
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 1A.2 修复 enforce.ts:38-42 snake_case bug
|
|
127
|
+
|
|
128
|
+
找到 `SessionStartInput` 接口定义(enforce.ts 约 38 行),确认字段用 camelCase(和其他 Input 类型一致)。如果有 `session_id`(snake_case),改为 `sessionId`。
|
|
129
|
+
|
|
130
|
+
### 1A.3 新建 test/fixtures/cc-hook-inputs/
|
|
131
|
+
|
|
132
|
+
从 lwk_flutter_v2-rewrite 的 `.dna/state/trace/trace-2026-04-17.jsonl` 中提取 8 个真实 CC hook input fixture:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
pre-tool-use-edit.json — Edit 工具调用
|
|
136
|
+
pre-tool-use-bash.json — Bash 工具调用
|
|
137
|
+
pre-tool-use-read.json — Read 工具调用
|
|
138
|
+
post-tool-use-bash-test.json — Bash 跑 flutter test 的输出
|
|
139
|
+
session-start.json — SessionStart 事件
|
|
140
|
+
subagent-stop-surgeon.json — surgeon subagent 停止
|
|
141
|
+
subagent-stop-investigator.json — investigator subagent 停止
|
|
142
|
+
stop-event.json — Stop 事件
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 1A.4 新建 test/contract/hook-contract.test.ts
|
|
146
|
+
|
|
147
|
+
对每个 fixture:`fixture → validateHookInput() → enforce*() → 验证输出格式`
|
|
148
|
+
|
|
149
|
+
全链路 contract test:确保真实 CC 输入能正确通过验证 + enforce 链。
|
|
150
|
+
|
|
151
|
+
### 1A.5 验证
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
npm test # 全绿,+20 新测试
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Phase 1B: EnforceResult 类型
|
|
160
|
+
|
|
161
|
+
**目标**: enforce 函数返回结构化结果,消除 cli.ts 的 decision 猜测逻辑
|
|
162
|
+
**测试**: +15(改约 30 处现有断言)
|
|
163
|
+
**破坏性**: 是(限定在此 phase)
|
|
164
|
+
**依赖**: Phase 1A
|
|
165
|
+
|
|
166
|
+
### 1B.1 protocol.ts 新增类型
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
export interface TraceMetadata {
|
|
170
|
+
matched_rule: MatchedRule;
|
|
171
|
+
gene_id?: string;
|
|
172
|
+
step_id?: string;
|
|
173
|
+
role?: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type MatchedRule =
|
|
177
|
+
| "scope"
|
|
178
|
+
| "filter"
|
|
179
|
+
| "gate"
|
|
180
|
+
| "schema"
|
|
181
|
+
| "handoff"
|
|
182
|
+
| "step_rule"
|
|
183
|
+
| "output_schema"
|
|
184
|
+
// Phase 2B 新增(先定义,后实现):
|
|
185
|
+
| "context_gate"
|
|
186
|
+
| "reflection_gate"
|
|
187
|
+
| "workflow_boundary";
|
|
188
|
+
|
|
189
|
+
export interface EnforceResult {
|
|
190
|
+
output: HookOutput;
|
|
191
|
+
trace?: TraceMetadata;
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### 1B.2 更新 enforce.ts 的 8 个函数
|
|
196
|
+
|
|
197
|
+
每个 enforce* 函数的返回类型从 `HookOutput | null` 改为 `EnforceResult | null`。
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// 之前
|
|
201
|
+
export function enforcePreToolUse(...): HookOutput | null {
|
|
202
|
+
return { decision: "block", reason: "..." };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 之后
|
|
206
|
+
export function enforcePreToolUse(...): EnforceResult | null {
|
|
207
|
+
return {
|
|
208
|
+
output: { decision: "block", reason: "..." },
|
|
209
|
+
trace: { matched_rule: "scope", gene_id: "scan_read_only", role: "scanner" }
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**注意**: trace metadata 不进入 HookOutput(CC 协议边界不该混入内部 metadata)。trace 由 cli.ts 读取后写入 trace log,不返回给 CC。
|
|
215
|
+
|
|
216
|
+
### 1B.3 新增 3 个 enforce 纯函数
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
// 已在 v1.5.12 实现 checkReflectionLimit,确认返回 EnforceResult
|
|
220
|
+
export function checkReflectionLimit(
|
|
221
|
+
failCount: number, maxAttempts: number,
|
|
222
|
+
handoffCount: number, maxHandoffs: number
|
|
223
|
+
): EnforceResult
|
|
224
|
+
|
|
225
|
+
// 已在 v1.5.12 实现 checkContextReadiness,确认返回 EnforceResult
|
|
226
|
+
export function checkContextReadiness(
|
|
227
|
+
sessionReads: string[], required: string[]
|
|
228
|
+
): EnforceResult
|
|
229
|
+
|
|
230
|
+
// 新增
|
|
231
|
+
export function checkWorkflowBoundary(
|
|
232
|
+
workflowCompleted: boolean, targetSkill: string
|
|
233
|
+
): EnforceResult
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### 1B.4 更新 cli.ts
|
|
237
|
+
|
|
238
|
+
cli.ts 从 enforce 结果中提取 trace metadata,写入 trace log:
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
const result = enforcePreToolUse(ir, input, state);
|
|
242
|
+
if (result) {
|
|
243
|
+
// 返回 HookOutput 给 CC
|
|
244
|
+
writeOutput(result.output);
|
|
245
|
+
// trace metadata 写 trace log(不返回给 CC)
|
|
246
|
+
if (result.trace) appendTrace({ ...result.trace, timestamp: now() });
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### 1B.5 更新 src/mcp/tools-enforce.ts
|
|
251
|
+
|
|
252
|
+
检查 MCP enforce 工具是否调用 enforce 函数。如果调用了,同步更新返回类型处理。
|
|
253
|
+
|
|
254
|
+
### 1B.6 更新测试
|
|
255
|
+
|
|
256
|
+
约 30 处现有断言需要从 `expect(result).toEqual({ decision: "block", ... })` 改为 `expect(result.output).toEqual({ decision: "block", ... })`。
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
npm test # 全绿,+15 新测试
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Phase 2A: DNAStateManager
|
|
265
|
+
|
|
266
|
+
**目标**: 统一所有 state 操作,支持 session reads + surgeon attempts + experience chain
|
|
267
|
+
**测试**: +20
|
|
268
|
+
**破坏性**: 无(旧函数 @deprecated 保留)
|
|
269
|
+
**可与 Phase 1A 并行**
|
|
270
|
+
|
|
271
|
+
### 2A.1 新建 src/hooks/state-manager.ts
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
export class DNAStateManager {
|
|
275
|
+
constructor(private stateDir: string, private sessionId?: string) {}
|
|
276
|
+
|
|
277
|
+
// ── 现有 state 操作封装 ──
|
|
278
|
+
readWorkflowState(): WorkflowState | null
|
|
279
|
+
writeWorkflowState(state: WorkflowState): void
|
|
280
|
+
readAuditLog(): AuditEntry[]
|
|
281
|
+
appendAudit(entry: AuditEntry): void
|
|
282
|
+
|
|
283
|
+
// ── 新增: Session reads 追踪 ──
|
|
284
|
+
appendSessionRead(filePath: string): void
|
|
285
|
+
getSessionReads(): string[]
|
|
286
|
+
// 路径: .dna/state/workflow/session-reads.json
|
|
287
|
+
|
|
288
|
+
// ── 新增: Surgeon attempts 追踪 ──
|
|
289
|
+
readSurgeonAttempts(): SurgeonAttemptState
|
|
290
|
+
writeSurgeonAttempts(state: SurgeonAttemptState): void
|
|
291
|
+
// 路径: .dna/state/workflow/surgeon-attempts.json
|
|
292
|
+
|
|
293
|
+
// ── 新增: Experience chain ──
|
|
294
|
+
appendExperience(entry: ExperienceEntry): void
|
|
295
|
+
readExperienceChain(): ExperienceEntry[]
|
|
296
|
+
writeExperienceChain(chain: ExperienceEntry[]): void
|
|
297
|
+
// 路径: .dna/state/workflow/experience.md (markdown)
|
|
298
|
+
// + .dna/state/workflow/experience.json (structured)
|
|
299
|
+
|
|
300
|
+
// ── 生命周期 ──
|
|
301
|
+
init(): void // 创建 session 目录结构
|
|
302
|
+
cleanup(): void // 先算 summary(读 trace),再 rotate/clean
|
|
303
|
+
isStale(): boolean // >2h 判定为 stale
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export interface SurgeonAttemptState {
|
|
307
|
+
round: number;
|
|
308
|
+
edit_count: number;
|
|
309
|
+
bash_test_count: number;
|
|
310
|
+
last_green_count: number;
|
|
311
|
+
current_green_count: number;
|
|
312
|
+
fail_count: number;
|
|
313
|
+
handoff_count: number;
|
|
314
|
+
baseline_established: boolean;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export interface ExperienceEntry {
|
|
318
|
+
round: number;
|
|
319
|
+
analysis: string;
|
|
320
|
+
attempt: string;
|
|
321
|
+
result: string;
|
|
322
|
+
lesson: string;
|
|
323
|
+
timestamp: string;
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### 2A.2 保留 state.ts 向后兼容
|
|
328
|
+
|
|
329
|
+
在现有 state.ts 函数上加 `@deprecated` 注释,内部改为调用 StateManager:
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
/** @deprecated Use DNAStateManager instead */
|
|
333
|
+
export function readWorkflowState(stateDir: string): WorkflowState | null {
|
|
334
|
+
return new DNAStateManager(stateDir).readWorkflowState();
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### 2A.3 测试
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
npm test # 全绿,+20 新测试
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Phase 2B: Context Gate + Reflection Gate + Workflow Boundary
|
|
347
|
+
|
|
348
|
+
**目标**: 3 个核心 Harness 加固机制
|
|
349
|
+
**测试**: +15
|
|
350
|
+
**破坏性**: 无
|
|
351
|
+
**依赖**: Phase 1B (EnforceResult) + Phase 2A (StateManager)
|
|
352
|
+
|
|
353
|
+
### 2B.1 Context Gate — src/hooks/cli.ts
|
|
354
|
+
|
|
355
|
+
在 PreToolUse 处理中新增:
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
function handleContextGate(
|
|
359
|
+
input: PreToolUseInput,
|
|
360
|
+
ir: ConstraintIR,
|
|
361
|
+
stateManager: DNAStateManager,
|
|
362
|
+
): EnforceResult | null {
|
|
363
|
+
// 只对 Edit/Write/Bash 触发
|
|
364
|
+
if (!["Edit", "Write", "Bash"].includes(input.tool_name)) return null;
|
|
365
|
+
|
|
366
|
+
// 获取当前角色的必读文件列表
|
|
367
|
+
const role = getCurrentRole(ir, input);
|
|
368
|
+
if (!role) return null;
|
|
369
|
+
|
|
370
|
+
const required = getRoleContextFiles(ir, role);
|
|
371
|
+
if (required.length === 0) return null;
|
|
372
|
+
|
|
373
|
+
// 检查 session reads
|
|
374
|
+
const reads = stateManager.getSessionReads();
|
|
375
|
+
const result = checkContextReadiness(reads, required);
|
|
376
|
+
|
|
377
|
+
if (!result.output.decision || result.output.decision === "allow") return null;
|
|
378
|
+
|
|
379
|
+
// Block — 返回缺失文件清单
|
|
380
|
+
return result;
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**注意:直接 block,不是 warn。** 之前讨论的 Phase 1 warn 已取消,直接上 block。
|
|
385
|
+
|
|
386
|
+
### 2B.2 Reflection Gate — src/hooks/cli.ts
|
|
387
|
+
|
|
388
|
+
在 PostToolUse 处理中新增:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
function handleSurgeonReflection(
|
|
392
|
+
input: PostToolUseInput,
|
|
393
|
+
stepDef: WorkflowStepDef | null,
|
|
394
|
+
stateManager: DNAStateManager,
|
|
395
|
+
): EnforceResult | null {
|
|
396
|
+
if (!stepDef?.max_attempts) return null;
|
|
397
|
+
|
|
398
|
+
const attempts = stateManager.readSurgeonAttempts();
|
|
399
|
+
|
|
400
|
+
// 追踪 Edit/Bash 调用
|
|
401
|
+
if (input.tool_name === "Edit") {
|
|
402
|
+
attempts.edit_count++;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (input.tool_name === "Bash" && looksLikeTestOutput(input.tool_output)) {
|
|
406
|
+
attempts.bash_test_count++;
|
|
407
|
+
const greenCount = parseGreenCount(input.tool_output);
|
|
408
|
+
if (greenCount !== null) {
|
|
409
|
+
attempts.current_green_count = greenCount;
|
|
410
|
+
if (!attempts.baseline_established) {
|
|
411
|
+
attempts.baseline_established = true;
|
|
412
|
+
attempts.last_green_count = greenCount;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 判定进展(只在有 baseline 后)
|
|
418
|
+
if (attempts.baseline_established &&
|
|
419
|
+
attempts.edit_count >= 2 &&
|
|
420
|
+
attempts.bash_test_count >= 1) {
|
|
421
|
+
if (attempts.current_green_count <= attempts.last_green_count) {
|
|
422
|
+
// 无进展
|
|
423
|
+
const result = checkReflectionLimit(
|
|
424
|
+
attempts.fail_count + 1, stepDef.max_attempts,
|
|
425
|
+
attempts.handoff_count, stepDef.max_handoffs ?? 3
|
|
426
|
+
);
|
|
427
|
+
attempts.fail_count++;
|
|
428
|
+
stateManager.writeSurgeonAttempts(attempts);
|
|
429
|
+
|
|
430
|
+
if (result.output.decision !== "allow") {
|
|
431
|
+
return result; // warn / handoff / skip
|
|
432
|
+
}
|
|
433
|
+
} else {
|
|
434
|
+
// 有进展,重置计数
|
|
435
|
+
attempts.last_green_count = attempts.current_green_count;
|
|
436
|
+
attempts.edit_count = 0;
|
|
437
|
+
attempts.bash_test_count = 0;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
stateManager.writeSurgeonAttempts(attempts);
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function parseGreenCount(output: string): number | null {
|
|
446
|
+
// 匹配 flutter test 输出格式
|
|
447
|
+
// "00:05 +23: All tests passed!" → 23
|
|
448
|
+
// "00:05 +23 -5: Some tests failed." → 23
|
|
449
|
+
// "5 passed, 3 failed" → 5
|
|
450
|
+
const patterns = [
|
|
451
|
+
/\+(\d+)(?:\s|:)/, // flutter test format: +N
|
|
452
|
+
/(\d+)\s+passed/i, // generic: N passed
|
|
453
|
+
/(\d+)\s+tests?\s+passed/i, // verbose: N tests passed
|
|
454
|
+
];
|
|
455
|
+
for (const p of patterns) {
|
|
456
|
+
const m = output.match(p);
|
|
457
|
+
if (m) return parseInt(m[1], 10);
|
|
458
|
+
}
|
|
459
|
+
return null; // 解析失败 → fail-open
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### 2B.3 Workflow Boundary — src/hooks/cli.ts
|
|
464
|
+
|
|
465
|
+
在 PreToolUse 处理中新增:
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
function handleWorkflowBoundary(
|
|
469
|
+
input: PreToolUseInput,
|
|
470
|
+
stateManager: DNAStateManager,
|
|
471
|
+
): EnforceResult | null {
|
|
472
|
+
// 只检测 Skill 工具调用
|
|
473
|
+
if (input.tool_name !== "Skill") return null;
|
|
474
|
+
|
|
475
|
+
const wfState = stateManager.readWorkflowState();
|
|
476
|
+
if (!wfState?.completed) return null;
|
|
477
|
+
|
|
478
|
+
return checkWorkflowBoundary(true, input.tool_input?.skill ?? "unknown");
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### 2B.4 PostToolUse 追踪
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
// 在 PostToolUse 处理中,所有 Read 调用记录到 session reads
|
|
486
|
+
if (input.tool_name === "Read" && input.tool_input?.file_path) {
|
|
487
|
+
stateManager.appendSessionRead(input.tool_input.file_path);
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### 2B.5 测试
|
|
492
|
+
|
|
493
|
+
```
|
|
494
|
+
test/hooks-harness.test.ts 新增:
|
|
495
|
+
- context gate: 未读必需文件 + Edit → block
|
|
496
|
+
- context gate: 已读必需文件 + Edit → allow
|
|
497
|
+
- reflection gate: 3 次无进展 → warn(handoff)
|
|
498
|
+
- reflection gate: 有进展 → 不触发
|
|
499
|
+
- reflection gate: parseGreenCount 解析 3 种格式
|
|
500
|
+
- workflow boundary: completed + Skill → block
|
|
501
|
+
- workflow boundary: not completed + Skill → allow
|
|
502
|
+
- session reads 追踪正确
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
```bash
|
|
506
|
+
npm test # 全绿,+15 新测试
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
## Phase 3: Stop Hook 智能编排 + Integration Tests
|
|
512
|
+
|
|
513
|
+
**目标**: Stop hook 从"继续工作"升级为"输出下一步 Agent 调用";全链路集成测试
|
|
514
|
+
**测试**: +25
|
|
515
|
+
**破坏性**: 无
|
|
516
|
+
**依赖**: Phase 2B
|
|
517
|
+
|
|
518
|
+
### 3.1 Stop Hook 智能编排 — src/hooks/cli.ts
|
|
519
|
+
|
|
520
|
+
在 enforceStop 中,读 workflow state 后输出明确的下一步指令:
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
function buildWorkflowGuidance(
|
|
524
|
+
ir: ConstraintIR,
|
|
525
|
+
stateManager: DNAStateManager,
|
|
526
|
+
): string | null {
|
|
527
|
+
const wfState = stateManager.readWorkflowState();
|
|
528
|
+
if (!wfState) return null;
|
|
529
|
+
|
|
530
|
+
const attempts = stateManager.readSurgeonAttempts();
|
|
531
|
+
const stepDef = getStepDef(ir, wfState.current_step);
|
|
532
|
+
|
|
533
|
+
// 情况 1: surgeon 无进展,需要重新分析
|
|
534
|
+
if (wfState.current_step === "fix_bugs" &&
|
|
535
|
+
stepDef?.max_attempts &&
|
|
536
|
+
attempts.fail_count >= stepDef.max_attempts) {
|
|
537
|
+
const experience = stateManager.readExperienceChain();
|
|
538
|
+
return `[DNA WORKFLOW] Surgeon 已无进展 ${attempts.fail_count} 次。
|
|
539
|
+
下一步: 重新分析。执行:
|
|
540
|
+
Agent(subagent_type="dna-frw-analyzer", prompt="重新分析 module,
|
|
541
|
+
读 .dna/state/workflow/experience.md 了解 ${experience.length} 次历史失败")`;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// 情况 2: workflow 完成
|
|
545
|
+
if (wfState.completed) {
|
|
546
|
+
return `[DNA WORKFLOW] Workflow "${wfState.workflow}" 完成。
|
|
547
|
+
输出最终报告并停止。不要启动其他 workflow。`;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// 情况 3: 正常继续
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
在 enforceStop 的 reason 中追加 workflow guidance(如果有)。
|
|
556
|
+
|
|
557
|
+
### 3.2 Integration Tests — test/integration/
|
|
558
|
+
|
|
559
|
+
新建 test/integration/hook-lifecycle.test.ts:
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
// spawn 真实 dna-hook 进程,通过 stdin/stdout 模拟完整 workflow
|
|
563
|
+
|
|
564
|
+
describe("hook lifecycle integration", () => {
|
|
565
|
+
it("context gate blocks Edit before reading context files", async () => {
|
|
566
|
+
// stdin: PreToolUse Edit event (surgeon, no prior reads)
|
|
567
|
+
// expected stdout: { decision: "block", reason: "...Missing: CLAUDE.md..." }
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it("context gate allows Edit after reading required files", async () => {
|
|
571
|
+
// stdin: PostToolUse Read CLAUDE.md → PostToolUse Read workflow.md → PreToolUse Edit
|
|
572
|
+
// expected stdout: (no block)
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it("reflection gate triggers after 3 no-progress cycles", async () => {
|
|
576
|
+
// stdin: sequence of Edit + Bash(test, same green count) × 3
|
|
577
|
+
// expected: warn with handoff message
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it("stop hook injects workflow guidance", async () => {
|
|
581
|
+
// setup workflow state with fail_count >= max
|
|
582
|
+
// stdin: Stop event
|
|
583
|
+
// expected: reason contains "[DNA WORKFLOW]" + Agent() call
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### 3.3 Property Tests — test/property/
|
|
589
|
+
|
|
590
|
+
新建 test/property/enforce-invariants.test.ts(使用 fast-check):
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
import fc from "fast-check";
|
|
594
|
+
|
|
595
|
+
// 不变量 1: fail-open — 任何无效输入都不 throw
|
|
596
|
+
// 不变量 2: scope 一致性 — deny 的 tool 永远 block
|
|
597
|
+
// 不变量 3: context gate 幂等 — 同一输入多次调用结果相同
|
|
598
|
+
// 不变量 4: reflection limit 单调 — fail_count 只增不减(除非 reset)
|
|
599
|
+
// 不变量 5: parseGreenCount null 安全 — 任意字符串不 throw
|
|
600
|
+
// 不变量 6: checkReflectionLimit 边界 — maxAttempts=0 时总是 handoff
|
|
601
|
+
// 不变量 7: workflow boundary 完成判定 — completed=false 时总是 allow
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
### 3.4 验证
|
|
605
|
+
|
|
606
|
+
```bash
|
|
607
|
+
npm test # 全绿,+25 新测试
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## Phase 4: Dogfood + Cleanup
|
|
613
|
+
|
|
614
|
+
**目标**: 真实项目验证 + 清理
|
|
615
|
+
**测试**: +5
|
|
616
|
+
**依赖**: Phase 3
|
|
617
|
+
|
|
618
|
+
### 4.1 intentdna 项目验证
|
|
619
|
+
|
|
620
|
+
```bash
|
|
621
|
+
dna verify --health # 7 项检查全通过
|
|
622
|
+
dna sync # 无报错
|
|
623
|
+
npm test # 全绿
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### 4.2 lwk_flutter_v2-rewrite 验证
|
|
627
|
+
|
|
628
|
+
```bash
|
|
629
|
+
cd /Users/samuel/lawark/lwk_flutter_v2-rewrite
|
|
630
|
+
dna sync # 新 agent/skill 生成
|
|
631
|
+
/dna-frw-diagnosis home # 验证: analyzer 不跑 Bash, 输出 diagnosis spec
|
|
632
|
+
# 验证: context gate block 生效
|
|
633
|
+
# 验证: analysis_reviewer 审查
|
|
634
|
+
/dna-frw-fix home # 验证: surgeon 先读 context 再 Edit
|
|
635
|
+
# 验证: reflection gate 触发
|
|
636
|
+
# 验证: workflow boundary 阻止跨 workflow
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
### 4.3 清理
|
|
640
|
+
|
|
641
|
+
如果 StateManager 稳定,考虑移除 state.ts 中 @deprecated 函数。
|
|
642
|
+
|
|
643
|
+
### 4.4 发版
|
|
644
|
+
|
|
645
|
+
```bash
|
|
646
|
+
/dna-release patch "hooks infra + harness hardening — schema validation, EnforceResult, StateManager, context/reflection/boundary gates"
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
## 验收清单
|
|
652
|
+
|
|
653
|
+
### Phase 0
|
|
654
|
+
- [ ] surgeon instructions 第一行是 REQUIRED FIRST
|
|
655
|
+
- [ ] fix_bugs prompt 包含读 experience.md
|
|
656
|
+
- [ ] behavior-lock 场景 2 全量回归
|
|
657
|
+
|
|
658
|
+
### Phase 1A
|
|
659
|
+
- [ ] schema.ts validateHookInput() 实现
|
|
660
|
+
- [ ] 8 个真实 CC fixture 文件
|
|
661
|
+
- [ ] contract tests 全链路通过
|
|
662
|
+
- [ ] SessionStartInput snake_case bug 已修
|
|
663
|
+
|
|
664
|
+
### Phase 1B
|
|
665
|
+
- [ ] EnforceResult + TraceMetadata 类型定义
|
|
666
|
+
- [ ] matched_rule 包含 context_gate / reflection_gate / workflow_boundary
|
|
667
|
+
- [ ] 8 个 enforce 函数返回 EnforceResult
|
|
668
|
+
- [ ] cli.ts 不再猜 decision
|
|
669
|
+
- [ ] trace metadata 不进入 HookOutput(CC 协议边界)
|
|
670
|
+
- [ ] mcp/tools-enforce.ts 同步更新
|
|
671
|
+
|
|
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 个不变量
|
|
691
|
+
|
|
692
|
+
### Phase 4
|
|
693
|
+
- [ ] dna verify --health 通过
|
|
694
|
+
- [ ] lwk_flutter diagnosis → fix 流程验证
|
|
695
|
+
- [ ] 发版
|