@tea-agent/loop-agent 0.11.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.
- package/CHANGELOG.md +74 -1
- package/README.md +33 -4
- package/dist/application/dag/generate-task-dag.js +45 -0
- package/dist/application/dag/run-dag.js +10 -0
- package/dist/application/dag/validate-dag.js +11 -0
- package/dist/cli/command-definitions.js +10 -3
- package/dist/commands/init.js +74 -7
- package/dist/commands/knowledge.js +129 -31
- package/dist/governance/manifest-types.js +3 -0
- package/dist/shared/package-metadata.js +135 -0
- package/dist/task/config-types.js +6 -1
- package/dist/worker/cli.js +99 -2
- package/dist/worker/delivery/package.js +3 -3
- package/dist/worker/feature/decision-loader.js +37 -6
- package/dist/worker/feature/next-action.js +10 -2
- package/dist/worker/feature/ready-plan-projection.js +81 -0
- package/dist/worker/feature/reducer.js +2 -1
- package/dist/worker/feature/review.js +19 -2
- package/dist/worker/feature/run.js +27 -2
- package/dist/worker/follow-up/approve.js +5 -2
- package/dist/worker/follow-up/factory.js +1 -1
- package/dist/worker/observability/event-history.js +216 -0
- package/dist/worker/observability/read-model.js +552 -118
- package/dist/worker/observe/paths.js +17 -0
- package/dist/worker/observe/routes.js +310 -23
- package/dist/worker/observe/server.js +59 -1
- package/dist/worker/observe/spec-evidence.js +281 -0
- package/dist/worker/observe/static/api.js +46 -0
- package/dist/worker/observe/static/app.js +120 -2598
- package/dist/worker/observe/static/constants.js +148 -0
- package/dist/worker/observe/static/copy.js +67 -0
- package/dist/worker/observe/static/dag-helpers.js +172 -0
- package/dist/worker/observe/static/dag-model.js +72 -0
- package/dist/worker/observe/static/dom.js +61 -0
- package/dist/worker/observe/static/format-pool.js +67 -0
- package/dist/worker/observe/static/format.js +292 -0
- package/dist/worker/observe/static/index.html +300 -82
- package/dist/worker/observe/static/kpi.js +94 -0
- package/dist/worker/observe/static/relations.js +133 -0
- package/dist/worker/observe/static/router.js +93 -0
- package/dist/worker/observe/static/run-processing.js +148 -0
- package/dist/worker/observe/static/shell-chrome.js +68 -0
- package/dist/worker/observe/static/state.js +253 -0
- package/dist/worker/observe/static/styles.css +1731 -495
- package/dist/worker/observe/static/views/batch.js +227 -0
- package/dist/worker/observe/static/views/dag-graph.js +172 -0
- package/dist/worker/observe/static/views/dag-inspector.js +596 -0
- package/dist/worker/observe/static/views/dag.js +362 -0
- package/dist/worker/observe/static/views/dashboard.js +445 -0
- package/dist/worker/observe/static/views/failures.js +143 -0
- package/dist/worker/observe/static/views/feature.js +492 -0
- package/dist/worker/observe/static/views/pool.js +350 -0
- package/dist/worker/observe/static/views/run.js +453 -0
- package/dist/worker/observe/static/views/session-timeline.js +205 -0
- package/dist/worker/observe/static/views/shell.js +7 -0
- package/dist/worker/observe/static/views/task.js +314 -0
- package/dist/worker/observe/static/views/timeline.js +163 -0
- package/dist/worker/pool/doctor.js +165 -0
- package/dist/worker/pool/migrate-state.js +303 -0
- package/dist/worker/pool/run-store.js +205 -17
- package/dist/worker/pool/types.js +17 -1
- package/dist/worker/pool/validation.js +100 -15
- package/dist/worker/report/morning-report.js +12 -2
- package/dist/worker/runner/run-ready.js +41 -26
- package/dist/worker/task-graph/ready-planner.js +136 -0
- package/dist/workflows/dag/controller-identity.js +104 -0
- package/dist/workflows/dag/convergence/controller.js +16 -8
- package/dist/workflows/dag/failure-routing.js +12 -1
- package/dist/workflows/dag/init-hybrid.js +1233 -11
- package/dist/workflows/dag/node-execution.js +123 -29
- package/dist/workflows/dag/repair-artifact.js +91 -0
- package/dist/workflows/dag/report.js +50 -0
- package/dist/workflows/dag/retry-policy.js +138 -0
- package/dist/workflows/dag/runner.js +32 -0
- package/dist/workflows/dag/runtime-contract.js +87 -0
- package/dist/workflows/dag/skill-snapshot.js +2 -0
- package/dist/workflows/dag/types.js +45 -1
- package/dist/workflows/dag/validate.js +68 -4
- package/docs/README.md +1 -1
- package/docs/agent-dag-recovery-playbook.md +9 -0
- package/docs/agent-dag-runner.md +26 -1
- package/docs/architecture/dag-execution.md +6 -0
- package/docs/architecture/evolution.md +7 -5
- package/docs/architecture/facts-and-state.md +15 -2
- package/docs/architecture/worker-and-feature.md +6 -2
- package/docs/decisions/README.md +3 -0
- package/docs/design/README.md +12 -3
- package/docs/exec-plans/active/README.md +2 -2
- package/docs/exec-plans/completed/README.md +12 -0
- package/docs/feature-workflow.md +108 -2
- package/docs/loop-agent-harness.md +15 -4
- package/docs/progress/README.md +22 -0
- package/docs/reports/README.md +14 -2
- package/docs/templates/agent-dag-report.schema.json +17 -0
- package/docs/templates/agent-dag.schema.json +69 -1
- package/docs/templates/agent-dag.supervised-implementation.json +8 -2
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +139 -0
- package/docs/templates/backend-test-dag.json +288 -0
- package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -0
- package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -0
- package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -0
- package/docs/templates/knowledge-sync-dag.json +177 -0
- package/docs/templates/knowledge-sync-draft.schema.json +71 -0
- package/docs/verification-matrix.md +2 -1
- package/package.json +8 -2
- package/scripts/kb-bootstrap-init-skeleton.sh +239 -0
- package/scripts/kb-graph-incremental-prepare.mjs +372 -0
- package/scripts/kb-graph-incremental-prepare.sh +5 -0
- package/scripts/kb-graph-materialize.mjs +105 -0
- package/scripts/kb-graph-materialize.sh +4 -0
- package/scripts/kb-graph-promote.mjs +153 -0
- package/scripts/kb-graph-promote.sh +4 -0
- package/scripts/kb-query.mjs +554 -0
- package/scripts/kb-query.sh +5 -0
- package/skills/agent-worker/SKILL.md +3 -1
- package/skills/agent-worker/references/agent-worker-operator.md +18 -1
- package/skills/frontend-design-review/SKILL.md +26 -24
- package/skills/frontend-implementation/SKILL.md +29 -26
- package/skills/frontend-implementation/references/node-contracts.md +50 -19
- package/skills/frontend-review/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +2 -0
- package/skills/loop-agent/references/hybrid-dag.md +22 -3
- package/skills/loop-agent/references/verification-and-failure-handling.md +6 -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();
|
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
-
|
|
29
|
-
|
|
30
|
-
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
37
|
-
|
|
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
|
|
43
|
-
|
|
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
|
-
|
|
59
|
-
decision, and exact plan change required.
|
|
61
|
+
Each correction names its criterion/section, gap, and exact plan change.
|