intentdna 1.5.3 → 1.5.5
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/cli/commands/sync.d.ts +23 -0
- package/dist/cli/commands/sync.js +49 -5
- package/dist/cli/commands/verify.d.ts +11 -0
- package/dist/cli/commands/verify.js +190 -0
- package/dist/cli/index.js +3 -0
- package/dist/hooks/cli.js +15 -2
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/tools-enforce.d.ts +27 -0
- package/dist/mcp/tools-enforce.js +178 -0
- package/dist/schema/yaml-parser.d.ts +2 -2
- package/dist/schema/yaml-parser.js +30 -2
- package/dist/templates/flutter-rewrite.dna.yaml +144 -15
- package/package.json +1 -1
- package/spec/flutter-rewrite-template-optimization.md +270 -0
- package/spec/foundation-hardening.md +3 -3
|
@@ -54,4 +54,27 @@ export { getPackageVersion } from "../util/version.js";
|
|
|
54
54
|
* Throws if two configs declare the same namespace.
|
|
55
55
|
*/
|
|
56
56
|
export declare function checkNamespaceCollisions(configPaths: string[]): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Compare two semver strings. Returns:
|
|
59
|
+
* 1 if a > b
|
|
60
|
+
* 0 if a == b
|
|
61
|
+
* -1 if a < b
|
|
62
|
+
* Handles major.minor.patch format. Non-numeric parts are ignored.
|
|
63
|
+
*/
|
|
64
|
+
export declare function compareSemver(a: string, b: string): -1 | 0 | 1;
|
|
65
|
+
/** Info about a single template that has an available upgrade. */
|
|
66
|
+
export interface TemplateUpgradeInfo {
|
|
67
|
+
configPath: string;
|
|
68
|
+
templateName: string;
|
|
69
|
+
currentVersion: string;
|
|
70
|
+
availableVersion: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Check template versions against current package version.
|
|
74
|
+
* Returns upgrade info for outdated configs and prints human-readable suggestions.
|
|
75
|
+
*
|
|
76
|
+
* U1: Semver-aware comparison — only suggests upgrades when package is strictly newer.
|
|
77
|
+
* Handles missing _template_version (old configs created before versioning).
|
|
78
|
+
*/
|
|
79
|
+
export declare function checkTemplateVersions(configPaths: string[]): Promise<TemplateUpgradeInfo[]>;
|
|
57
80
|
export declare function runSync(opts: SyncOptions): Promise<number>;
|
|
@@ -253,28 +253,72 @@ export async function checkNamespaceCollisions(configPaths) {
|
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Compare two semver strings. Returns:
|
|
258
|
+
* 1 if a > b
|
|
259
|
+
* 0 if a == b
|
|
260
|
+
* -1 if a < b
|
|
261
|
+
* Handles major.minor.patch format. Non-numeric parts are ignored.
|
|
262
|
+
*/
|
|
263
|
+
export function compareSemver(a, b) {
|
|
264
|
+
const parse = (v) => v.split(".").map(s => parseInt(s, 10) || 0);
|
|
265
|
+
const pa = parse(a);
|
|
266
|
+
const pb = parse(b);
|
|
267
|
+
const len = Math.max(pa.length, pb.length);
|
|
268
|
+
for (let i = 0; i < len; i++) {
|
|
269
|
+
const va = pa[i] ?? 0;
|
|
270
|
+
const vb = pb[i] ?? 0;
|
|
271
|
+
if (va > vb)
|
|
272
|
+
return 1;
|
|
273
|
+
if (va < vb)
|
|
274
|
+
return -1;
|
|
275
|
+
}
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
256
278
|
/**
|
|
257
279
|
* Check template versions against current package version.
|
|
258
|
-
*
|
|
280
|
+
* Returns upgrade info for outdated configs and prints human-readable suggestions.
|
|
281
|
+
*
|
|
282
|
+
* U1: Semver-aware comparison — only suggests upgrades when package is strictly newer.
|
|
283
|
+
* Handles missing _template_version (old configs created before versioning).
|
|
259
284
|
*/
|
|
260
|
-
async function checkTemplateVersions(configPaths) {
|
|
285
|
+
export async function checkTemplateVersions(configPaths) {
|
|
261
286
|
const pkgVersion = await getPackageVersion();
|
|
262
287
|
if (!pkgVersion)
|
|
263
|
-
return;
|
|
288
|
+
return [];
|
|
289
|
+
const upgrades = [];
|
|
264
290
|
for (const configPath of configPaths) {
|
|
265
291
|
try {
|
|
266
292
|
const content = await readFile(configPath, "utf-8");
|
|
267
293
|
const data = parseYAML(content);
|
|
268
294
|
const tmplVersion = typeof data._template_version === "string" ? data._template_version : undefined;
|
|
269
295
|
const tmplName = typeof data._source_template === "string" ? data._source_template : undefined;
|
|
270
|
-
if (
|
|
271
|
-
|
|
296
|
+
if (tmplName && !tmplVersion) {
|
|
297
|
+
// Old config without version tracking — suggest upgrade
|
|
298
|
+
upgrades.push({
|
|
299
|
+
configPath,
|
|
300
|
+
templateName: tmplName,
|
|
301
|
+
currentVersion: "unknown",
|
|
302
|
+
availableVersion: pkgVersion,
|
|
303
|
+
});
|
|
304
|
+
process.stderr.write(`Upgrade available: "${tmplName}" has no version tag. Run: dna init --upgrade ${tmplName}\n`);
|
|
305
|
+
}
|
|
306
|
+
else if (tmplVersion && tmplName && compareSemver(pkgVersion, tmplVersion) > 0) {
|
|
307
|
+
// Package is strictly newer than template — suggest upgrade
|
|
308
|
+
upgrades.push({
|
|
309
|
+
configPath,
|
|
310
|
+
templateName: tmplName,
|
|
311
|
+
currentVersion: tmplVersion,
|
|
312
|
+
availableVersion: pkgVersion,
|
|
313
|
+
});
|
|
314
|
+
process.stderr.write(`Upgrade available: "${tmplName}" ${tmplVersion} → ${pkgVersion}. Run: dna init --upgrade ${tmplName}\n`);
|
|
272
315
|
}
|
|
273
316
|
}
|
|
274
317
|
catch {
|
|
275
318
|
// Non-critical — skip version check for unparseable files
|
|
276
319
|
}
|
|
277
320
|
}
|
|
321
|
+
return upgrades;
|
|
278
322
|
}
|
|
279
323
|
function resolveSpeciesPath(ref) {
|
|
280
324
|
if (!ref.startsWith("species:"))
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
export interface VerifyOptions {
|
|
13
13
|
lockFile: string;
|
|
14
14
|
stats?: boolean;
|
|
15
|
+
health?: boolean;
|
|
15
16
|
}
|
|
16
17
|
export interface LockFileEntry {
|
|
17
18
|
sha256: string;
|
|
@@ -31,3 +32,13 @@ export declare function sha256(content: string): string;
|
|
|
31
32
|
export declare function parseLockFile(raw: string): LockFile;
|
|
32
33
|
export declare function verifyLock(lock: LockFile): Promise<VerifyResult[]>;
|
|
33
34
|
export declare function runVerify(opts: VerifyOptions): Promise<number>;
|
|
35
|
+
export interface HealthCheckResult {
|
|
36
|
+
name: string;
|
|
37
|
+
status: "pass" | "warn" | "fail";
|
|
38
|
+
detail: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* U3: Comprehensive health report for DNA installation.
|
|
42
|
+
* Checks IR, configs, templates, hooks, and trace system.
|
|
43
|
+
*/
|
|
44
|
+
export declare function runHealth(): Promise<number>;
|
|
@@ -54,6 +54,10 @@ export async function verifyLock(lock) {
|
|
|
54
54
|
}
|
|
55
55
|
// ── CLI entry ──────────────────────────────────────────────
|
|
56
56
|
export async function runVerify(opts) {
|
|
57
|
+
// Handle --health mode
|
|
58
|
+
if (opts.health) {
|
|
59
|
+
return runHealth();
|
|
60
|
+
}
|
|
57
61
|
// Handle --stats mode
|
|
58
62
|
if (opts.stats) {
|
|
59
63
|
return runStats();
|
|
@@ -214,3 +218,189 @@ function truncate(s, max) {
|
|
|
214
218
|
return s;
|
|
215
219
|
return s.slice(0, max - 3) + "...";
|
|
216
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* U3: Comprehensive health report for DNA installation.
|
|
223
|
+
* Checks IR, configs, templates, hooks, and trace system.
|
|
224
|
+
*/
|
|
225
|
+
export async function runHealth() {
|
|
226
|
+
const cwd = process.cwd();
|
|
227
|
+
const checks = [];
|
|
228
|
+
const { resolve } = await import("node:path");
|
|
229
|
+
const { stat, readFile: rf, readdir } = await import("node:fs/promises");
|
|
230
|
+
// 1. Check compiled IR
|
|
231
|
+
const irPath = resolve(cwd, ".dna", "compiled", "ir.json");
|
|
232
|
+
try {
|
|
233
|
+
const raw = await rf(irPath, "utf-8");
|
|
234
|
+
const data = JSON.parse(raw);
|
|
235
|
+
if (data.ir_version && data.ir) {
|
|
236
|
+
const age = Date.now() - new Date(data.compiled_at ?? data.ir.compiled_at).getTime();
|
|
237
|
+
const ageHours = Math.round(age / 3600000);
|
|
238
|
+
checks.push({
|
|
239
|
+
name: "Compiled IR",
|
|
240
|
+
status: "pass",
|
|
241
|
+
detail: `v${data.ir_version}, compiled ${ageHours}h ago, ${data.ir.source_dna_ids?.length ?? 0} template(s)`,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
else if (data.compiled_at) {
|
|
245
|
+
checks.push({ name: "Compiled IR", status: "warn", detail: "Legacy format — run `dna sync` to upgrade" });
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
checks.push({ name: "Compiled IR", status: "fail", detail: "Unrecognized format" });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
checks.push({ name: "Compiled IR", status: "fail", detail: "Not found — run `dna sync`" });
|
|
253
|
+
}
|
|
254
|
+
// 2. Check DNA config files
|
|
255
|
+
const { autoDetectConfigs } = await import("./sync.js");
|
|
256
|
+
const configs = await autoDetectConfigs();
|
|
257
|
+
if (configs.length > 0) {
|
|
258
|
+
checks.push({ name: "DNA configs", status: "pass", detail: `${configs.length} config(s) found` });
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
checks.push({ name: "DNA configs", status: "fail", detail: "No config files found — run `dna init --template <name>`" });
|
|
262
|
+
}
|
|
263
|
+
// 3. Check config validity
|
|
264
|
+
if (configs.length > 0) {
|
|
265
|
+
const { loadDNA } = await import("../../compiler/index.js");
|
|
266
|
+
let valid = 0;
|
|
267
|
+
let invalid = 0;
|
|
268
|
+
const errors = [];
|
|
269
|
+
for (const configPath of configs) {
|
|
270
|
+
try {
|
|
271
|
+
await loadDNA(configPath);
|
|
272
|
+
valid++;
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
invalid++;
|
|
276
|
+
const { basename } = await import("node:path");
|
|
277
|
+
errors.push(`${basename(configPath)}: ${err instanceof Error ? err.message : String(err)}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (invalid === 0) {
|
|
281
|
+
checks.push({ name: "Config validity", status: "pass", detail: `${valid} config(s) valid` });
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
checks.push({ name: "Config validity", status: "fail", detail: errors.join("; ") });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// 4. Check template versions
|
|
288
|
+
if (configs.length > 0) {
|
|
289
|
+
const { checkTemplateVersions: checkVersions } = await import("./sync.js");
|
|
290
|
+
// Suppress stderr output from checkTemplateVersions — we'll report ourselves
|
|
291
|
+
const origWrite = process.stderr.write;
|
|
292
|
+
process.stderr.write = (() => true);
|
|
293
|
+
try {
|
|
294
|
+
const upgrades = await checkVersions(configs);
|
|
295
|
+
if (upgrades.length === 0) {
|
|
296
|
+
checks.push({ name: "Template versions", status: "pass", detail: "All up to date" });
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
const names = upgrades.map(u => `${u.templateName} (${u.currentVersion} → ${u.availableVersion})`);
|
|
300
|
+
checks.push({ name: "Template versions", status: "warn", detail: `Upgrade available: ${names.join(", ")}` });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
checks.push({ name: "Template versions", status: "warn", detail: "Could not check versions" });
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
process.stderr.write = origWrite;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// 5. Check .claude settings or plugin registration
|
|
311
|
+
const settingsPath = resolve(cwd, ".claude", "settings.json");
|
|
312
|
+
try {
|
|
313
|
+
const raw = await rf(settingsPath, "utf-8");
|
|
314
|
+
const settings = JSON.parse(raw);
|
|
315
|
+
const hooks = settings.hooks;
|
|
316
|
+
const hasHooks = hooks && typeof hooks === "object" && Object.keys(hooks).length > 0;
|
|
317
|
+
if (hasHooks) {
|
|
318
|
+
const eventCount = Object.keys(hooks).length;
|
|
319
|
+
checks.push({ name: "Hook registration", status: "pass", detail: `${eventCount} event(s) in settings.json` });
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
checks.push({ name: "Hook registration", status: "warn", detail: "No hooks in settings.json — using plugin mode?" });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// Check for plugin registration instead
|
|
327
|
+
const mcpPath = resolve(cwd, ".claude", ".mcp.json");
|
|
328
|
+
try {
|
|
329
|
+
const raw = await rf(mcpPath, "utf-8");
|
|
330
|
+
const mcp = JSON.parse(raw);
|
|
331
|
+
if (mcp.mcpServers?.intentdna) {
|
|
332
|
+
checks.push({ name: "MCP server", status: "pass", detail: "dna-mcp registered in .mcp.json" });
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
checks.push({ name: "Hook registration", status: "warn", detail: "No hooks or MCP server found" });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
checks.push({ name: "Hook registration", status: "warn", detail: "No settings.json or .mcp.json — run `dna setup` or `dna sync`" });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// 6. Check trace system
|
|
343
|
+
const traceDir = resolve(cwd, ".dna", "state", "trace");
|
|
344
|
+
try {
|
|
345
|
+
const files = await readdir(traceDir);
|
|
346
|
+
const traceFiles = files.filter(f => f.startsWith("trace-") && f.endsWith(".jsonl"));
|
|
347
|
+
if (traceFiles.length > 0) {
|
|
348
|
+
// Read latest trace file size
|
|
349
|
+
const latest = traceFiles.sort().pop();
|
|
350
|
+
const latestStat = await stat(resolve(traceDir, latest));
|
|
351
|
+
const sizeKB = Math.round(latestStat.size / 1024);
|
|
352
|
+
checks.push({ name: "Trace system", status: "pass", detail: `${traceFiles.length} file(s), latest ${sizeKB}KB` });
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
checks.push({ name: "Trace system", status: "warn", detail: "No trace files — hooks not fired yet" });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
checks.push({ name: "Trace system", status: "warn", detail: "No trace directory — will be created on first hook call" });
|
|
360
|
+
}
|
|
361
|
+
// 7. Check lock file drift
|
|
362
|
+
const lockPath = resolve(cwd, ".dna", "lock");
|
|
363
|
+
try {
|
|
364
|
+
const raw = await rf(lockPath, "utf-8");
|
|
365
|
+
const lock = parseLockFile(raw);
|
|
366
|
+
const results = await verifyLock(lock);
|
|
367
|
+
const drifted = results.filter(r => r.status !== "match");
|
|
368
|
+
if (drifted.length === 0) {
|
|
369
|
+
checks.push({ name: "Output drift", status: "pass", detail: `${results.length} file(s) match lock` });
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
checks.push({ name: "Output drift", status: "warn", detail: `${drifted.length}/${results.length} file(s) drifted — run \`dna sync\`` });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
checks.push({ name: "Output drift", status: "warn", detail: "No lock file — run `dna sync` to generate" });
|
|
377
|
+
}
|
|
378
|
+
// ── Format output ──────────────────────────────────────
|
|
379
|
+
const ICONS = { pass: "ok", warn: "!!", fail: "XX" };
|
|
380
|
+
process.stderr.write("\nIntent DNA Health Report\n");
|
|
381
|
+
process.stderr.write("========================\n\n");
|
|
382
|
+
let hasFailure = false;
|
|
383
|
+
let hasWarning = false;
|
|
384
|
+
for (const check of checks) {
|
|
385
|
+
const icon = ICONS[check.status];
|
|
386
|
+
const label = check.name.padEnd(20);
|
|
387
|
+
process.stderr.write(` [${icon}] ${label} ${check.detail}\n`);
|
|
388
|
+
if (check.status === "fail")
|
|
389
|
+
hasFailure = true;
|
|
390
|
+
if (check.status === "warn")
|
|
391
|
+
hasWarning = true;
|
|
392
|
+
}
|
|
393
|
+
process.stderr.write("\n");
|
|
394
|
+
if (hasFailure) {
|
|
395
|
+
process.stderr.write("Status: issues found — see [XX] items above\n");
|
|
396
|
+
return 1;
|
|
397
|
+
}
|
|
398
|
+
else if (hasWarning) {
|
|
399
|
+
process.stderr.write("Status: healthy with warnings — see [!!] items above\n");
|
|
400
|
+
return 0;
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
process.stderr.write("Status: all checks passed\n");
|
|
404
|
+
return 0;
|
|
405
|
+
}
|
|
406
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -58,6 +58,7 @@ Examples:
|
|
|
58
58
|
dna verify Verify synced files against .dna/lock
|
|
59
59
|
dna verify --lock /path/to/.dna/lock
|
|
60
60
|
dna verify --stats Show hook call statistics (last 24h)
|
|
61
|
+
dna verify --health Comprehensive health check of DNA installation
|
|
61
62
|
dna import . Import existing harness configs into DNA format
|
|
62
63
|
dna run --dna my.dna.json --workflow dev-pipeline --task P5.8
|
|
63
64
|
dna run --dna my.dna.json --workflow dev-pipeline --task P5.8 --dry-run
|
|
@@ -126,6 +127,7 @@ async function main() {
|
|
|
126
127
|
options: {
|
|
127
128
|
lock: { type: "string", default: ".dna/lock" },
|
|
128
129
|
stats: { type: "boolean", default: false },
|
|
130
|
+
health: { type: "boolean", default: false },
|
|
129
131
|
},
|
|
130
132
|
allowPositionals: true,
|
|
131
133
|
strict: false,
|
|
@@ -134,6 +136,7 @@ async function main() {
|
|
|
134
136
|
const code = await runVerify({
|
|
135
137
|
lockFile: verifyValues.lock,
|
|
136
138
|
stats: verifyValues.stats,
|
|
139
|
+
health: verifyValues.health,
|
|
137
140
|
});
|
|
138
141
|
process.exit(code);
|
|
139
142
|
break;
|
package/dist/hooks/cli.js
CHANGED
|
@@ -278,7 +278,8 @@ async function loadIR(irPath) {
|
|
|
278
278
|
// Support both raw IR and wrapped CompiledIRFile format
|
|
279
279
|
if (data.ir_version && data.ir) {
|
|
280
280
|
if (data.ir_version !== EXPECTED_IR_VERSION) {
|
|
281
|
-
process.stderr.write(
|
|
281
|
+
process.stderr.write(`\n Intent DNA: IR version mismatch (v${data.ir_version} on disk, v${EXPECTED_IR_VERSION} expected)\n` +
|
|
282
|
+
` Fix: run \`dna sync\` to recompile\n\n`);
|
|
282
283
|
}
|
|
283
284
|
return data.ir;
|
|
284
285
|
}
|
|
@@ -286,9 +287,21 @@ async function loadIR(irPath) {
|
|
|
286
287
|
if (data.compiled_at && data.source_dna_ids) {
|
|
287
288
|
return data;
|
|
288
289
|
}
|
|
290
|
+
process.stderr.write(`\n Intent DNA: unrecognized IR format at ${irPath}\n` +
|
|
291
|
+
` Fix: run \`dna sync\` to regenerate\n\n`);
|
|
289
292
|
return null;
|
|
290
293
|
}
|
|
291
|
-
catch {
|
|
294
|
+
catch (err) {
|
|
295
|
+
// U3: Human-readable error instead of silent failure
|
|
296
|
+
const isNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
297
|
+
if (isNotFound) {
|
|
298
|
+
process.stderr.write(`\n Intent DNA: no compiled IR found at ${irPath}\n` +
|
|
299
|
+
` Fix: run \`dna sync\` to compile your DNA config\n\n`);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
process.stderr.write(`\n Intent DNA: failed to load IR — ${err instanceof Error ? err.message : String(err)}\n` +
|
|
303
|
+
` Fix: run \`dna sync\` to recompile\n\n`);
|
|
304
|
+
}
|
|
292
305
|
return null;
|
|
293
306
|
}
|
|
294
307
|
}
|
package/dist/mcp/index.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { createMCPServer } from "./server.js";
|
|
14
14
|
import { createStateTools } from "./tools-state.js";
|
|
15
15
|
import { createCompileTools } from "./tools-compile.js";
|
|
16
|
+
import { createEnforceTools } from "./tools-enforce.js";
|
|
16
17
|
// Parse args
|
|
17
18
|
const args = process.argv.slice(2);
|
|
18
19
|
let projectDir = process.cwd();
|
|
@@ -34,5 +35,6 @@ catch { /* use default */ }
|
|
|
34
35
|
const tools = [
|
|
35
36
|
...createStateTools(projectDir),
|
|
36
37
|
...createCompileTools(projectDir),
|
|
38
|
+
...createEnforceTools(projectDir),
|
|
37
39
|
];
|
|
38
40
|
createMCPServer({ name: "intentdna", version }, tools);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent DNA — MCP Enforce Tools (U2)
|
|
3
|
+
*
|
|
4
|
+
* Provides enforcement via MCP server with in-memory IR cache.
|
|
5
|
+
* Zero cold-start: IR is loaded once and cached with mtime-based invalidation.
|
|
6
|
+
*
|
|
7
|
+
* Tools:
|
|
8
|
+
* - dna_enforce: Run enforcement for a hook event (PreToolUse, PostToolUse, etc.)
|
|
9
|
+
*/
|
|
10
|
+
import type { ConstraintIR, RoleDef } from "../schema/types.js";
|
|
11
|
+
import type { ToolDef } from "./server.js";
|
|
12
|
+
/** In-memory IR cache with mtime-based invalidation. */
|
|
13
|
+
export declare class IRCache {
|
|
14
|
+
private ir;
|
|
15
|
+
private roles;
|
|
16
|
+
private lastMtime;
|
|
17
|
+
private irPath;
|
|
18
|
+
constructor(projectDir: string);
|
|
19
|
+
/** Get cached IR, reloading from disk only if file changed. */
|
|
20
|
+
get(): Promise<{
|
|
21
|
+
ir: ConstraintIR;
|
|
22
|
+
roles?: Record<string, RoleDef>;
|
|
23
|
+
} | null>;
|
|
24
|
+
/** Invalidate the cache (e.g., after dna sync). */
|
|
25
|
+
invalidate(): void;
|
|
26
|
+
}
|
|
27
|
+
export declare function createEnforceTools(projectDir: string): ToolDef[];
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent DNA — MCP Enforce Tools (U2)
|
|
3
|
+
*
|
|
4
|
+
* Provides enforcement via MCP server with in-memory IR cache.
|
|
5
|
+
* Zero cold-start: IR is loaded once and cached with mtime-based invalidation.
|
|
6
|
+
*
|
|
7
|
+
* Tools:
|
|
8
|
+
* - dna_enforce: Run enforcement for a hook event (PreToolUse, PostToolUse, etc.)
|
|
9
|
+
*/
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { stat, readFile } from "node:fs/promises";
|
|
12
|
+
import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, } from "../hooks/enforce.js";
|
|
13
|
+
import { textResult, errorResult } from "./server.js";
|
|
14
|
+
// ── IR Cache ────────────────────────────────────────────────
|
|
15
|
+
/** In-memory IR cache with mtime-based invalidation. */
|
|
16
|
+
export class IRCache {
|
|
17
|
+
ir = null;
|
|
18
|
+
roles = undefined;
|
|
19
|
+
lastMtime = 0;
|
|
20
|
+
irPath;
|
|
21
|
+
constructor(projectDir) {
|
|
22
|
+
this.irPath = resolve(projectDir, ".dna", "compiled", "ir.json");
|
|
23
|
+
}
|
|
24
|
+
/** Get cached IR, reloading from disk only if file changed. */
|
|
25
|
+
async get() {
|
|
26
|
+
try {
|
|
27
|
+
const fileStat = await stat(this.irPath);
|
|
28
|
+
const mtime = fileStat.mtimeMs;
|
|
29
|
+
const cachedIR = this.ir;
|
|
30
|
+
if (cachedIR && mtime === this.lastMtime) {
|
|
31
|
+
return { ir: cachedIR, roles: this.roles };
|
|
32
|
+
}
|
|
33
|
+
// File changed or first load — read from disk
|
|
34
|
+
const raw = await readFile(this.irPath, "utf-8");
|
|
35
|
+
const data = JSON.parse(raw);
|
|
36
|
+
if (data.ir_version && data.ir) {
|
|
37
|
+
const ir = data.ir;
|
|
38
|
+
this.ir = ir;
|
|
39
|
+
this.roles = data.roles;
|
|
40
|
+
this.lastMtime = mtime;
|
|
41
|
+
return { ir, roles: this.roles };
|
|
42
|
+
}
|
|
43
|
+
// Direct ConstraintIR format (backward compat)
|
|
44
|
+
if (data.compiled_at && data.source_dna_ids) {
|
|
45
|
+
const ir = data;
|
|
46
|
+
this.ir = ir;
|
|
47
|
+
this.roles = undefined;
|
|
48
|
+
this.lastMtime = mtime;
|
|
49
|
+
return { ir };
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Invalidate the cache (e.g., after dna sync). */
|
|
58
|
+
invalidate() {
|
|
59
|
+
this.ir = null;
|
|
60
|
+
this.roles = undefined;
|
|
61
|
+
this.lastMtime = 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const VALID_EVENTS = new Set([
|
|
65
|
+
"PreToolUse", "PostToolUse", "UserPromptSubmit",
|
|
66
|
+
"SubagentStop", "PreCompact", "Notification", "SessionStart",
|
|
67
|
+
]);
|
|
68
|
+
function dispatchEnforce(event, ir, input, roles) {
|
|
69
|
+
switch (event) {
|
|
70
|
+
case "PreToolUse":
|
|
71
|
+
return enforcePreToolUse(ir, {
|
|
72
|
+
tool_name: String(input.tool_name ?? ""),
|
|
73
|
+
tool_input: (input.tool_input ?? {}),
|
|
74
|
+
agent_type: typeof input.agent_type === "string" ? input.agent_type : undefined,
|
|
75
|
+
cwd: typeof input.cwd === "string" ? input.cwd : undefined,
|
|
76
|
+
}, undefined, roles);
|
|
77
|
+
case "PostToolUse":
|
|
78
|
+
return enforcePostToolUse(ir, {
|
|
79
|
+
tool_name: String(input.tool_name ?? ""),
|
|
80
|
+
tool_input: (input.tool_input ?? {}),
|
|
81
|
+
tool_output: typeof input.tool_output === "string" ? input.tool_output : undefined,
|
|
82
|
+
agent_type: typeof input.agent_type === "string" ? input.agent_type : undefined,
|
|
83
|
+
});
|
|
84
|
+
case "UserPromptSubmit":
|
|
85
|
+
return enforceUserPromptSubmit(ir, {
|
|
86
|
+
prompt: typeof input.prompt === "string" ? input.prompt : undefined,
|
|
87
|
+
});
|
|
88
|
+
case "SubagentStop":
|
|
89
|
+
return enforceSubagentStop(ir, {
|
|
90
|
+
agent_name: typeof input.agent_name === "string" ? input.agent_name : undefined,
|
|
91
|
+
agent_type: typeof input.agent_type === "string" ? input.agent_type : undefined,
|
|
92
|
+
});
|
|
93
|
+
case "PreCompact":
|
|
94
|
+
return enforcePreCompact(ir);
|
|
95
|
+
case "Notification":
|
|
96
|
+
return enforceNotification(ir, {
|
|
97
|
+
title: typeof input.title === "string" ? input.title : undefined,
|
|
98
|
+
message: typeof input.message === "string" ? input.message : undefined,
|
|
99
|
+
});
|
|
100
|
+
case "SessionStart":
|
|
101
|
+
return enforceSessionStart(ir, {
|
|
102
|
+
cwd: typeof input.cwd === "string" ? input.cwd : undefined,
|
|
103
|
+
session_id: typeof input.session_id === "string" ? input.session_id : undefined,
|
|
104
|
+
});
|
|
105
|
+
default:
|
|
106
|
+
return { continue: true, suppressOutput: true };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// ── Tool Registration ───────────────────────────────────────
|
|
110
|
+
export function createEnforceTools(projectDir) {
|
|
111
|
+
const cache = new IRCache(projectDir);
|
|
112
|
+
return [
|
|
113
|
+
{
|
|
114
|
+
name: "dna_enforce",
|
|
115
|
+
description: "Run DNA enforcement for a hook event. Uses in-memory cached IR for zero cold-start. " +
|
|
116
|
+
"Returns the same result as the dna-hook binary but without spawning a new process.",
|
|
117
|
+
inputSchema: {
|
|
118
|
+
type: "object",
|
|
119
|
+
properties: {
|
|
120
|
+
event: {
|
|
121
|
+
type: "string",
|
|
122
|
+
description: "Hook event type",
|
|
123
|
+
enum: [...VALID_EVENTS],
|
|
124
|
+
},
|
|
125
|
+
input: {
|
|
126
|
+
type: "object",
|
|
127
|
+
description: "Hook input (same fields as Claude Code passes to hooks via stdin)",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
required: ["event", "input"],
|
|
131
|
+
},
|
|
132
|
+
handler: async (args) => {
|
|
133
|
+
const event = String(args.event);
|
|
134
|
+
if (!VALID_EVENTS.has(event)) {
|
|
135
|
+
return errorResult(`Invalid event: ${event}. Valid: ${[...VALID_EVENTS].join(", ")}`);
|
|
136
|
+
}
|
|
137
|
+
const cached = await cache.get();
|
|
138
|
+
if (!cached) {
|
|
139
|
+
return errorResult("No compiled IR found. Run `dna sync` to compile your DNA config.");
|
|
140
|
+
}
|
|
141
|
+
const input = (args.input ?? {});
|
|
142
|
+
const result = dispatchEnforce(event, cached.ir, input, cached.roles);
|
|
143
|
+
// Format as human-readable text
|
|
144
|
+
const lines = [];
|
|
145
|
+
lines.push(`Decision: ${result.continue ? "allow" : "BLOCK"}`);
|
|
146
|
+
if (result.reason)
|
|
147
|
+
lines.push(`Reason: ${result.reason}`);
|
|
148
|
+
if (result.hookSpecificOutput?.additionalContext) {
|
|
149
|
+
lines.push(`Context: ${result.hookSpecificOutput.additionalContext}`);
|
|
150
|
+
}
|
|
151
|
+
return textResult(lines.join("\n"));
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "dna_enforce_cache_status",
|
|
156
|
+
description: "Check IR cache status — whether IR is loaded, file path, and last load time",
|
|
157
|
+
inputSchema: { type: "object", properties: {} },
|
|
158
|
+
handler: async () => {
|
|
159
|
+
const cached = await cache.get();
|
|
160
|
+
if (!cached) {
|
|
161
|
+
return textResult("IR cache: empty (no compiled IR found at .dna/compiled/ir.json)");
|
|
162
|
+
}
|
|
163
|
+
const lines = [
|
|
164
|
+
"IR cache: loaded",
|
|
165
|
+
` Templates: ${cached.ir.source_dna_ids.join(", ")}`,
|
|
166
|
+
` Compiled: ${cached.ir.compiled_at}`,
|
|
167
|
+
` Directives: ${cached.ir.prompt_directives.length}`,
|
|
168
|
+
` Gates: ${cached.ir.pre_execution_gates.length}`,
|
|
169
|
+
` Filters: ${cached.ir.tool_filters.length}`,
|
|
170
|
+
];
|
|
171
|
+
if (cached.roles) {
|
|
172
|
+
lines.push(` Roles: ${Object.keys(cached.roles).join(", ")}`);
|
|
173
|
+
}
|
|
174
|
+
return textResult(lines.join("\n"));
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Parses the YAML subset used by DNA templates and dna import output.
|
|
5
5
|
* Supports: scalars, arrays (inline and block), objects (indentation),
|
|
6
|
-
* comments, quoted strings
|
|
7
|
-
*
|
|
6
|
+
* comments, quoted strings, block scalars (|, >).
|
|
7
|
+
* Does NOT support: anchors, aliases, tags, complex keys, merge keys.
|
|
8
8
|
*
|
|
9
9
|
* Zero external dependencies.
|
|
10
10
|
*/
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Parses the YAML subset used by DNA templates and dna import output.
|
|
5
5
|
* Supports: scalars, arrays (inline and block), objects (indentation),
|
|
6
|
-
* comments, quoted strings
|
|
7
|
-
*
|
|
6
|
+
* comments, quoted strings, block scalars (|, >).
|
|
7
|
+
* Does NOT support: anchors, aliases, tags, complex keys, merge keys.
|
|
8
8
|
*
|
|
9
9
|
* Zero external dependencies.
|
|
10
10
|
*/
|
|
@@ -49,6 +49,34 @@ function parseBlock(lines, startLine, baseIndent) {
|
|
|
49
49
|
const key = content.slice(0, colonIdx).trim();
|
|
50
50
|
const valueStr = content.slice(colonIdx + 1).trim();
|
|
51
51
|
if (valueStr === "" || valueStr === "|" || valueStr === ">") {
|
|
52
|
+
// Block scalar: collect indented lines as a single string
|
|
53
|
+
if (valueStr === "|" || valueStr === ">") {
|
|
54
|
+
const blockLines = [];
|
|
55
|
+
let j = i + 1;
|
|
56
|
+
const blockIndent = j < lines.length ? getIndent(lines[j]) : indent + 2;
|
|
57
|
+
while (j < lines.length) {
|
|
58
|
+
const bLine = lines[j];
|
|
59
|
+
const bStripped = bLine.trimEnd();
|
|
60
|
+
if (bStripped === "") {
|
|
61
|
+
blockLines.push("");
|
|
62
|
+
j++;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const bIndent = getIndent(bLine);
|
|
66
|
+
if (bIndent < blockIndent)
|
|
67
|
+
break;
|
|
68
|
+
blockLines.push(bLine.slice(blockIndent));
|
|
69
|
+
j++;
|
|
70
|
+
}
|
|
71
|
+
// Trim trailing empty lines
|
|
72
|
+
while (blockLines.length > 0 && blockLines[blockLines.length - 1] === "") {
|
|
73
|
+
blockLines.pop();
|
|
74
|
+
}
|
|
75
|
+
const sep = valueStr === "|" ? "\n" : " ";
|
|
76
|
+
obj[key] = blockLines.join(sep) + "\n";
|
|
77
|
+
i = j;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
52
80
|
// Value is a nested block — check next line's indent
|
|
53
81
|
const nextNonEmpty = findNextNonEmptyLine(lines, i + 1);
|
|
54
82
|
if (nextNonEmpty < lines.length) {
|
|
@@ -98,6 +98,25 @@ genes:
|
|
|
98
98
|
signal: uncertain_information
|
|
99
99
|
response: escalate_to_human
|
|
100
100
|
|
|
101
|
+
minimal_change:
|
|
102
|
+
description: Each fix should be the smallest possible change
|
|
103
|
+
codons:
|
|
104
|
+
- type: attract
|
|
105
|
+
target: single_file_change
|
|
106
|
+
- type: repel
|
|
107
|
+
target: unnecessary_refactoring
|
|
108
|
+
- type: threshold
|
|
109
|
+
condition: "files_changed_per_round <= 5"
|
|
110
|
+
action: escalate
|
|
111
|
+
|
|
112
|
+
evidence_based:
|
|
113
|
+
description: All completion claims require fresh test evidence
|
|
114
|
+
codons:
|
|
115
|
+
- type: attract
|
|
116
|
+
target: run_tests_before_claiming_done
|
|
117
|
+
- type: repel
|
|
118
|
+
target: trust_without_evidence
|
|
119
|
+
|
|
101
120
|
contexts: {}
|
|
102
121
|
|
|
103
122
|
|
|
@@ -147,6 +166,9 @@ roles:
|
|
|
147
166
|
- Find where v2 diverges
|
|
148
167
|
- Report with exact file paths and line numbers
|
|
149
168
|
- Never suggest code changes
|
|
169
|
+
- "If round > 1: review previous round's git diff first, judge if direction is correct"
|
|
170
|
+
- "If previous fix produced 0 red→green transitions: warn 'no progress'"
|
|
171
|
+
- "Categorize by severity: CRITICAL (compile errors) > HIGH (logic failures) > LOW (widget/style)"
|
|
150
172
|
|
|
151
173
|
surgeon:
|
|
152
174
|
description: Fixes breakpoints and builds missing layers by understanding v1 intent and rewriting in v2 style.
|
|
@@ -165,6 +187,12 @@ roles:
|
|
|
165
187
|
- After each change, run tests
|
|
166
188
|
- If fix doesn't work, revert and re-analyze
|
|
167
189
|
- Never "improve" code — preserve exact behavior
|
|
190
|
+
- "Before each fix: run `git diff` to confirm previous round's change scope"
|
|
191
|
+
- "After each file change: run `flutter analyze` to verify compilation"
|
|
192
|
+
- "Maximum 5 files per round — if more needed, the scope is too large, split it"
|
|
193
|
+
- "Same issue failed 3 times → STOP and report as blocked, do not retry"
|
|
194
|
+
- "Commit message must include: what changed, why, which test it targets"
|
|
195
|
+
- Do not introduce new abstractions or refactor unrelated code
|
|
168
196
|
|
|
169
197
|
# ── Core align 角色 ──
|
|
170
198
|
core_aligner:
|
|
@@ -188,18 +216,46 @@ workflows:
|
|
|
188
216
|
steps:
|
|
189
217
|
- id: scan
|
|
190
218
|
role: scanner
|
|
191
|
-
description: "
|
|
192
|
-
prompt:
|
|
219
|
+
description: "Detect scenario (first-time vs re-run), scan v1 module accordingly."
|
|
220
|
+
prompt: |
|
|
221
|
+
Scenario detection:
|
|
222
|
+
- Check if {{test_path}}/$ARGUMENTS/ has test files AND {{behavior_docs}}/$ARGUMENTS.md exists
|
|
223
|
+
- Both missing → Scenario 1 (first-time): full scan, output complete behavior doc
|
|
224
|
+
- At least one exists → Scenario 2 (re-run/incremental): read existing behavior doc, compare against v1 source changes, output incremental diff only (do NOT rewrite the entire doc)
|
|
225
|
+
|
|
226
|
+
Scenario 1: Scan module '$ARGUMENTS' in {{v1_path}}/. For each page, list: action → function() → return value. Output to {{behavior_docs}}/$ARGUMENTS.md.
|
|
227
|
+
Scenario 2: Read {{behavior_docs}}/$ARGUMENTS.md. Compare against current v1 source in {{v1_path}}/. Output only the DIFF (added/removed/changed behaviors). Append changes to existing doc, do not rewrite.
|
|
193
228
|
handoff:
|
|
194
229
|
produces:
|
|
195
230
|
- type: file
|
|
196
231
|
path: "{{behavior_docs}}/$ARGUMENTS.md"
|
|
197
|
-
description: "Behavior document for module"
|
|
232
|
+
description: "Behavior document for module (full or incremental)"
|
|
198
233
|
- id: write_tests
|
|
199
234
|
role: test_writer
|
|
200
235
|
depends_on: [scan]
|
|
201
|
-
description: "
|
|
202
|
-
prompt:
|
|
236
|
+
description: "Write or update tests based on scenario, verify compilation."
|
|
237
|
+
prompt: |
|
|
238
|
+
Scenario detection (same as scan step):
|
|
239
|
+
- {{test_path}}/$ARGUMENTS/ has NO test files → Scenario 1 (first-time)
|
|
240
|
+
- {{test_path}}/$ARGUMENTS/ has existing tests → Scenario 2 (re-run)
|
|
241
|
+
|
|
242
|
+
Scenario 1 (first-time):
|
|
243
|
+
- Read {{behavior_docs}}/$ARGUMENTS.md
|
|
244
|
+
- Write tests in {{test_path}}/$ARGUMENTS/. Test ALL layers: logic, widget, navigation
|
|
245
|
+
- Run `flutter analyze` to verify compilation — do NOT run `flutter test` (no implementation yet)
|
|
246
|
+
- Git commit: "behavior-lock($ARGUMENTS): N tests (fresh, not yet runnable)"
|
|
247
|
+
|
|
248
|
+
Scenario 2 (re-run/incremental):
|
|
249
|
+
- Read the incremental diff from behavior doc
|
|
250
|
+
- Update existing tests incrementally — do NOT rewrite all tests (preserves rescue progress)
|
|
251
|
+
- Run `flutter test {{test_path}}/$ARGUMENTS/` with NO timeout — record baseline
|
|
252
|
+
- Append baseline to behavior doc
|
|
253
|
+
- Git commit: "behavior-lock($ARGUMENTS): N tests (X green, Y red from behavior change)"
|
|
254
|
+
|
|
255
|
+
BANNED patterns:
|
|
256
|
+
- Do NOT use `sleep N && check` polling — run commands in foreground
|
|
257
|
+
- Do NOT use `timeout Nm flutter test` — let tests run to completion
|
|
258
|
+
- Do NOT rewrite existing test files from scratch — update incrementally
|
|
203
259
|
handoff:
|
|
204
260
|
consumes:
|
|
205
261
|
- type: file
|
|
@@ -214,12 +270,25 @@ workflows:
|
|
|
214
270
|
|
|
215
271
|
rescue:
|
|
216
272
|
name: Rescue
|
|
217
|
-
description: "Fix v2 module $ARGUMENTS —
|
|
273
|
+
description: "Fix v2 module $ARGUMENTS — investigate, fix, review, verify, report. Max 10 rounds with convergence protection."
|
|
274
|
+
max_rounds: 10
|
|
275
|
+
convergence_rule: "2 consecutive rounds with 0 test progress (green count not increasing) → STOP. Output blocked items + analysis."
|
|
218
276
|
steps:
|
|
219
277
|
- id: investigate
|
|
220
278
|
role: investigator
|
|
221
|
-
description: "Run tests, assess current state, pick next targets."
|
|
222
|
-
prompt:
|
|
279
|
+
description: "Run tests, assess current state, pick next targets by severity."
|
|
280
|
+
prompt: |
|
|
281
|
+
Round context:
|
|
282
|
+
- If round > 1: review previous round's git diff first
|
|
283
|
+
- If previous round had 0 test progress (no red→green or skip→green): warn "no progress" and consider changing approach
|
|
284
|
+
|
|
285
|
+
Run tests in {{test_path}}/$ARGUMENTS/. Categorize all non-passing tests by severity:
|
|
286
|
+
CRITICAL: compile errors, import failures — fix first
|
|
287
|
+
HIGH: logic test failures (state/notifier/service) — fix second
|
|
288
|
+
LOW: widget test failures (rendering/navigation) — fix after logic is done
|
|
289
|
+
|
|
290
|
+
Pick highest severity batch. Trace: what does v1 do vs what does v2 do? Find the breakpoints.
|
|
291
|
+
Report findings and the plan for this round.
|
|
223
292
|
handoff:
|
|
224
293
|
produces:
|
|
225
294
|
- type: summary
|
|
@@ -230,11 +299,20 @@ workflows:
|
|
|
230
299
|
- id: fix
|
|
231
300
|
role: surgeon
|
|
232
301
|
depends_on: [investigate]
|
|
233
|
-
description: "Fix
|
|
302
|
+
description: "Fix identified issues. Max 5 files per round."
|
|
234
303
|
checkpoints:
|
|
235
304
|
- assert: clean_working_tree
|
|
236
|
-
message: "Commit all changes before proceeding
|
|
237
|
-
prompt:
|
|
305
|
+
message: "Commit all changes before proceeding"
|
|
306
|
+
prompt: |
|
|
307
|
+
Fix the identified breakpoints. Rules:
|
|
308
|
+
- Read v1 intent in {{v1_path}}/, rewrite in v2 style in {{v2_path}}/ (not copy v1 verbatim)
|
|
309
|
+
- Maximum 5 files per round — if more needed, split the scope
|
|
310
|
+
- Run `flutter analyze` after each file change
|
|
311
|
+
- Same issue failed 3 times → STOP, report as blocked, do not retry
|
|
312
|
+
- Logic tests: implement notifier/state/service code
|
|
313
|
+
- Widget tests: copy widget from v1, change bindings (Obx→Consumer, Get.to→context.go), set up infra (ProviderScope, mock providers, GoRouter) if needed
|
|
314
|
+
- Run tests after each fix. Red→green or Skip→green = progress. Still failing = revert and re-analyze
|
|
315
|
+
- Commit: "rescue($ARGUMENTS): round N — what changed, why, which tests targeted"
|
|
238
316
|
handoff:
|
|
239
317
|
consumes:
|
|
240
318
|
- type: summary
|
|
@@ -243,11 +321,19 @@ workflows:
|
|
|
243
321
|
produces:
|
|
244
322
|
- type: git_commit
|
|
245
323
|
description: "Rescue round commit"
|
|
246
|
-
- id:
|
|
324
|
+
- id: review
|
|
247
325
|
role: investigator
|
|
248
326
|
depends_on: [fix]
|
|
249
|
-
description: "
|
|
250
|
-
prompt:
|
|
327
|
+
description: "Read-only review of surgeon's changes."
|
|
328
|
+
prompt: |
|
|
329
|
+
Review surgeon's git diff (read-only, do NOT modify any files):
|
|
330
|
+
1. Are changes minimal? No unnecessary files touched?
|
|
331
|
+
2. Does the code match v2 patterns (Riverpod, GoRouter)?
|
|
332
|
+
3. Are mocks correct (not faked just to make tests pass)?
|
|
333
|
+
4. Any new issues introduced?
|
|
334
|
+
|
|
335
|
+
Verdict: APPROVE → proceed to verify
|
|
336
|
+
Verdict: REQUEST_CHANGES → describe specific problems. Next round's investigate step will include this feedback.
|
|
251
337
|
handoff:
|
|
252
338
|
consumes:
|
|
253
339
|
- type: git_commit
|
|
@@ -255,7 +341,50 @@ workflows:
|
|
|
255
341
|
description: "Committed fix from surgeon"
|
|
256
342
|
produces:
|
|
257
343
|
- type: summary
|
|
258
|
-
description: "
|
|
344
|
+
description: "Review verdict (APPROVE or REQUEST_CHANGES)"
|
|
345
|
+
- id: verify
|
|
346
|
+
role: investigator
|
|
347
|
+
depends_on: [review]
|
|
348
|
+
description: "Independent test verification + regression check."
|
|
349
|
+
prompt: |
|
|
350
|
+
Run tests independently (do not trust surgeon's reported results):
|
|
351
|
+
1. `flutter test {{test_path}}/$ARGUMENTS/` (no timeout)
|
|
352
|
+
2. `flutter analyze` (compilation check)
|
|
353
|
+
3. Check for regressions in core module tests if applicable
|
|
354
|
+
|
|
355
|
+
Record test delta vs previous round.
|
|
356
|
+
Each acceptance criterion: VERIFIED / PARTIAL / MISSING
|
|
357
|
+
Verdict: PASS or FAIL
|
|
358
|
+
handoff:
|
|
359
|
+
consumes:
|
|
360
|
+
- type: summary
|
|
361
|
+
from: review
|
|
362
|
+
description: "Review verdict"
|
|
363
|
+
produces:
|
|
364
|
+
- type: test_result
|
|
365
|
+
path: "{{test_path}}/$ARGUMENTS/"
|
|
366
|
+
description: "Verified test results"
|
|
367
|
+
- id: report
|
|
368
|
+
role: investigator
|
|
369
|
+
depends_on: [verify]
|
|
370
|
+
description: "Round summary with convergence judgment."
|
|
371
|
+
prompt: |
|
|
372
|
+
Summary:
|
|
373
|
+
1. Test delta: +N green, -M red, ±K skipped vs last round
|
|
374
|
+
2. Remaining tests by category (logic vs widget vs platform)
|
|
375
|
+
3. Convergence check:
|
|
376
|
+
- If 2 consecutive rounds with no test progress → STOP, output blocked items + analysis
|
|
377
|
+
- If all green → "Module $ARGUMENTS rescue complete"
|
|
378
|
+
- Otherwise → "Run /rescue $ARGUMENTS to continue (round N+1 of max 10)"
|
|
379
|
+
4. If review verdict was REQUEST_CHANGES: include the specific feedback for next round
|
|
380
|
+
handoff:
|
|
381
|
+
consumes:
|
|
382
|
+
- type: test_result
|
|
383
|
+
from: verify
|
|
384
|
+
description: "Verified test results from verify step"
|
|
385
|
+
produces:
|
|
386
|
+
- type: summary
|
|
387
|
+
description: "Round summary with test delta and convergence status"
|
|
259
388
|
|
|
260
389
|
core-align:
|
|
261
390
|
name: Core Align
|
package/package.json
CHANGED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# Spec: flutter-rewrite 模板优化
|
|
2
|
+
|
|
3
|
+
## 问题
|
|
4
|
+
|
|
5
|
+
flutter-rewrite 模板的 behavior-lock 和 rescue 工作流都有设计缺陷,导致:
|
|
6
|
+
1. behavior-lock 不区分首次和重跑,首次跑测试浪费时间
|
|
7
|
+
2. rescue 没有 code review 和 verify 步骤,surgeon 自己写自己审
|
|
8
|
+
3. 没有回归检查、收敛保护、diff 审查
|
|
9
|
+
|
|
10
|
+
参考 OMC agent 设计(code-reviewer/verifier/executor/test-engineer)改进。
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 一、behavior-lock 优化
|
|
15
|
+
|
|
16
|
+
### 场景检测
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
检查 {{test_path}}/$ARGUMENTS/ 是否已有测试文件
|
|
20
|
+
检查 {{behavior_docs}}/$ARGUMENTS.md 是否已存在
|
|
21
|
+
|
|
22
|
+
两者都不存在 → 场景 1(首次)
|
|
23
|
+
至少一个存在 → 场景 2(重跑/增量)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 场景 1: 首次
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
scan → 全量扫描 v1,输出完整行为文档
|
|
30
|
+
write → 写测试文件 → `flutter analyze` 验证编译 → 不跑 flutter test
|
|
31
|
+
commit: "behavior-lock($MODULE): N tests (fresh, not yet runnable)"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 场景 2: 重跑/增量
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
scan → 读已有行为文档,对比 v1 变化,输出增量 diff
|
|
38
|
+
write → 增量更新测试(不全量重写)→ `flutter test` 无超时 → 记录 baseline
|
|
39
|
+
commit: "behavior-lock($MODULE): N tests (X green, Y red from behavior change)"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### scanner 增量
|
|
43
|
+
|
|
44
|
+
场景 2 时 scanner 不重写行为文档,而是:
|
|
45
|
+
1. 读已有 behavior doc
|
|
46
|
+
2. 对比 v1 源码变化
|
|
47
|
+
3. 增量更新
|
|
48
|
+
4. 输出 diff 摘要
|
|
49
|
+
|
|
50
|
+
### 禁止的模式
|
|
51
|
+
|
|
52
|
+
| 禁止 | 原因 | 替代 |
|
|
53
|
+
|------|------|------|
|
|
54
|
+
| `sleep N && check` 轮询 | 浪费时间 | 前台跑 |
|
|
55
|
+
| `timeout Nm flutter test` | 测试可能需要很久 | 不设超时 |
|
|
56
|
+
| 全量重写已有测试 | 丢失 rescue 成果 | 增量更新 |
|
|
57
|
+
| 首次跑 `flutter test` | 没实现,编译不过 | `flutter analyze` |
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 二、rescue 优化
|
|
62
|
+
|
|
63
|
+
### 当前流程(缺陷)
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
investigate → fix → report → 重复
|
|
67
|
+
↑ 无人审查
|
|
68
|
+
↑ 无回归检查
|
|
69
|
+
↑ 无收敛保护
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 优化后流程
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
investigate → fix → review → verify → report
|
|
76
|
+
↑ 只读审查 diff ↑ 自己跑测试+回归
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 新增: review 步骤(参考 OMC code-reviewer)
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
- id: review
|
|
83
|
+
role: investigator # read-only,不能改代码
|
|
84
|
+
depends_on: [fix]
|
|
85
|
+
prompt: |
|
|
86
|
+
Review surgeon's changes (git diff):
|
|
87
|
+
1. 改动是否最小化?有没有改不该改的文件?
|
|
88
|
+
2. 是否匹配 v2 代码风格(Riverpod, GoRouter)?
|
|
89
|
+
3. mock 是否正确(不是为了让测试过而写假 mock)?
|
|
90
|
+
4. 有没有引入新问题?
|
|
91
|
+
|
|
92
|
+
Verdict: APPROVE / REQUEST_CHANGES
|
|
93
|
+
如果 REQUEST_CHANGES → 说明具体问题,下轮 investigate 带上 review 意见
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 新增: verify 步骤(参考 OMC verifier)
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
- id: verify
|
|
100
|
+
role: investigator # read-only
|
|
101
|
+
depends_on: [review] # review APPROVE 后才 verify
|
|
102
|
+
prompt: |
|
|
103
|
+
自己跑测试验证(不信任 surgeon 的报告):
|
|
104
|
+
1. flutter test {{test_path}}/$ARGUMENTS/ (当前模块,无超时)
|
|
105
|
+
2. flutter test 核心模块(回归检查)
|
|
106
|
+
3. flutter analyze(编译检查)
|
|
107
|
+
|
|
108
|
+
每个 acceptance criterion: VERIFIED / PARTIAL / MISSING
|
|
109
|
+
Verdict: PASS / FAIL
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### surgeon 加约束(参考 OMC executor)
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
surgeon:
|
|
116
|
+
instructions:
|
|
117
|
+
# 现有的保留,新增:
|
|
118
|
+
- 每次 fix 前先 `git diff` 确认上轮改动范围
|
|
119
|
+
- 每个文件改动后跑 `flutter analyze`
|
|
120
|
+
- 单轮最多改 5 个文件,超过说明范围太大需要拆分
|
|
121
|
+
- 同一问题失败 3 次 → 停止,报告卡点,不要无限重试
|
|
122
|
+
- commit message 必须包含:改了什么、为什么、对应哪个测试
|
|
123
|
+
- 不引入新抽象,不重构不相关代码
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### investigator 加约束
|
|
127
|
+
|
|
128
|
+
```yaml
|
|
129
|
+
investigator:
|
|
130
|
+
instructions:
|
|
131
|
+
# 现有的保留,新增:
|
|
132
|
+
- 非首轮:先 review 上轮 git diff,判断方向是否正确
|
|
133
|
+
- 如果上轮 fix 没有让任何测试从 red→green,警告"无进展"
|
|
134
|
+
- 分类时标注严重度:CRITICAL(编译不过)> HIGH(逻辑错误)> LOW(widget 样式)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 收敛保护
|
|
138
|
+
|
|
139
|
+
```yaml
|
|
140
|
+
rescue:
|
|
141
|
+
max_rounds: 10
|
|
142
|
+
convergence_rule: |
|
|
143
|
+
连续 2 轮无测试进展(green 数没增加)→ 停止
|
|
144
|
+
输出卡点分析 + 剩余问题清单
|
|
145
|
+
round_budget: |
|
|
146
|
+
每轮最多改 5 个文件
|
|
147
|
+
每轮必须让至少 1 个测试从 red/skip → green,否则视为无进展
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 完整 rescue workflow
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
rescue:
|
|
154
|
+
name: Rescue
|
|
155
|
+
max_rounds: 10
|
|
156
|
+
steps:
|
|
157
|
+
- id: investigate
|
|
158
|
+
role: investigator
|
|
159
|
+
prompt: |
|
|
160
|
+
Round context:
|
|
161
|
+
- If round > 1: review previous round's git diff first
|
|
162
|
+
- If previous round had 0 progress: warn and consider changing approach
|
|
163
|
+
|
|
164
|
+
Run tests, categorize by severity:
|
|
165
|
+
CRITICAL: compile errors, import failures
|
|
166
|
+
HIGH: logic test failures (state/notifier)
|
|
167
|
+
LOW: widget test failures (rendering/navigation)
|
|
168
|
+
|
|
169
|
+
Pick highest severity batch. Trace v1 vs v2. Report breakpoints.
|
|
170
|
+
|
|
171
|
+
- id: fix
|
|
172
|
+
role: surgeon
|
|
173
|
+
depends_on: [investigate]
|
|
174
|
+
checkpoints:
|
|
175
|
+
- assert: clean_working_tree
|
|
176
|
+
prompt: |
|
|
177
|
+
Fix identified breakpoints. Rules:
|
|
178
|
+
- Read v1 intent, rewrite in v2 style (not copy v1 verbatim)
|
|
179
|
+
- Maximum 5 files per round
|
|
180
|
+
- flutter analyze after each file change
|
|
181
|
+
- Same issue failed 3 times → STOP, report as blocked
|
|
182
|
+
- Commit: rescue($ARGUMENTS): round N — what changed and why
|
|
183
|
+
|
|
184
|
+
- id: review
|
|
185
|
+
role: investigator
|
|
186
|
+
depends_on: [fix]
|
|
187
|
+
prompt: |
|
|
188
|
+
Review surgeon's git diff (read-only):
|
|
189
|
+
1. Changes minimal? No unnecessary files touched?
|
|
190
|
+
2. Matches v2 patterns (Riverpod, GoRouter)?
|
|
191
|
+
3. Mocks correct (not faked to pass)?
|
|
192
|
+
4. No new issues introduced?
|
|
193
|
+
Verdict: APPROVE → proceed to verify
|
|
194
|
+
Verdict: REQUEST_CHANGES → next round investigate includes review feedback
|
|
195
|
+
|
|
196
|
+
- id: verify
|
|
197
|
+
role: investigator
|
|
198
|
+
depends_on: [review]
|
|
199
|
+
prompt: |
|
|
200
|
+
Run independently (don't trust surgeon's results):
|
|
201
|
+
1. flutter test {{test_path}}/$ARGUMENTS/ (no timeout)
|
|
202
|
+
2. flutter analyze
|
|
203
|
+
3. Check: any regression in core tests?
|
|
204
|
+
Record delta vs last round. PASS or FAIL.
|
|
205
|
+
|
|
206
|
+
- id: report
|
|
207
|
+
role: investigator
|
|
208
|
+
depends_on: [verify]
|
|
209
|
+
prompt: |
|
|
210
|
+
Summary:
|
|
211
|
+
1. Test delta: +N green, -M red, ±K skipped
|
|
212
|
+
2. Remaining by category (logic vs widget vs platform)
|
|
213
|
+
3. Convergence: are we making progress?
|
|
214
|
+
4. If 2 rounds no progress → STOP, output blocked items
|
|
215
|
+
5. If all green → "Module $ARGUMENTS rescue complete"
|
|
216
|
+
6. Otherwise → "Run /rescue $ARGUMENTS to continue"
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## 三、全局规则(两个工作流共享)
|
|
222
|
+
|
|
223
|
+
### DNA gene 层面
|
|
224
|
+
|
|
225
|
+
```yaml
|
|
226
|
+
genes:
|
|
227
|
+
# 现有 preserve_behavior 等保留,新增:
|
|
228
|
+
minimal_change:
|
|
229
|
+
description: Each fix should be the smallest possible change
|
|
230
|
+
codons:
|
|
231
|
+
- type: attract
|
|
232
|
+
target: single_file_change
|
|
233
|
+
- type: repel
|
|
234
|
+
target: unnecessary_refactoring
|
|
235
|
+
- type: threshold
|
|
236
|
+
condition: "files_changed_per_round <= 5"
|
|
237
|
+
action: escalate
|
|
238
|
+
|
|
239
|
+
evidence_based:
|
|
240
|
+
description: All completion claims require fresh test evidence
|
|
241
|
+
codons:
|
|
242
|
+
- type: attract
|
|
243
|
+
target: run_tests_before_claiming_done
|
|
244
|
+
- type: repel
|
|
245
|
+
target: trust_without_evidence
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Acceptance Criteria
|
|
251
|
+
|
|
252
|
+
### behavior-lock
|
|
253
|
+
- [ ] 场景检测正确(首次 vs 重跑)
|
|
254
|
+
- [ ] 首次: 只 analyze,不 test
|
|
255
|
+
- [ ] 重跑: 增量更新测试,不全量重写
|
|
256
|
+
- [ ] 重跑: 跑测试无超时
|
|
257
|
+
- [ ] scanner 增量: 输出 diff 而非全量重写
|
|
258
|
+
- [ ] 禁止 sleep 轮询模式
|
|
259
|
+
|
|
260
|
+
### rescue
|
|
261
|
+
- [ ] review 步骤: investigator 审查 surgeon 的 git diff
|
|
262
|
+
- [ ] verify 步骤: 独立跑测试 + 回归检查
|
|
263
|
+
- [ ] surgeon 约束: 最多 5 文件/轮,3 次失败停止
|
|
264
|
+
- [ ] 收敛保护: 2 轮无进展 → 停止
|
|
265
|
+
- [ ] 轮次上限: 最多 10 轮
|
|
266
|
+
- [ ] report 包含收敛判断
|
|
267
|
+
|
|
268
|
+
### 全局
|
|
269
|
+
- [ ] minimal_change gene 编译到 IR
|
|
270
|
+
- [ ] evidence_based gene 编译到 IR
|
|
@@ -172,6 +172,6 @@ hook 报错时输出人类可读的提示而非 JSON。`dna verify` 输出友好
|
|
|
172
172
|
- [x] 远程通信架构预留(接口定义,不实现)
|
|
173
173
|
|
|
174
174
|
### 第三阶段
|
|
175
|
-
- [
|
|
176
|
-
- [
|
|
177
|
-
- [
|
|
175
|
+
- [x] dna sync 自动检测模板版本 + 提示升级
|
|
176
|
+
- [x] MCP server 常驻进程解决冷启动
|
|
177
|
+
- [x] 错误信息人类可读
|