@tea-agent/loop-agent 0.12.0 → 0.13.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +50 -2
  2. package/README.md +30 -2
  3. package/dist/application/dag/generate-task-dag.js +30 -0
  4. package/dist/cli/command-definitions.js +10 -3
  5. package/dist/commands/knowledge.js +129 -31
  6. package/dist/governance/manifest-types.js +3 -0
  7. package/dist/task/config-types.js +5 -1
  8. package/dist/worker/cli.js +96 -1
  9. package/dist/worker/delivery/package.js +3 -3
  10. package/dist/worker/feature/decision-loader.js +37 -6
  11. package/dist/worker/feature/next-action.js +10 -2
  12. package/dist/worker/feature/ready-plan-projection.js +81 -0
  13. package/dist/worker/feature/reducer.js +2 -1
  14. package/dist/worker/feature/review.js +19 -2
  15. package/dist/worker/feature/run.js +27 -2
  16. package/dist/worker/follow-up/approve.js +5 -2
  17. package/dist/worker/follow-up/factory.js +1 -1
  18. package/dist/worker/observability/read-model.js +246 -41
  19. package/dist/worker/observe/routes.js +158 -15
  20. package/dist/worker/observe/spec-evidence.js +281 -0
  21. package/dist/worker/observe/static/api.js +19 -0
  22. package/dist/worker/observe/static/app.js +2 -2
  23. package/dist/worker/observe/static/relations.js +17 -12
  24. package/dist/worker/observe/static/router.js +8 -0
  25. package/dist/worker/observe/static/styles.css +12 -0
  26. package/dist/worker/observe/static/views/batch.js +3 -2
  27. package/dist/worker/observe/static/views/dag-inspector.js +123 -4
  28. package/dist/worker/observe/static/views/dashboard.js +8 -5
  29. package/dist/worker/observe/static/views/feature.js +43 -4
  30. package/dist/worker/observe/static/views/pool.js +5 -2
  31. package/dist/worker/observe/static/views/run.js +1 -1
  32. package/dist/worker/observe/static/views/task.js +69 -15
  33. package/dist/worker/pool/doctor.js +165 -0
  34. package/dist/worker/pool/migrate-state.js +303 -0
  35. package/dist/worker/pool/run-store.js +205 -17
  36. package/dist/worker/pool/types.js +17 -1
  37. package/dist/worker/pool/validation.js +100 -15
  38. package/dist/worker/report/morning-report.js +12 -2
  39. package/dist/worker/runner/run-ready.js +41 -26
  40. package/dist/worker/task-graph/ready-planner.js +136 -0
  41. package/dist/workflows/dag/convergence/controller.js +16 -8
  42. package/dist/workflows/dag/failure-routing.js +12 -1
  43. package/dist/workflows/dag/init-hybrid.js +837 -8
  44. package/dist/workflows/dag/types.js +1 -0
  45. package/docs/README.md +1 -1
  46. package/docs/agent-dag-recovery-playbook.md +9 -0
  47. package/docs/architecture/evolution.md +4 -3
  48. package/docs/architecture/facts-and-state.md +14 -1
  49. package/docs/architecture/worker-and-feature.md +6 -2
  50. package/docs/decisions/README.md +3 -0
  51. package/docs/design/README.md +8 -0
  52. package/docs/exec-plans/active/README.md +2 -0
  53. package/docs/exec-plans/completed/README.md +3 -2
  54. package/docs/feature-workflow.md +80 -2
  55. package/docs/loop-agent-harness.md +15 -4
  56. package/docs/progress/README.md +4 -0
  57. package/docs/reports/README.md +6 -0
  58. package/docs/templates/backend-test-dag.json +12 -0
  59. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -0
  60. package/docs/templates/knowledge-sync-dag.json +177 -0
  61. package/docs/templates/knowledge-sync-draft.schema.json +71 -0
  62. package/docs/verification-matrix.md +2 -1
  63. package/package.json +8 -2
  64. package/scripts/kb-bootstrap-init-skeleton.sh +239 -0
  65. package/scripts/kb-graph-incremental-prepare.mjs +372 -0
  66. package/scripts/kb-graph-incremental-prepare.sh +5 -0
  67. package/scripts/kb-graph-materialize.mjs +105 -0
  68. package/scripts/kb-graph-materialize.sh +4 -0
  69. package/scripts/kb-graph-promote.mjs +153 -0
  70. package/scripts/kb-graph-promote.sh +4 -0
  71. package/scripts/kb-query.mjs +554 -0
  72. package/scripts/kb-query.sh +5 -0
  73. package/skills/agent-worker/SKILL.md +3 -1
  74. package/skills/agent-worker/references/agent-worker-operator.md +18 -1
  75. package/skills/frontend-design-review/SKILL.md +26 -24
  76. package/skills/frontend-implementation/SKILL.md +29 -26
  77. package/skills/frontend-implementation/references/node-contracts.md +50 -19
  78. package/skills/frontend-review/SKILL.md +1 -1
  79. package/skills/loop-agent/references/command-reference.md +1 -0
