@tea-agent/loop-agent 0.24.11-beta.0 → 0.25.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 +28 -5
- package/dist/commands/init.js +16 -2
- package/dist/executors/dag-pi-executor.js +74 -13
- package/dist/executors/pi-executor.js +25 -7
- package/dist/executors/pi-prompt-transport.js +198 -0
- package/dist/executors/pi-sdk-executor.js +1 -1
- package/dist/executors/process-tree.js +33 -0
- package/dist/executors/shell-executor.js +332 -24
- package/dist/executors/shell-verification.js +4 -2
- package/dist/infrastructure/harness/task-store.js +31 -0
- package/dist/shared/operator/capabilities.js +6 -0
- package/dist/verification/maven/cache.js +142 -0
- package/dist/verification/maven/index.js +120 -0
- package/dist/verification/maven/plan-commands.js +421 -0
- package/dist/verification/maven/pom-static.js +136 -0
- package/dist/verification/maven/scope-resolve.js +153 -0
- package/dist/verification/maven/stale.js +130 -0
- package/dist/verification/maven/types.js +23 -0
- package/dist/verification/maven/workspace-graph.js +322 -0
- package/dist/worker/cli.js +236 -21
- package/dist/worker/delivery/final-verification.js +12 -0
- package/dist/worker/delivery/git-transaction.js +32 -7
- package/dist/worker/delivery/package.js +9 -5
- package/dist/worker/delivery/verification-bundle.js +26 -5
- package/dist/worker/feature/advance.js +301 -0
- package/dist/worker/feature/doctor.js +223 -0
- package/dist/worker/feature/next-action.js +11 -3
- package/dist/worker/feature/scaffold.js +798 -0
- package/dist/worker/observe/dag-run-artifacts.js +90 -0
- package/dist/worker/observe/node-input.js +444 -0
- package/dist/worker/observe/routes.js +17 -0
- package/dist/worker/observe/static/api.js +9 -0
- package/dist/worker/observe/static/constants.js +9 -0
- package/dist/worker/observe/static/state.js +14 -0
- package/dist/worker/observe/static/styles.css +74 -0
- package/dist/worker/observe/static/views/dag-inspector.js +371 -15
- package/dist/worker/outcomes/projector.js +5 -1
- package/dist/worker/runner/run-ready.js +41 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +202 -9
- package/dist/workflows/dag/backend-test-result-contract.js +103 -0
- package/dist/workflows/dag/failure-category.js +5 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +4 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +0 -17
- package/dist/workflows/dag/frontend-test-result-contract.js +106 -41
- package/dist/workflows/dag/init-hybrid.js +220 -32
- package/dist/workflows/dag/node-execution.js +3 -2
- package/dist/workflows/dag/reconcile-run.js +24 -0
- package/dist/workflows/dag/types.js +46 -0
- package/dist/workflows/dag/validate.js +19 -2
- package/docs/architecture/dag-execution.md +5 -1
- package/docs/architecture/runtime-boundaries.md +11 -1
- package/docs/architecture/worker-and-feature.md +2 -0
- package/docs/templates/backend-test-dag.json +3 -3
- package/docs/templates/product-line/README.md +17 -1
- package/docs/templates/product-line/feature-scaffold-batch.example.yaml +31 -0
- package/docs/templates/product-line/scaffold-samples/README.md +36 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/acceptance.yaml +13 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/design.md +14 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/feature.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/requirement.md +6 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/tasks/BE-IMPL-001.yaml +83 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/tasks/task-graph.yaml +14 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/acceptance.yaml +15 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/design.md +18 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/feature.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/requirement.md +6 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/BE-IMPL-001.yaml +84 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/CONTRACT-001.yaml +84 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/FE-IMPL-001.yaml +86 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/task-graph.yaml +40 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/acceptance.yaml +13 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/design.md +14 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/feature.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/requirement.md +6 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/FE-IMPL-001.yaml +85 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/task-graph.yaml +14 -0
- package/examples/l5-report-coms-process-definition.html +322 -0
- package/package.json +1 -1
- package/skills/agent-worker/references/agent-worker-operator.md +84 -0
- package/skills/frontend-implementation/references/node-contracts.md +2 -2
- package/skills/loop-agent/SKILL.md +8 -8
- package/skills/loop-agent/references/command-reference.md +14 -2
- package/skills/loop-agent/references/harness-policy.md +22 -0
- package/skills/loop-agent/references/task-workflow.md +5 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, statSync, } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { readStaticPomFile, repoRelativePosix } from "./pom-static.js";
|
|
5
|
+
import { MAVEN_CACHE_SCHEMA_VERSION, MAVEN_PLANNER_VERSION, MavenPlanError, } from "./types.js";
|
|
6
|
+
const SKIP_DIR_NAMES = new Set([
|
|
7
|
+
".git",
|
|
8
|
+
".harness",
|
|
9
|
+
"node_modules",
|
|
10
|
+
"target",
|
|
11
|
+
"dist",
|
|
12
|
+
"build",
|
|
13
|
+
".idea",
|
|
14
|
+
".vscode",
|
|
15
|
+
"coverage",
|
|
16
|
+
".turbo",
|
|
17
|
+
".next",
|
|
18
|
+
]);
|
|
19
|
+
const WRAPPER_BASENAMES = new Set([
|
|
20
|
+
"mvnw",
|
|
21
|
+
"mvnw.cmd",
|
|
22
|
+
"mvnw.bat",
|
|
23
|
+
"maven-wrapper.properties",
|
|
24
|
+
"maven-wrapper.jar",
|
|
25
|
+
]);
|
|
26
|
+
function toPosix(value) {
|
|
27
|
+
return value.replace(/\\/g, "/");
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Walk repository files using Dirent and do not follow directory/file symlinks.
|
|
31
|
+
* Captures every successfully scanned directory in the same pass so cache-hit
|
|
32
|
+
* checks can detect a POM or wrapper added below a previously empty directory.
|
|
33
|
+
*/
|
|
34
|
+
function walkFiles(root) {
|
|
35
|
+
const files = [];
|
|
36
|
+
const directories = [];
|
|
37
|
+
const stack = [root];
|
|
38
|
+
while (stack.length > 0) {
|
|
39
|
+
const current = stack.pop();
|
|
40
|
+
let entries;
|
|
41
|
+
try {
|
|
42
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
directories.push({
|
|
48
|
+
path: current,
|
|
49
|
+
entryNames: entries
|
|
50
|
+
.filter((entry) => !entry.isSymbolicLink() && !SKIP_DIR_NAMES.has(entry.name))
|
|
51
|
+
.map((entry) => entry.name)
|
|
52
|
+
.sort(),
|
|
53
|
+
});
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
if (SKIP_DIR_NAMES.has(entry.name))
|
|
56
|
+
continue;
|
|
57
|
+
// Never follow symlinks (dir or file).
|
|
58
|
+
if (entry.isSymbolicLink())
|
|
59
|
+
continue;
|
|
60
|
+
const absolute = path.join(current, entry.name);
|
|
61
|
+
// Double-check with lstat in case platform Dirent is incomplete.
|
|
62
|
+
try {
|
|
63
|
+
const lst = lstatSync(absolute);
|
|
64
|
+
if (lst.isSymbolicLink())
|
|
65
|
+
continue;
|
|
66
|
+
if (lst.isDirectory()) {
|
|
67
|
+
stack.push(absolute);
|
|
68
|
+
}
|
|
69
|
+
else if (lst.isFile()) {
|
|
70
|
+
files.push(absolute);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { files, directories };
|
|
79
|
+
}
|
|
80
|
+
function isPomFile(filePath) {
|
|
81
|
+
return path.basename(filePath).toLowerCase() === "pom.xml";
|
|
82
|
+
}
|
|
83
|
+
function isWrapperFile(filePath) {
|
|
84
|
+
const base = path.basename(filePath).toLowerCase();
|
|
85
|
+
if (WRAPPER_BASENAMES.has(base))
|
|
86
|
+
return true;
|
|
87
|
+
const posix = toPosix(filePath).toLowerCase();
|
|
88
|
+
return posix.endsWith("/.mvn/wrapper/maven-wrapper.properties");
|
|
89
|
+
}
|
|
90
|
+
function contentSha(absolutePath) {
|
|
91
|
+
const buf = readFileSync(absolutePath);
|
|
92
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
93
|
+
}
|
|
94
|
+
function resolveModulePomPath(repoRoot, parentDir, moduleDecl) {
|
|
95
|
+
const absDir = path.resolve(repoRoot, parentDir, moduleDecl);
|
|
96
|
+
const candidates = [
|
|
97
|
+
path.join(absDir, "pom.xml"),
|
|
98
|
+
// moduleDecl may already point at a pom file
|
|
99
|
+
absDir.endsWith("pom.xml") ? absDir : null,
|
|
100
|
+
].filter((value) => Boolean(value));
|
|
101
|
+
for (const candidate of candidates) {
|
|
102
|
+
try {
|
|
103
|
+
const lst = lstatSync(candidate);
|
|
104
|
+
if (lst.isSymbolicLink())
|
|
105
|
+
continue;
|
|
106
|
+
if (lst.isFile()) {
|
|
107
|
+
return repoRelativePosix(repoRoot, candidate);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// try next
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Build reactor graphs with transitive nested aggregator membership.
|
|
118
|
+
* -pl coordinates are module paths relative to the reactor root, not bare artifactIds.
|
|
119
|
+
*/
|
|
120
|
+
function buildReactors(repoRoot, nodes) {
|
|
121
|
+
const byPom = new Map(nodes.map((node) => [node.pomPath, node]));
|
|
122
|
+
const reactors = [];
|
|
123
|
+
// Prefer outermost reactor root for each member when nested.
|
|
124
|
+
const reactorRootByPomPath = {};
|
|
125
|
+
const rootDepth = new Map();
|
|
126
|
+
for (const node of nodes) {
|
|
127
|
+
if (node.modules.length === 0)
|
|
128
|
+
continue;
|
|
129
|
+
const modulePomByArtifactId = {};
|
|
130
|
+
const artifactIdByPomPath = {
|
|
131
|
+
[node.pomPath]: node.artifactId,
|
|
132
|
+
};
|
|
133
|
+
const modulePathByPomPath = {};
|
|
134
|
+
const pomPaths = new Set([node.pomPath]);
|
|
135
|
+
const queue = node.modules.map((moduleDecl) => ({
|
|
136
|
+
parentNode: node,
|
|
137
|
+
moduleDecl,
|
|
138
|
+
pathFromRoot: toPosix(moduleDecl).replace(/\/+$/, ""),
|
|
139
|
+
stack: [node.pomPath],
|
|
140
|
+
}));
|
|
141
|
+
const seenEdges = new Set();
|
|
142
|
+
while (queue.length > 0) {
|
|
143
|
+
const item = queue.shift();
|
|
144
|
+
const edgeKey = `${item.parentNode.pomPath}=>${item.moduleDecl}`;
|
|
145
|
+
if (seenEdges.has(edgeKey))
|
|
146
|
+
continue;
|
|
147
|
+
seenEdges.add(edgeKey);
|
|
148
|
+
if (item.stack.includes(item.parentNode.pomPath) && item.stack.length > 1) {
|
|
149
|
+
// stack already contains parent from deeper cycle checks below
|
|
150
|
+
}
|
|
151
|
+
const modulePom = resolveModulePomPath(repoRoot, item.parentNode.dir, item.moduleDecl);
|
|
152
|
+
if (!modulePom) {
|
|
153
|
+
throw new MavenPlanError(`Reactor module path cannot be resolved statically from ${item.parentNode.pomPath}: ${item.moduleDecl}`, {
|
|
154
|
+
code: "maven-reactor-module-missing",
|
|
155
|
+
candidates: [item.parentNode.pomPath, item.moduleDecl],
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (item.stack.includes(modulePom)) {
|
|
159
|
+
throw new MavenPlanError(`Cyclic reactor module relationship detected involving ${modulePom}`, {
|
|
160
|
+
code: "maven-reactor-cycle",
|
|
161
|
+
candidates: [...item.stack, modulePom],
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
const moduleNode = byPom.get(modulePom);
|
|
165
|
+
if (!moduleNode) {
|
|
166
|
+
throw new MavenPlanError(`Reactor module POM was not discovered: ${modulePom}`, { code: "maven-reactor-module-missing", candidates: [modulePom] });
|
|
167
|
+
}
|
|
168
|
+
// module path relative to reactor root (for -pl)
|
|
169
|
+
const modulePath = item.pathFromRoot
|
|
170
|
+
.replace(/\/pom\.xml$/i, "")
|
|
171
|
+
.replace(/^\.\//, "");
|
|
172
|
+
if (modulePomByArtifactId[moduleNode.artifactId] &&
|
|
173
|
+
modulePomByArtifactId[moduleNode.artifactId] !== modulePom) {
|
|
174
|
+
throw new MavenPlanError(`Duplicate artifactId '${moduleNode.artifactId}' in reactor ${node.pomPath}`, {
|
|
175
|
+
code: "maven-reactor-duplicate-artifactId",
|
|
176
|
+
candidates: [
|
|
177
|
+
modulePomByArtifactId[moduleNode.artifactId],
|
|
178
|
+
modulePom,
|
|
179
|
+
],
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
modulePomByArtifactId[moduleNode.artifactId] = modulePom;
|
|
183
|
+
artifactIdByPomPath[modulePom] = moduleNode.artifactId;
|
|
184
|
+
modulePathByPomPath[modulePom] = modulePath;
|
|
185
|
+
pomPaths.add(modulePom);
|
|
186
|
+
// Recurse into nested aggregator modules.
|
|
187
|
+
for (const childDecl of moduleNode.modules) {
|
|
188
|
+
const childPath = toPosix(path.posix.join(modulePath, toPosix(childDecl))).replace(/\\/g, "/");
|
|
189
|
+
queue.push({
|
|
190
|
+
parentNode: moduleNode,
|
|
191
|
+
moduleDecl: childDecl,
|
|
192
|
+
pathFromRoot: childPath,
|
|
193
|
+
stack: [...item.stack, modulePom],
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const reactor = {
|
|
198
|
+
rootPomPath: node.pomPath,
|
|
199
|
+
modulePomByArtifactId,
|
|
200
|
+
artifactIdByPomPath,
|
|
201
|
+
// Store module path coordinates for -pl via artifactIdByPomPath values when
|
|
202
|
+
// consumers need paths: expose via modulePathByPomPath on the side channel.
|
|
203
|
+
pomPaths: [...pomPaths].sort(),
|
|
204
|
+
// Extended field kept optional for schema compatibility via cast below.
|
|
205
|
+
...(Object.keys(modulePathByPomPath).length > 0
|
|
206
|
+
? { modulePathByPomPath }
|
|
207
|
+
: {}),
|
|
208
|
+
};
|
|
209
|
+
// Attach modulePathByPomPath for planners (types extended separately).
|
|
210
|
+
reactor
|
|
211
|
+
.modulePathByPomPath = modulePathByPomPath;
|
|
212
|
+
reactors.push(reactor);
|
|
213
|
+
// Register membership: prefer outermost (shallower path depth) reactor root.
|
|
214
|
+
const depth = node.pomPath.split("/").length;
|
|
215
|
+
for (const member of pomPaths) {
|
|
216
|
+
const existing = reactorRootByPomPath[member];
|
|
217
|
+
if (!existing) {
|
|
218
|
+
reactorRootByPomPath[member] = node.pomPath;
|
|
219
|
+
rootDepth.set(member, depth);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const existingDepth = rootDepth.get(member) ?? Number.MAX_SAFE_INTEGER;
|
|
223
|
+
if (depth < existingDepth) {
|
|
224
|
+
reactorRootByPomPath[member] = node.pomPath;
|
|
225
|
+
rootDepth.set(member, depth);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return { reactors, reactorRootByPomPath };
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Statically discover all POMs and wrappers under the repository.
|
|
233
|
+
* Never executes Maven goals. Does not follow symlinks.
|
|
234
|
+
*/
|
|
235
|
+
/** Test/probe counter: increments only on full walk+parse builds. */
|
|
236
|
+
let fullBuildCount = 0;
|
|
237
|
+
export function getMavenFullBuildCount() {
|
|
238
|
+
return fullBuildCount;
|
|
239
|
+
}
|
|
240
|
+
export function resetMavenFullBuildCount() {
|
|
241
|
+
fullBuildCount = 0;
|
|
242
|
+
}
|
|
243
|
+
export function buildMavenWorkspaceGraph(repoRoot) {
|
|
244
|
+
if (!existsSync(repoRoot)) {
|
|
245
|
+
throw new MavenPlanError(`Repository root does not exist: ${repoRoot}`, {
|
|
246
|
+
code: "maven-repo-missing",
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
fullBuildCount += 1;
|
|
250
|
+
const walk = walkFiles(repoRoot);
|
|
251
|
+
const allFiles = walk.files;
|
|
252
|
+
const pomAbs = allFiles.filter(isPomFile).sort();
|
|
253
|
+
const wrapperAbs = allFiles.filter(isWrapperFile).sort();
|
|
254
|
+
const nodes = [];
|
|
255
|
+
const fingerprintParts = [];
|
|
256
|
+
const fileMeta = [];
|
|
257
|
+
const dirMeta = walk.directories.map((directory) => ({
|
|
258
|
+
path: directory.path === repoRoot
|
|
259
|
+
? ""
|
|
260
|
+
: repoRelativePosix(repoRoot, directory.path),
|
|
261
|
+
entryNames: directory.entryNames,
|
|
262
|
+
}));
|
|
263
|
+
for (const abs of pomAbs) {
|
|
264
|
+
const pomPath = repoRelativePosix(repoRoot, abs);
|
|
265
|
+
const fields = readStaticPomFile(abs);
|
|
266
|
+
const st = statSync(abs);
|
|
267
|
+
const dir = toPosix(path.posix.dirname(pomPath));
|
|
268
|
+
nodes.push({
|
|
269
|
+
pomPath,
|
|
270
|
+
dir: dir === "." ? "" : dir,
|
|
271
|
+
artifactId: fields.artifactId,
|
|
272
|
+
groupId: fields.groupId,
|
|
273
|
+
packaging: fields.packaging,
|
|
274
|
+
modules: fields.modules,
|
|
275
|
+
parentRelativePath: fields.parentRelativePath,
|
|
276
|
+
});
|
|
277
|
+
const sha = contentSha(abs);
|
|
278
|
+
fingerprintParts.push(`pom:${pomPath}:${sha}`);
|
|
279
|
+
fileMeta.push({
|
|
280
|
+
path: pomPath,
|
|
281
|
+
size: st.size,
|
|
282
|
+
mtimeMs: st.mtimeMs,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
const wrapperPaths = [];
|
|
286
|
+
for (const abs of wrapperAbs) {
|
|
287
|
+
const rel = repoRelativePosix(repoRoot, abs);
|
|
288
|
+
wrapperPaths.push(rel);
|
|
289
|
+
const st = statSync(abs);
|
|
290
|
+
const sha = contentSha(abs);
|
|
291
|
+
fingerprintParts.push(`wrapper:${rel}:${sha}`);
|
|
292
|
+
fileMeta.push({
|
|
293
|
+
path: rel,
|
|
294
|
+
size: st.size,
|
|
295
|
+
mtimeMs: st.mtimeMs,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
fingerprintParts.sort();
|
|
299
|
+
const fingerprint = createHash("sha256")
|
|
300
|
+
.update(fingerprintParts.join("\n"))
|
|
301
|
+
.update(`\nplanner:${MAVEN_PLANNER_VERSION}`)
|
|
302
|
+
.update(`\nschema:${MAVEN_CACHE_SCHEMA_VERSION}`)
|
|
303
|
+
.digest("hex");
|
|
304
|
+
const { reactors, reactorRootByPomPath } = buildReactors(repoRoot, nodes);
|
|
305
|
+
return {
|
|
306
|
+
schemaVersion: MAVEN_CACHE_SCHEMA_VERSION,
|
|
307
|
+
plannerVersion: MAVEN_PLANNER_VERSION,
|
|
308
|
+
fingerprint,
|
|
309
|
+
pomPaths: nodes.map((n) => n.pomPath).sort(),
|
|
310
|
+
wrapperPaths: wrapperPaths.sort(),
|
|
311
|
+
nodes,
|
|
312
|
+
reactors,
|
|
313
|
+
reactorRootByPomPath,
|
|
314
|
+
manifest: {
|
|
315
|
+
files: fileMeta.sort((a, b) => a.path.localeCompare(b.path)),
|
|
316
|
+
directories: dirMeta.sort((a, b) => a.path.localeCompare(b.path)),
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
export function computeWorkspaceFingerprint(repoRoot) {
|
|
321
|
+
return buildMavenWorkspaceGraph(repoRoot).fingerprint;
|
|
322
|
+
}
|
package/dist/worker/cli.js
CHANGED
|
@@ -31,8 +31,11 @@ import { approveFollowUp } from "./follow-up/approve.js";
|
|
|
31
31
|
import { prepareFeatureDelivery } from "./delivery/package.js";
|
|
32
32
|
import { previewFeatureCloseout, renderCloseoutPreview, } from "./closeout/preview.js";
|
|
33
33
|
import { applyFeatureCloseout } from "./closeout/apply.js";
|
|
34
|
+
import { advanceFeature, renderFeatureAdvance, } from "./feature/advance.js";
|
|
35
|
+
import { diagnoseFeature, renderFeatureDoctor, } from "./feature/doctor.js";
|
|
36
|
+
import { FEATURE_SCAFFOLD_TEMPLATES, ScaffoldError, scaffoldFeaturePacket, } from "./feature/scaffold.js";
|
|
34
37
|
import { projectMonthlyMetrics } from "./metrics/projector.js";
|
|
35
|
-
import { runFeatureFinalVerification } from "./delivery/final-verification.js";
|
|
38
|
+
import { defaultFeatureEvidencePaths, runFeatureFinalVerification, } from "./delivery/final-verification.js";
|
|
36
39
|
import { advanceGitCheckpoint } from "./delivery/git-transaction.js";
|
|
37
40
|
export function buildAgentWorkerProgram() {
|
|
38
41
|
const program = new Command();
|
|
@@ -41,7 +44,8 @@ export function buildAgentWorkerProgram() {
|
|
|
41
44
|
.description("Local product-line worker utilities for TaskSpec validation")
|
|
42
45
|
.version(readPackageVersion(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))))) ?? "0.0.0", "-V, --version", "display version")
|
|
43
46
|
.showHelpAfterError()
|
|
44
|
-
.showSuggestionAfterError()
|
|
47
|
+
.showSuggestionAfterError()
|
|
48
|
+
.enablePositionalOptions();
|
|
45
49
|
const task = program.command("task").description("TaskSpec utilities");
|
|
46
50
|
const batch = program
|
|
47
51
|
.command("batch")
|
|
@@ -244,6 +248,125 @@ export function buildAgentWorkerProgram() {
|
|
|
244
248
|
if (result.status === "failed" || result.status === "needs-action")
|
|
245
249
|
process.exitCode = 1;
|
|
246
250
|
});
|
|
251
|
+
feature
|
|
252
|
+
.command("advance")
|
|
253
|
+
.requiredOption("--feature-dir <dir>", "Feature directory containing acceptance.yaml and tasks/")
|
|
254
|
+
.requiredOption("--repo <repo-root>", "Target repo root")
|
|
255
|
+
.option("--task-id <id>", "Final verification TaskSpec id (default: resolve FINAL-VERIFY* from Feature review)")
|
|
256
|
+
.option("--loop-agent-bin <bin>", "loop-agent binary", "loop-agent")
|
|
257
|
+
.option("--evidence-mode <mode>", "Evidence aggregate for verify-final: auto, qa-execute, or typed", "auto")
|
|
258
|
+
.option("--qa-evidence <path>", "Override QA pass evidence path")
|
|
259
|
+
.option("--final-verification <path>", "Override final verification evidence path")
|
|
260
|
+
.option("--waivers <path>", "Repo-local acceptance waiver decisions JSON")
|
|
261
|
+
.option("--dry-run", "Run verify-final + delivery dry-run + closeout preview without Delivery writes or closeout apply")
|
|
262
|
+
.option("--apply", "After ready closeout preview, apply Feature Closeout (requires --owner)")
|
|
263
|
+
.option("--owner <owner>", "Closeout owner when using --apply")
|
|
264
|
+
.option("--expected-controller-version <version>", "Exact expected controller version")
|
|
265
|
+
.option("--expected-controller-fingerprint <value>", "Exact expected controller fingerprint sha256:<hex>")
|
|
266
|
+
.option("--json", "Emit stable JSON")
|
|
267
|
+
.description("Orchestrate ADR 0007 delivery path: verify-final → delivery → closeout preview[/apply]")
|
|
268
|
+
.action(async (options) => {
|
|
269
|
+
if (options.evidenceMode !== "auto" &&
|
|
270
|
+
options.evidenceMode !== "qa-execute" &&
|
|
271
|
+
options.evidenceMode !== "typed") {
|
|
272
|
+
throw new Error("feature advance --evidence-mode must be auto, qa-execute, or typed");
|
|
273
|
+
}
|
|
274
|
+
const repoRoot = path.resolve(options.repo);
|
|
275
|
+
const client = new LoopAgentClient({
|
|
276
|
+
loopAgentBin: options.loopAgentBin,
|
|
277
|
+
artifactRoot: path.join(getTaskPoolRoot(repoRoot), "artifacts", `feature-advance-${Date.now()}`),
|
|
278
|
+
resolveIdentity: true,
|
|
279
|
+
});
|
|
280
|
+
const result = await advanceFeature({
|
|
281
|
+
featureDir: path.resolve(options.featureDir),
|
|
282
|
+
repoRoot,
|
|
283
|
+
...(options.taskId ? { taskId: options.taskId } : {}),
|
|
284
|
+
client,
|
|
285
|
+
controllerIdentity: client.getIdentity(),
|
|
286
|
+
controllerExpectation: buildIdentityExpectation(options),
|
|
287
|
+
evidenceMode: options.evidenceMode,
|
|
288
|
+
...(options.qaEvidence
|
|
289
|
+
? { qaEvidencePath: options.qaEvidence }
|
|
290
|
+
: {}),
|
|
291
|
+
...(options.finalVerification
|
|
292
|
+
? { finalVerificationPath: options.finalVerification }
|
|
293
|
+
: {}),
|
|
294
|
+
...(options.waivers ? { waiverPath: options.waivers } : {}),
|
|
295
|
+
dryRun: options.dryRun ?? false,
|
|
296
|
+
apply: options.apply ?? false,
|
|
297
|
+
...(options.owner ? { owner: options.owner } : {}),
|
|
298
|
+
});
|
|
299
|
+
process.stdout.write(options.json
|
|
300
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
301
|
+
: renderFeatureAdvance(result));
|
|
302
|
+
if (result.status === "failed" ||
|
|
303
|
+
result.status === "blocked") {
|
|
304
|
+
process.exitCode = 1;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
feature
|
|
308
|
+
.command("doctor")
|
|
309
|
+
.requiredOption("--feature-dir <dir>", "Feature directory containing acceptance.yaml and tasks/")
|
|
310
|
+
.requiredOption("--repo <repo-root>", "Target repo root")
|
|
311
|
+
.option("--json", "Emit stable JSON")
|
|
312
|
+
.description("Read-only Feature delivery readiness diagnosis (evidence, pool states, dirty worktree)")
|
|
313
|
+
.action(async (options) => {
|
|
314
|
+
const result = await diagnoseFeature({
|
|
315
|
+
featureDir: path.resolve(options.featureDir),
|
|
316
|
+
repoRoot: path.resolve(options.repo),
|
|
317
|
+
});
|
|
318
|
+
process.stdout.write(options.json
|
|
319
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
320
|
+
: renderFeatureDoctor(result));
|
|
321
|
+
if (result.status === "blocked")
|
|
322
|
+
process.exitCode = 1;
|
|
323
|
+
});
|
|
324
|
+
feature
|
|
325
|
+
.command("scaffold")
|
|
326
|
+
.requiredOption("--repo <repo-root>", "Target repo root")
|
|
327
|
+
.option("--template <id>", `Locked template: ${FEATURE_SCAFFOLD_TEMPLATES.join(" | ")}`)
|
|
328
|
+
.option("--feature-id <id>", "Feature id (F-YYYY-NNN)")
|
|
329
|
+
.option("--title <text>", "Human title")
|
|
330
|
+
.option("--description <text>", "Optional longer description")
|
|
331
|
+
.option("--be-path <pattern>", "Repo-relative backend allowed path (repeatable)", (value, previous) => [...previous, value], [])
|
|
332
|
+
.option("--fe-path <pattern>", "Repo-relative frontend allowed path (repeatable)", (value, previous) => [...previous, value], [])
|
|
333
|
+
.option("--contract-path <path>", "fe-with-api contract path (default docs/contracts/<feature-id>/api.md)")
|
|
334
|
+
.option("--verify-command <command>", "Explicit verify command for implementation tasks (repeatable)", (value, previous) => [...previous, value], [])
|
|
335
|
+
.option("--batch <file>", "YAML/JSON batch list (mutually exclusive with single-feature flags)")
|
|
336
|
+
.option("--dry-run", "Parse/render/validate without writing features/")
|
|
337
|
+
.option("--json", "Emit stable JSON only")
|
|
338
|
+
.description("Deterministically scaffold a Feature Packet (no model, no Task Pool, no fullstack-v1 default)")
|
|
339
|
+
.action(async (options) => {
|
|
340
|
+
try {
|
|
341
|
+
const result = options.batch
|
|
342
|
+
? await scaffoldFeaturePacket({
|
|
343
|
+
repoRoot: path.resolve(options.repo),
|
|
344
|
+
batchFile: path.resolve(options.batch),
|
|
345
|
+
dryRun: options.dryRun ?? false,
|
|
346
|
+
})
|
|
347
|
+
: await scaffoldFeaturePacket({
|
|
348
|
+
repoRoot: path.resolve(options.repo),
|
|
349
|
+
dryRun: options.dryRun ?? false,
|
|
350
|
+
feature: {
|
|
351
|
+
featureId: options.featureId ?? "",
|
|
352
|
+
title: options.title ?? "",
|
|
353
|
+
template: (options.template ??
|
|
354
|
+
""),
|
|
355
|
+
description: options.description,
|
|
356
|
+
bePaths: options.bePath,
|
|
357
|
+
fePaths: options.fePath,
|
|
358
|
+
contractPath: options.contractPath,
|
|
359
|
+
verifyCommands: options.verifyCommand,
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
process.stdout.write(options.json
|
|
363
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
364
|
+
: renderFeatureScaffold(result));
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
handleScaffoldCliError(error, options.json ?? false);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
247
370
|
feature
|
|
248
371
|
.command("verify-final")
|
|
249
372
|
.requiredOption("--feature-dir <dir>", "Feature directory containing acceptance.yaml and tasks/")
|
|
@@ -283,18 +406,30 @@ export function buildAgentWorkerProgram() {
|
|
|
283
406
|
.command("delivery")
|
|
284
407
|
.requiredOption("--feature-dir <dir>", "Feature directory containing acceptance.yaml and tasks/")
|
|
285
408
|
.requiredOption("--repo <repo-root>", "Target repo root")
|
|
286
|
-
.
|
|
287
|
-
.
|
|
409
|
+
.option("--qa-evidence <path>", "Repo-local QA pass evidence (default: .harness/task-pool/evidence/<featureId>/qa-pass.json from verify-final)")
|
|
410
|
+
.option("--final-verification <path>", "Repo-local final verification evidence (default: .../final-verification.json from verify-final)")
|
|
288
411
|
.option("--waivers <path>", "Repo-local acceptance waiver decisions JSON")
|
|
289
412
|
.option("--dry-run", "Validate and show the Delivery Package plan without writes")
|
|
290
413
|
.option("--json", "Emit stable JSON")
|
|
291
414
|
.description("Validate checkpointed Feature evidence and prepare a local Delivery Package")
|
|
292
415
|
.action(async (options) => {
|
|
416
|
+
const featureDir = path.resolve(options.featureDir);
|
|
417
|
+
const repoRoot = path.resolve(options.repo);
|
|
418
|
+
// ADR 0007: default to verify-final evidence locations when omitted.
|
|
419
|
+
let qaEvidencePath = options.qaEvidence;
|
|
420
|
+
let finalVerificationPath = options.finalVerification;
|
|
421
|
+
if (!qaEvidencePath || !finalVerificationPath) {
|
|
422
|
+
const validation = await validateFeatureTaskGraph(featureDir);
|
|
423
|
+
const defaults = defaultFeatureEvidencePaths(validation.featureId);
|
|
424
|
+
qaEvidencePath = qaEvidencePath ?? defaults.qaEvidencePath;
|
|
425
|
+
finalVerificationPath =
|
|
426
|
+
finalVerificationPath ?? defaults.finalVerificationPath;
|
|
427
|
+
}
|
|
293
428
|
const result = await prepareFeatureDelivery({
|
|
294
|
-
featureDir
|
|
295
|
-
repoRoot
|
|
296
|
-
qaEvidencePath
|
|
297
|
-
finalVerificationPath
|
|
429
|
+
featureDir,
|
|
430
|
+
repoRoot,
|
|
431
|
+
qaEvidencePath,
|
|
432
|
+
finalVerificationPath,
|
|
298
433
|
...(options.waivers ? { waiverPath: options.waivers } : {}),
|
|
299
434
|
dryRun: options.dryRun ?? false,
|
|
300
435
|
});
|
|
@@ -593,18 +728,17 @@ export function buildAgentWorkerProgram() {
|
|
|
593
728
|
process.stdout.write(`${JSON.stringify(snapshot)}\n`);
|
|
594
729
|
});
|
|
595
730
|
consoleCmd
|
|
596
|
-
.
|
|
597
|
-
.
|
|
731
|
+
.description("Loop Operator Console (local); bare `agent-worker console` starts it directly (repo defaults to current working directory, port defaults to 8790)")
|
|
732
|
+
.enablePositionalOptions()
|
|
733
|
+
.passThroughOptions()
|
|
734
|
+
.option("--repo <repo-root>", "Target repo root (default: current working directory)", process.cwd())
|
|
598
735
|
.option("--port <port>", "HTTP port", "8790")
|
|
599
736
|
.option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no network auth)", "127.0.0.1")
|
|
600
737
|
.option("--debug", "Log request source/method/path/status to stderr")
|
|
601
738
|
.option("--app-data <path>", "Override Console application-data root (Draft/Operation/confirmation)")
|
|
602
|
-
.description("Start Loop Operator Console HTTP server (Operate + Inspect; static SPA + Operator API)")
|
|
603
739
|
.action(async (options) => {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
const server = await createConsoleServer({
|
|
607
|
-
repoRoot,
|
|
740
|
+
await runConsoleServe({
|
|
741
|
+
repoRoot: path.resolve(options.repo),
|
|
608
742
|
host: options.host,
|
|
609
743
|
port: Number.parseInt(options.port, 10),
|
|
610
744
|
debug: options.debug === true,
|
|
@@ -612,13 +746,25 @@ export function buildAgentWorkerProgram() {
|
|
|
612
746
|
? path.resolve(options.appData)
|
|
613
747
|
: undefined,
|
|
614
748
|
});
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
749
|
+
});
|
|
750
|
+
consoleCmd
|
|
751
|
+
.command("serve")
|
|
752
|
+
.option("--repo <repo-root>", "Target repo root (default: current working directory)", process.cwd())
|
|
753
|
+
.option("--port <port>", "HTTP port", "8790")
|
|
754
|
+
.option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no network auth)", "127.0.0.1")
|
|
755
|
+
.option("--debug", "Log request source/method/path/status to stderr")
|
|
756
|
+
.option("--app-data <path>", "Override Console application-data root (Draft/Operation/confirmation)")
|
|
757
|
+
.description("Start Loop Operator Console HTTP server (compat entry; equivalent to bare `agent-worker console`)")
|
|
758
|
+
.action(async (options) => {
|
|
759
|
+
await runConsoleServe({
|
|
760
|
+
repoRoot: path.resolve(options.repo),
|
|
761
|
+
host: options.host,
|
|
762
|
+
port: Number.parseInt(options.port, 10),
|
|
763
|
+
debug: options.debug === true,
|
|
764
|
+
appDataRoot: options.appData
|
|
765
|
+
? path.resolve(options.appData)
|
|
766
|
+
: undefined,
|
|
620
767
|
});
|
|
621
|
-
await server.close();
|
|
622
768
|
});
|
|
623
769
|
consoleCmd
|
|
624
770
|
.command("doctor")
|
|
@@ -693,11 +839,80 @@ export async function main(argv = process.argv) {
|
|
|
693
839
|
async function readTaskYaml(taskSpecPath) {
|
|
694
840
|
return YAML.parse(await readFile(taskSpecPath, "utf-8"));
|
|
695
841
|
}
|
|
842
|
+
/** Shared console serve entry used by both bare `console` and `console serve`. */
|
|
843
|
+
async function runConsoleServe(options) {
|
|
844
|
+
const { createConsoleServer } = await import("./console/server.js");
|
|
845
|
+
try {
|
|
846
|
+
const server = await createConsoleServer({
|
|
847
|
+
repoRoot: options.repoRoot,
|
|
848
|
+
host: options.host,
|
|
849
|
+
port: options.port,
|
|
850
|
+
debug: options.debug === true,
|
|
851
|
+
appDataRoot: options.appDataRoot,
|
|
852
|
+
});
|
|
853
|
+
process.stdout.write(`${server.url}\n`);
|
|
854
|
+
await new Promise((resolve) => {
|
|
855
|
+
const shutdown = () => resolve();
|
|
856
|
+
process.once("SIGINT", shutdown);
|
|
857
|
+
process.once("SIGTERM", shutdown);
|
|
858
|
+
});
|
|
859
|
+
await server.close();
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
863
|
+
const code = error?.code;
|
|
864
|
+
const isPortInUse = code === "EADDRINUSE" ||
|
|
865
|
+
/listen EADDRINUSE/i.test(detail) ||
|
|
866
|
+
/address already in use/i.test(detail);
|
|
867
|
+
if (isPortInUse) {
|
|
868
|
+
process.stderr.write(`端口 ${options.port} 已被占用,请使用 --port <port> 指定其它端口后重试。\n`);
|
|
869
|
+
process.exit(1);
|
|
870
|
+
}
|
|
871
|
+
process.stderr.write(`${detail}\n`);
|
|
872
|
+
process.exit(1);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
696
875
|
function handlePoolRecoveryCliError(error) {
|
|
697
876
|
const detail = error instanceof Error ? error.message : String(error);
|
|
698
877
|
process.stderr.write(`${detail}\n`);
|
|
699
878
|
process.exitCode = 1;
|
|
700
879
|
}
|
|
880
|
+
function handleScaffoldCliError(error, json) {
|
|
881
|
+
const code = error instanceof ScaffoldError ? error.code : "write-failed";
|
|
882
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
883
|
+
const details = error instanceof ScaffoldError ? error.details : undefined;
|
|
884
|
+
if (json) {
|
|
885
|
+
process.stdout.write(`${JSON.stringify({
|
|
886
|
+
schemaVersion: 1,
|
|
887
|
+
ok: false,
|
|
888
|
+
code,
|
|
889
|
+
message,
|
|
890
|
+
...(details !== undefined ? { details } : {}),
|
|
891
|
+
}, null, 2)}\n`);
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
process.stderr.write(`${message}\n`);
|
|
895
|
+
}
|
|
896
|
+
process.exitCode = 1;
|
|
897
|
+
}
|
|
898
|
+
function renderFeatureScaffold(result) {
|
|
899
|
+
const lines = [
|
|
900
|
+
result.dryRun
|
|
901
|
+
? `Feature scaffold dry-run: ${result.features.length} packet(s) validated`
|
|
902
|
+
: `Feature scaffold wrote ${result.features.length} packet(s)`,
|
|
903
|
+
"",
|
|
904
|
+
];
|
|
905
|
+
for (const feature of result.features) {
|
|
906
|
+
lines.push(`- ${feature.featureId} [${feature.template}] → ${feature.featureDir}`);
|
|
907
|
+
}
|
|
908
|
+
if (!result.dryRun && result.writtenPaths.length > 0) {
|
|
909
|
+
lines.push("", "Written:");
|
|
910
|
+
for (const p of result.writtenPaths)
|
|
911
|
+
lines.push(` ${p}`);
|
|
912
|
+
}
|
|
913
|
+
lines.push("");
|
|
914
|
+
return `${lines.join("\n")}\n`;
|
|
915
|
+
}
|
|
701
916
|
function handleFollowUpCliError(error, json, action) {
|
|
702
917
|
const detail = error instanceof Error ? error.message : String(error);
|
|
703
918
|
if (!json)
|
|
@@ -16,6 +16,18 @@ import { preflightTargetRepo } from "../preflight.js";
|
|
|
16
16
|
import { gitTransactionRecordSchema, transactionRecordPath } from "./git-transaction.js";
|
|
17
17
|
import { buildFeatureVerificationBundle, writeFeatureVerificationBundle, } from "./verification-bundle.js";
|
|
18
18
|
const execFileAsync = promisify(execFile);
|
|
19
|
+
/**
|
|
20
|
+
* Canonical Delivery evidence locations written by `feature verify-final`
|
|
21
|
+
* (ADR 0007). Paths are repo-relative with forward slashes.
|
|
22
|
+
*/
|
|
23
|
+
export function defaultFeatureEvidencePaths(featureId) {
|
|
24
|
+
const base = `.harness/task-pool/evidence/${featureId}`;
|
|
25
|
+
return {
|
|
26
|
+
qaEvidencePath: `${base}/qa-pass.json`,
|
|
27
|
+
finalVerificationPath: `${base}/final-verification.json`,
|
|
28
|
+
bundleEvidencePath: `${base}/feature-verification-bundle.json`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
19
31
|
export async function runFeatureFinalVerification(input) {
|
|
20
32
|
const dependencies = {
|
|
21
33
|
preflight: preflightTargetRepo,
|