@@ -0,0 +1,554 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Phase A knowledge-base query (by_id / by_feature / search).
4
+ * Authority: Git files under knowledge/ and features/. Not RAG.
5
+ *
6
+ * Usage:
7
+ * node scripts/kb-query.mjs --mode by_feature --feature F-2026-004 --json
8
+ * node scripts/kb-query.mjs --mode by_id --id SVC-order --json
9
+ * node scripts/kb-query.mjs --mode search --text 预占 --json
10
+ */
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import process from "node:process";
14
+
15
+ function usage(exitCode = 2) {
16
+ const text = `Usage: node scripts/kb-query.mjs --mode <by_id|by_feature|search> [options]
17
+
18
+ Options:
19
+ --root <dir> Repository root (default: cwd)
20
+ --mode <mode> by_id | by_feature | search
21
+ --id <id> Entity id (by_id)
22
+ --feature <F-id> Feature id (by_feature)
23
+ --text <q> Search text (search)
24
+ --glob <pattern> Optional path substring filter for search (repeatable)
25
+ --include-staging Also scan knowledge/bootstrap/staging/**
26
+ --json JSON response (default)
27
+ --markdown Human markdown summary
28
+ -h, --help Help
29
+ `;
30
+ if (exitCode === 0) console.log(text);
31
+ else console.error(text);
32
+ process.exit(exitCode);
33
+ }
34
+
35
+ function parseArgs(argv) {
36
+ const out = {
37
+ root: process.cwd(),
38
+ mode: null,
39
+ id: null,
40
+ feature: null,
41
+ text: null,
42
+ globs: [],
43
+ includeStaging: false,
44
+ format: "json",
45
+ };
46
+ for (let i = 0; i < argv.length; i++) {
47
+ const a = argv[i];
48
+ const next = () => {
49
+ const v = argv[++i];
50
+ if (v === undefined) throw new Error(`missing value for ${a}`);
51
+ return v;
52
+ };
53
+ switch (a) {
54
+ case "--root":
55
+ out.root = path.resolve(next());
56
+ break;
57
+ case "--mode":
58
+ out.mode = next();
59
+ break;
60
+ case "--id":
61
+ out.id = next();
62
+ break;
63
+ case "--feature":
64
+ out.feature = next();
65
+ break;
66
+ case "--text":
67
+ out.text = next();
68
+ break;
69
+ case "--glob":
70
+ out.globs.push(next());
71
+ break;
72
+ case "--include-staging":
73
+ out.includeStaging = true;
74
+ break;
75
+ case "--json":
76
+ out.format = "json";
77
+ break;
78
+ case "--markdown":
79
+ out.format = "markdown";
80
+ break;
81
+ case "-h":
82
+ case "--help":
83
+ usage(0);
84
+ break;
85
+ default:
86
+ throw new Error(`unknown argument: ${a}`);
87
+ }
88
+ }
89
+ return out;
90
+ }
91
+
92
+ function exists(p) {
93
+ try {
94
+ fs.accessSync(p);
95
+ return true;
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+
101
+ function readText(p) {
102
+ return fs.readFileSync(p, "utf8");
103
+ }
104
+
105
+ function walkFiles(dir, acc = []) {
106
+ if (!exists(dir)) return acc;
107
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
108
+ const p = path.join(dir, ent.name);
109
+ if (ent.isDirectory()) walkFiles(p, acc);
110
+ else acc.push(p);
111
+ }
112
+ return acc;
113
+ }
114
+
115
+ function toPosix(root, abs) {
116
+ return path.relative(root, abs).split(path.sep).join("/");
117
+ }
118
+
119
+ function excerpt(text, max = 240) {
120
+ const one = text.replace(/\s+/g, " ").trim();
121
+ if (one.length <= max) return one;
122
+ return `${one.slice(0, max)}…`;
123
+ }
124
+
125
+ function parseYamlishEntitiesIndex(text) {
126
+ /** Minimal line parser for entities-index.yaml written by materialize / hand. */
127
+ const entities = [];
128
+ let cur = null;
129
+ for (const line of text.split(/\r?\n/)) {
130
+ const id = line.match(/^\s+id:\s*(.+)\s*$/);
131
+ const kind = line.match(/^\s+kind:\s*(.+)\s*$/);
132
+ const p = line.match(/^\s+path:\s*(.+)\s*$/);
133
+ const item = line.match(/^\s*-\s+kind:\s*(.+)\s*$/);
134
+ if (item) {
135
+ if (cur?.id) entities.push(cur);
136
+ cur = { kind: item[1].trim(), id: null, path: null };
137
+ continue;
138
+ }
139
+ if (!cur) continue;
140
+ if (kind) cur.kind = kind[1].trim();
141
+ if (id) cur.id = id[1].trim().replace(/^["']|["']$/g, "");
142
+ if (p) cur.path = p[1].trim().replace(/^["']|["']$/g, "");
143
+ }
144
+ if (cur?.id) entities.push(cur);
145
+ return entities.filter((e) => e.id && e.path);
146
+ }
147
+
148
+ function parseKnowledgeLinks(text) {
149
+ const edges = [];
150
+ // Very small subset: lines like "- type: feature_touches_service" then " to: SVC-x" or "target: "
151
+ let pendingType = null;
152
+ for (const line of text.split(/\r?\n/)) {
153
+ const t = line.match(/^\s*-\s*type:\s*(\S+)/);
154
+ const to =
155
+ line.match(/^\s+(?:to|target|id):\s*(\S+)/) ||
156
+ line.match(/^\s+service_id:\s*(\S+)/);
157
+ if (t) pendingType = t[1];
158
+ if (to && pendingType) {
159
+ edges.push({ type: pendingType, to: to[1].replace(/[",']/g, "") });
160
+ pendingType = null;
161
+ }
162
+ // compact: - feature_touches_service: SVC-order
163
+ const compact = line.match(
164
+ /^\s*-\s*(feature_touches_\w+|touches_\w+|depends_on):\s*(\S+)/,
165
+ );
166
+ if (compact) edges.push({ type: compact[1], to: compact[2] });
167
+ }
168
+ return edges;
169
+ }
170
+
171
+ function loadEntitiesIndex(root) {
172
+ const p = path.join(root, "knowledge/graph/entities-index.yaml");
173
+ if (!exists(p)) return [];
174
+ return parseYamlishEntitiesIndex(readText(p));
175
+ }
176
+
177
+ function scanMetaEntities(root, includeStaging) {
178
+ const bases = [
179
+ "knowledge/domains",
180
+ "knowledge/services",
181
+ "knowledge/modules",
182
+ ];
183
+ if (includeStaging) {
184
+ bases.push(
185
+ "knowledge/bootstrap/staging/domains",
186
+ "knowledge/bootstrap/staging/services",
187
+ "knowledge/bootstrap/staging/modules",
188
+ );
189
+ }
190
+ const entities = [];
191
+ for (const base of bases) {
192
+ for (const file of walkFiles(path.join(root, base))) {
193
+ if (!file.endsWith("meta.yaml") && !file.endsWith("meta.yml")) continue;
194
+ const text = readText(file);
195
+ const id = (text.match(/^id:\s*(\S+)/m) || [])[1];
196
+ const kind = (text.match(/^kind:\s*(\S+)/m) || [])[1] || "unknown";
197
+ if (!id) continue;
198
+ entities.push({
199
+ kind,
200
+ id: id.replace(/["']/g, ""),
201
+ path: toPosix(root, file),
202
+ });
203
+ }
204
+ }
205
+ return entities;
206
+ }
207
+
208
+ function mergeEntities(root, includeStaging) {
209
+ const map = new Map();
210
+ for (const e of [
211
+ ...loadEntitiesIndex(root),
212
+ ...scanMetaEntities(root, includeStaging),
213
+ ]) {
214
+ map.set(`${e.kind}:${e.id}`, e);
215
+ }
216
+ // Features as entities
217
+ const featRoot = path.join(root, "features");
218
+ if (exists(featRoot)) {
219
+ for (const name of fs.readdirSync(featRoot)) {
220
+ if (!/^F-/.test(name)) continue;
221
+ const dir = path.join(featRoot, name);
222
+ if (!fs.statSync(dir).isDirectory()) continue;
223
+ map.set(`feature:${name}`, {
224
+ kind: "feature",
225
+ id: name,
226
+ path: `features/${name}/`,
227
+ });
228
+ }
229
+ }
230
+ return [...map.values()];
231
+ }
232
+
233
+ function resultFromEntity(root, entity, matchReason, score = 1.0) {
234
+ const abs = path.join(root, entity.path);
235
+ let title = entity.id;
236
+ let bodyExcerpt = "";
237
+ if (exists(abs) && fs.statSync(abs).isFile()) {
238
+ const text = readText(abs);
239
+ const t = text.match(/^title:\s*(.+)$/m) || text.match(/^#\s+(.+)$/m);
240
+ if (t) title = t[1].trim();
241
+ bodyExcerpt = excerpt(text);
242
+ } else if (exists(abs) && fs.statSync(abs).isDirectory()) {
243
+ const readme = path.join(abs, "requirement.md");
244
+ const overview = path.join(abs, "overview.md");
245
+ const pick = exists(readme) ? readme : exists(overview) ? overview : null;
246
+ if (pick) {
247
+ const text = readText(pick);
248
+ const t = text.match(/^#\s+(.+)$/m);
249
+ if (t) title = t[1].trim();
250
+ bodyExcerpt = excerpt(text);
251
+ }
252
+ }
253
+ return {
254
+ entity: { kind: entity.kind, id: entity.id },
255
+ path: entity.path,
256
+ title,
257
+ excerpt: bodyExcerpt,
258
+ score,
259
+ match_reason: matchReason,
260
+ };
261
+ }
262
+
263
+ function queryById(root, id, includeStaging) {
264
+ const entities = mergeEntities(root, includeStaging);
265
+ const hits = entities.filter((e) => e.id === id);
266
+ const results = hits.map((e) =>
267
+ resultFromEntity(root, e, hits.length === 1 ? "exact_id" : "id_match"),
268
+ );
269
+
270
+ // Also direct path conventions for testing cases
271
+ if (results.length === 0) {
272
+ const candidates = [
273
+ `features/${id}/`,
274
+ `knowledge/services/${id}/meta.yaml`,
275
+ `knowledge/domains/${id}/meta.yaml`,
276
+ `knowledge/modules/${id}/meta.yaml`,
277
+ ];
278
+ for (const rel of candidates) {
279
+ const abs = path.join(root, rel);
280
+ if (exists(abs)) {
281
+ results.push(
282
+ resultFromEntity(
283
+ root,
284
+ {
285
+ kind: rel.startsWith("features/") ? "feature" : "unknown",
286
+ id,
287
+ path: rel,
288
+ },
289
+ "path_convention",
290
+ ),
291
+ );
292
+ }
293
+ }
294
+ }
295
+
296
+ return {
297
+ schema_version: 1,
298
+ ok: results.length > 0,
299
+ mode: "by_id",
300
+ results,
301
+ edges_trace: [],
302
+ diagnostics: {
303
+ levels_used: ["Q1"],
304
+ truncated: false,
305
+ entity_count_scanned: entities.length,
306
+ },
307
+ error:
308
+ results.length === 0
309
+ ? `no entity with id=${id}; run materialize or check knowledge/graph/entities-index.yaml`
310
+ : undefined,
311
+ };
312
+ }
313
+
314
+ function queryByFeature(root, featureId, includeStaging) {
315
+ const featureDir = path.join(root, "features", featureId);
316
+ if (!exists(featureDir)) {
317
+ return {
318
+ schema_version: 1,
319
+ ok: false,
320
+ mode: "by_feature",
321
+ results: [],
322
+ edges_trace: [],
323
+ diagnostics: { levels_used: ["Q0"], truncated: false },
324
+ error: `missing features/${featureId}/`,
325
+ };
326
+ }
327
+
328
+ const results = [];
329
+ const edgesTrace = [];
330
+ results.push(
331
+ resultFromEntity(
332
+ root,
333
+ { kind: "feature", id: featureId, path: `features/${featureId}/` },
334
+ "feature_root",
335
+ ),
336
+ );
337
+
338
+ const interesting = [
339
+ "requirement.md",
340
+ "acceptance.yaml",
341
+ "knowledge-links.yaml",
342
+ "testing/acceptance-verdict.yaml",
343
+ "testing/coverage-matrix.yaml",
344
+ "testing/automation-map.yaml",
345
+ "testing/defects/registry.yaml",
346
+ "testing/cases/index.yaml",
347
+ "testing/cases/backend/index.yaml",
348
+ "testing/runs/latest.md",
349
+ ];
350
+ for (const rel of interesting) {
351
+ const abs = path.join(featureDir, rel);
352
+ if (!exists(abs)) continue;
353
+ const posix = `features/${featureId}/${rel}`;
354
+ const text = readText(abs);
355
+ results.push({
356
+ entity: {
357
+ kind: rel.includes("testing") ? "testing-asset" : "feature-doc",
358
+ id: `${featureId}:${rel}`,
359
+ },
360
+ path: posix,
361
+ title: rel,
362
+ excerpt: excerpt(text),
363
+ score: 1.0,
364
+ match_reason: "feature_pack",
365
+ });
366
+ }
367
+
368
+ const linksPath = path.join(featureDir, "knowledge-links.yaml");
369
+ const entities = mergeEntities(root, includeStaging);
370
+ if (exists(linksPath)) {
371
+ const edges = parseKnowledgeLinks(readText(linksPath));
372
+ for (const edge of edges) {
373
+ edgesTrace.push(`${edge.type}->${edge.to}`);
374
+ const hit = entities.find((e) => e.id === edge.to);
375
+ if (hit) {
376
+ results.push(
377
+ resultFromEntity(root, hit, `edge:${edge.type}`),
378
+ );
379
+ } else {
380
+ results.push({
381
+ entity: { kind: "unknown", id: edge.to },
382
+ path: `features/${featureId}/knowledge-links.yaml`,
383
+ title: edge.to,
384
+ excerpt: `linked via ${edge.type}; entity body not in index`,
385
+ score: 1.0,
386
+ match_reason: `edge:${edge.type}:unresolved`,
387
+ });
388
+ }
389
+ }
390
+ }
391
+
392
+ // edges.yaml mentions
393
+ const edgesFile = path.join(root, "knowledge/graph/edges.yaml");
394
+ if (exists(edgesFile)) {
395
+ const text = readText(edgesFile);
396
+ if (text.includes(featureId)) {
397
+ results.push({
398
+ entity: { kind: "graph-edges", id: "edges.yaml" },
399
+ path: "knowledge/graph/edges.yaml",
400
+ title: "graph edges",
401
+ excerpt: excerpt(
402
+ text
403
+ .split(/\r?\n/)
404
+ .filter((l) => l.includes(featureId))
405
+ .join("\n") || text,
406
+ ),
407
+ score: 1.0,
408
+ match_reason: "graph_edges_mention",
409
+ });
410
+ }
411
+ }
412
+
413
+ return {
414
+ schema_version: 1,
415
+ ok: true,
416
+ mode: "by_feature",
417
+ results,
418
+ edges_trace: edgesTrace,
419
+ diagnostics: {
420
+ levels_used: ["Q0", "Q1", "Q2"],
421
+ truncated: false,
422
+ },
423
+ };
424
+ }
425
+
426
+ function matchesGlobFilters(rel, globs) {
427
+ if (!globs.length) return true;
428
+ return globs.some((g) => {
429
+ const simple = g.replace(/\*\*/g, "").replace(/\*/g, "");
430
+ if (simple && rel.includes(simple)) return true;
431
+ try {
432
+ return globToLoose(g).test(rel);
433
+ } catch {
434
+ return false;
435
+ }
436
+ });
437
+ }
438
+
439
+ function querySearch(root, text, globs, includeStaging) {
440
+ const q = text.toLowerCase();
441
+ const roots = ["knowledge", "features"];
442
+ if (includeStaging) roots.push("knowledge/bootstrap/staging");
443
+ const results = [];
444
+ for (const base of roots) {
445
+ for (const file of walkFiles(path.join(root, base))) {
446
+ const rel = toPosix(root, file);
447
+ if (!matchesGlobFilters(rel, globs)) continue;
448
+ let body;
449
+ try {
450
+ body = readText(file);
451
+ } catch {
452
+ continue;
453
+ }
454
+ if (!body.toLowerCase().includes(q)) continue;
455
+ const idx = body.toLowerCase().indexOf(q);
456
+ const slice = body.slice(Math.max(0, idx - 40), idx + q.length + 80);
457
+ results.push({
458
+ entity: { kind: "file", id: rel },
459
+ path: rel,
460
+ title: path.basename(file),
461
+ excerpt: excerpt(slice, 200),
462
+ score: 0.5,
463
+ match_reason: "search_text",
464
+ });
465
+ if (results.length >= 20) break;
466
+ }
467
+ if (results.length >= 20) break;
468
+ }
469
+ return {
470
+ schema_version: 1,
471
+ ok: results.length > 0,
472
+ mode: "search",
473
+ results,
474
+ edges_trace: [],
475
+ diagnostics: {
476
+ levels_used: ["Q3"],
477
+ truncated: results.length >= 20,
478
+ },
479
+ error:
480
+ results.length === 0
481
+ ? `no matches for ${JSON.stringify(text)}`
482
+ : undefined,
483
+ };
484
+ }
485
+
486
+ function globToLoose(g) {
487
+ // minimal: treat * as .*
488
+ const esc = g.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
489
+ return new RegExp(esc);
490
+ }
491
+
492
+ function toMarkdown(resp) {
493
+ const lines = [
494
+ `# kb-query ${resp.mode}`,
495
+ `ok: ${resp.ok}`,
496
+ resp.error ? `error: ${resp.error}` : null,
497
+ "",
498
+ "## results",
499
+ ].filter(Boolean);
500
+ for (const r of resp.results || []) {
501
+ lines.push(
502
+ `- **${r.entity?.id ?? "?"}** (${r.entity?.kind ?? "?"}) \`${r.path}\` — ${r.match_reason}`,
503
+ );
504
+ if (r.excerpt) lines.push(` - ${r.excerpt}`);
505
+ }
506
+ if (resp.edges_trace?.length) {
507
+ lines.push("", "## edges_trace", ...resp.edges_trace.map((e) => `- ${e}`));
508
+ }
509
+ return `${lines.join("\n")}\n`;
510
+ }
511
+
512
+ function main() {
513
+ let args;
514
+ try {
515
+ args = parseArgs(process.argv.slice(2));
516
+ } catch (e) {
517
+ console.error(String(e?.message || e));
518
+ usage(2);
519
+ }
520
+ if (!args.mode) {
521
+ console.error("--mode is required");
522
+ usage(2);
523
+ }
524
+
525
+ let resp;
526
+ if (args.mode === "by_id") {
527
+ if (!args.id) {
528
+ console.error("by_id requires --id");
529
+ process.exit(2);
530
+ }
531
+ resp = queryById(args.root, args.id, args.includeStaging);
532
+ } else if (args.mode === "by_feature") {
533
+ if (!args.feature) {
534
+ console.error("by_feature requires --feature");
535
+ process.exit(2);
536
+ }
537
+ resp = queryByFeature(args.root, args.feature, args.includeStaging);
538
+ } else if (args.mode === "search") {
539
+ if (!args.text) {
540
+ console.error("search requires --text");
541
+ process.exit(2);
542
+ }
543
+ resp = querySearch(args.root, args.text, args.globs, args.includeStaging);
544
+ } else {
545
+ console.error(`unsupported mode: ${args.mode}`);
546
+ process.exit(2);
547
+ }
548
+
549
+ if (args.format === "markdown") process.stdout.write(toMarkdown(resp));
550
+ else process.stdout.write(`${JSON.stringify(resp, null, 2)}\n`);
551
+ process.exit(resp.ok ? 0 : 1);
552
+ }
553
+
554
+ main();
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ # Phase A knowledge query wrapper (by_id / by_feature / search).
3
+ set -euo pipefail
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ exec node "${ROOT_DIR}/scripts/kb-query.mjs" "$@"
@@ -26,11 +26,13 @@ references:
26
26
 
27
27
  ## Operator Flow
28
28
 
29
- 1. 校验 Feature Packet 与 TaskSpecs,再从 Task Pool facts 选择 Ready 工作,而不是从 chat state。
29
+ 1. 校验 Feature Packet 与 TaskSpecs,再从 Task Pool facts 选择 Ready 工作,而不是从 chat state。Task Pool 身份是 `{ featureId, taskId }`,不是裸 taskId。
30
30
  2. 在写入前解析并冻结目标 published controller;在 batch、task 与 run evidence 中保留 controller identity。
31
31
  3. 将仓库写入委托给受治理的 loop-agent DAG nodes,并审查其 task boundaries 与 write sets。
32
32
  4. 自举时保持 published version N 固定,由它维护 candidate N+1,再通过 deterministic isolated canary 证明 candidate takeover。
33
33
  5. 失败时先保留原始 run record、evidence 与 failure handoff,再重试或创建后续工作。
34
+ 6. Failed task 重试使用 `agent-worker task retry <task-id> --feature-id <feature-id> --repo <repo> --reason <reason>`;跨 Feature 同名时禁止省略 `--feature-id`。
35
+ 7. 升级或发现 legacy state 时先 `pool doctor`,再用 `pool migrate-state`(默认 dry-run;apply 需 `--owner` + `--reason`)。Observe 保持只读。
34
36
 
35
37
  ## References
36
38
 
@@ -19,10 +19,27 @@ Leaf DAG nodes 不得递归启动 `agent-worker`。Worker 负责 DAG 之外的 s
19
19
  ## Feature and Task Pool Flow
20
20
 
21
21
  1. 阅读 Feature Packet,校验其 TaskSpecs 与 dependency graph。
22
- 2. 从持久化的 Task Pool state 推导下一步动作;不要从 chat history 重建 lifecycle state。
22
+ 2. 从持久化的 Task Pool state 推导下一步动作;不要从 chat history 重建 lifecycle state。canonical state 键是 `{ featureId, taskId }`(路径 `.harness/task-pool/states/<featureId>/<taskId>.json`)。
23
23
  3. 对可写 batch 只冻结一次 controller,并将其 identity 传播到下游 evidence。
24
24
  4. 将每个选中的 TaskSpec 委托给 loop-agent,使用其结构化的 allowed / forbidden paths。
25
25
  5. 根据 canonical run facts 刷新 Feature review、reports 与 Task Pool state。
26
+ 6. 重试 Failed task:
27
+
28
+ ```text
29
+ agent-worker task retry <task-id> --feature-id <feature-id> --repo <repo> --reason <reason>
30
+ ```
31
+
32
+ 跨 Feature 同名 Task 时省略 `--feature-id` 必须 fail-closed。
33
+ 7. 诊断 / 迁移 legacy 扁平 state:
34
+
35
+ ```text
36
+ agent-worker pool doctor --repo <repo> --json
37
+ agent-worker pool migrate-state --repo <repo> # dry-run
38
+ agent-worker pool migrate-state --repo <repo> --apply --owner <owner> --reason <reason>
39
+ ```
40
+
41
+ doctor 只读;migrate 默认零写入,apply 失败全回滚且不改 JSONL。
42
+ 8. Observe(`observe serve|snapshot`)只读;canonical Task 路由为 `/api/features/:featureId/tasks/:taskId` 与 `#/feature/:featureId/task/:taskId`。
26
43
 
27
44
  ## Versioned Self-Hosting
28
45
 
@@ -8,39 +8,42 @@ references:
8
8
 
9
9
  # Frontend Design Review
10
10
 
11
- Use for `frontend-design-gate-pi`. Read the required checklist first. Review the
12
- contract, scout report, plan, task source/constraints, `task.json` boundaries, and
13
- traceable component/design evidence. The connector format is TODO; never fabricate
14
- a result. When knowledge-base access is absent, failed, or unmatched, require evidence
15
- that `<repoRoot>/openSpec/**` was searched before accepting other repo conventions.
11
+ For first/final design review nodes. Read the checklist, then audit contract, scout,
12
+ plan/revision, task constraints/bounds, and traceable design evidence. The knowledge-
13
+ base connector is TODO: never invent results. If absent/failed/unmatched, require
14
+ `<repoRoot>/openSpec/**` search/read evidence before repo conventions.
16
15
 
17
16
  ## Verdict Contract
18
17
 
19
- The first non-empty line must be exactly:
18
+ First non-empty line: exactly `VERDICT: pass` or `VERDICT: request-revision`.
19
+ Any blocker requires request-revision; the shell gate reads this line exactly.
20
20
 
21
- - `VERDICT: pass`
22
- - `VERDICT: request-revision`
23
-
24
- Use request-revision for any blocker. The shell gate reads this line exactly.
21
+ For `frontend-final-design-review-pi`: recheck the original plan when output is
22
+ `PASS_NO_REVISION_NEEDED`; otherwise verify every Required Plan Correction. Reject
23
+ remaining, incomplete, or newly introduced gaps.
25
24
 
26
25
  ## Blocking Conditions
27
26
 
28
- - An acceptance criterion lacks implementation or verification coverage.
29
- - Applicable UI states are missing without a reason.
30
- - A dependency is not explicitly permitted.
31
- - Confirmed project primitives/rules are ignored, or a claim has neither knowledge-base nor required `openSpec/` fallback evidence.
32
- - Planned paths exceed allowed/write scope or touch forbidden/generated paths.
33
- - Required static or behavior commands are missing or non-deterministic.
34
- - Interaction, responsive, accessibility, data, or failure behavior is ambiguous enough to require guessing.
27
+ - Any criterion lacks implementation/verification; UI states lack reasons; a
28
+ dependency lacks permission; confirmed primitives/rules are ignored; design claims
29
+ lack knowledge-base or required `openSpec/` evidence; paths cross write bounds;
30
+ commands are missing/non-deterministic; or interaction, responsive, accessibility,
31
+ data, or failure behavior requires guessing.
35
32
 
36
- Knowledge-base unavailability alone is advisory when relevant `openSpec/` rules were
37
- searched and applied. Block when that fallback was skipped, sources conflict without
38
- resolution, or no source resolves a required user-visible decision.
33
+ Knowledge-base absence is advisory if relevant `openSpec/` rules were searched/read
34
+ and applied. Block skipped fallback, unresolved conflict, or unresolved UI decisions.
39
35
 
40
36
  ## Method And Output
41
37
 
42
- Map each criterion to steps, files, states, and checks; audit paths; verify evidence;
43
- then classify findings as Blocking or Advisory. Do not edit files.
38
+ Map criteria to steps/files/states/checks, audit paths/evidence, classify Blocking or
39
+ Advisory, and never edit files.
40
+
41
+ ### Spec Evidence Rules
42
+
43
+ Run `grep`/`find`, then explicit `read` calls for applicable specs and checklist.
44
+ Only successful paired reads count as “已读取规范文件”; summaries do not. List each
45
+ read path/section in `Checked Items`. If the connector is unavailable, search/read
46
+ `openSpec/` before accepting repository conventions.
44
47
 
45
48
  ```markdown
46
49
  VERDICT: pass
@@ -55,5 +58,4 @@ VERDICT: pass
55
58
  - ...
56
59
  ```
57
60
 
58
- For revisions, every correction names the criterion/section, missing evidence or
59
- decision, and exact plan change required.
61
+ Each correction names its criterion/section, gap, and exact plan change.