@tea-agent/loop-agent 0.24.11-beta.0 → 0.25.1
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 +53 -4
- package/README.md +4 -4
- package/dist/application/dag/generate-task-dag.js +25 -8
- package/dist/commands/dag-init-hybrid.js +3 -2
- package/dist/commands/delegate.js +11 -11
- package/dist/commands/init.js +29 -14
- package/dist/commands/instructions.js +1 -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 +362 -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/task/read-model.js +3 -7
- package/dist/task/runtime.js +12 -3
- 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/run-task/run-task.js +18 -2
- package/dist/worker/runner/run-ready.js +41 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +300 -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 +224 -39
- package/dist/workflows/dag/node-execution.js +3 -2
- package/dist/workflows/dag/reconcile-run.js +24 -0
- package/dist/workflows/dag/types.js +57 -0
- package/dist/workflows/dag/validate.js +19 -2
- package/dist/workflows/loop/actions/dag-action.js +2 -1
- 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/init-surface.manifest.json +3 -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 +14 -16
- package/skills/loop-agent/references/command-reference.md +33 -12
- package/skills/loop-agent/references/harness-policy.md +28 -6
- package/skills/loop-agent/references/hybrid-dag.md +8 -7
- package/skills/loop-agent/references/post-implementation-and-patterns.md +19 -9
- package/skills/loop-agent/references/source-and-plan-practice.md +161 -0
- package/skills/loop-agent/references/task-workflow.md +13 -5
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,
|
|
@@ -101,11 +101,12 @@ export async function finalizeGitTask(transaction, outcome) {
|
|
|
101
101
|
await assertTransactionPosition(transaction.repoRoot, current);
|
|
102
102
|
const currentIgnored = await readIgnoredBaseline(transaction.repoRoot);
|
|
103
103
|
const ignoredBaselineByPath = new Map(current.ignoredBaseline.map((entry) => [entry.path, entry.sha256]));
|
|
104
|
-
const changedBaselineIgnored = currentIgnored
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
const changedBaselineIgnored = currentIgnored
|
|
105
|
+
.filter((entry) => ignoredBaselineByPath.has(entry.path) && ignoredBaselineByPath.get(entry.path) !== entry.sha256)
|
|
106
|
+
.map((entry) => entry.path);
|
|
107
|
+
const missingBaselineIgnored = current.ignoredBaseline
|
|
108
|
+
.filter((entry) => !currentIgnored.some((candidate) => candidate.path === entry.path))
|
|
109
|
+
.map((entry) => entry.path);
|
|
109
110
|
const existing = current.checkpoints.find((entry) => entry.workerRunId === outcome.workerRunId);
|
|
110
111
|
if (outcome.status === "reused" || existing) {
|
|
111
112
|
if (!existing)
|
|
@@ -123,12 +124,28 @@ export async function finalizeGitTask(transaction, outcome) {
|
|
|
123
124
|
const newIgnored = currentIgnored.map((entry) => entry.path).filter((entry) => !ignoredBaselineByPath.has(entry));
|
|
124
125
|
if (outcome.status === "succeeded") {
|
|
125
126
|
const allowedPaths = expandAllowedPathsForWorkflow(resolveWorkflow(outcome.taskSpec).workflow, outcome.taskSpec.constraints.allowed_paths);
|
|
127
|
+
const isAllowedEvidencePath = (entry) => !isSensitivePath(entry) &&
|
|
128
|
+
!isEphemeralToolCachePath(entry) &&
|
|
129
|
+
allowedPaths.some((glob) => matchesGlob(entry, glob));
|
|
130
|
+
// Pre-existing ignored evidence under allowed_paths may be rewritten by
|
|
131
|
+
// FINAL-VERIFY smoke / verify-final and must force-add into the checkpoint.
|
|
132
|
+
// Other baseline ignored mutations remain fail-closed (e.g. dist/, secrets).
|
|
133
|
+
const blockedBaselineChanged = changedBaselineIgnored.filter((entry) => !isAllowedEvidencePath(entry));
|
|
134
|
+
const blockedBaselineMissing = missingBaselineIgnored.filter((entry) => !isEphemeralToolCachePath(entry) && !isAllowedEvidencePath(entry));
|
|
135
|
+
if (blockedBaselineChanged.length > 0 || blockedBaselineMissing.length > 0) {
|
|
136
|
+
throw new Error(`pre-existing ignored file changed during Git transaction: ${[...blockedBaselineChanged, ...blockedBaselineMissing].join(", ")}`);
|
|
137
|
+
}
|
|
126
138
|
// Evidence under allowed_paths may be gitignored for local convenience (e.g.
|
|
127
139
|
// reports/welcome/** smoke JSON) but must still checkpoint for Task Pool Done.
|
|
128
140
|
// Sensitive / out-of-scope ignored files remain fail-closed; ephemeral tool
|
|
129
141
|
// caches neither block nor force-add.
|
|
130
142
|
const forceAddIgnored = [];
|
|
131
143
|
const blockedIgnored = [];
|
|
144
|
+
const rewrittenBaselineEvidence = changedBaselineIgnored.filter((entry) => isAllowedEvidencePath(entry));
|
|
145
|
+
for (const entry of rewrittenBaselineEvidence) {
|
|
146
|
+
if (!forceAddIgnored.includes(entry))
|
|
147
|
+
forceAddIgnored.push(entry);
|
|
148
|
+
}
|
|
132
149
|
for (const entry of newIgnored) {
|
|
133
150
|
if (isEphemeralToolCachePath(entry))
|
|
134
151
|
continue;
|
|
@@ -137,7 +154,8 @@ export async function finalizeGitTask(transaction, outcome) {
|
|
|
137
154
|
continue;
|
|
138
155
|
}
|
|
139
156
|
if (allowedPaths.some((glob) => matchesGlob(entry, glob))) {
|
|
140
|
-
forceAddIgnored.
|
|
157
|
+
if (!forceAddIgnored.includes(entry))
|
|
158
|
+
forceAddIgnored.push(entry);
|
|
141
159
|
}
|
|
142
160
|
else {
|
|
143
161
|
blockedIgnored.push(entry);
|
|
@@ -166,7 +184,10 @@ export async function finalizeGitTask(transaction, outcome) {
|
|
|
166
184
|
await assertTransactionPosition(transaction.repoRoot, { ...current, lastCheckpoint: commit });
|
|
167
185
|
await assertCheckpointMetadata(transaction.repoRoot, commit, outcome.taskSpec, outcome.workerRunId);
|
|
168
186
|
const afterCommitIgnored = await readIgnoredBaseline(transaction.repoRoot);
|
|
169
|
-
|
|
187
|
+
// force-add may promote allowed evidence from ignored → tracked; drop those
|
|
188
|
+
// paths from the post-commit baseline equality check.
|
|
189
|
+
const forceAdded = new Set(forceAddIgnored);
|
|
190
|
+
assertIgnoredBaselineUnchanged(current.ignoredBaseline.filter((entry) => !isEphemeralToolCachePath(entry.path) && !forceAdded.has(entry.path)), afterCommitIgnored.filter((entry) => !isEphemeralToolCachePath(entry.path) && !forceAdded.has(entry.path)));
|
|
170
191
|
}
|
|
171
192
|
catch (error) {
|
|
172
193
|
const branch = await git(transaction.repoRoot, ["branch", "--show-current"]).catch(() => "");
|
|
@@ -199,6 +220,10 @@ export async function finalizeGitTask(transaction, outcome) {
|
|
|
199
220
|
changedFiles: checkpointFiles,
|
|
200
221
|
};
|
|
201
222
|
}
|
|
223
|
+
// Failed / keep-diff paths still fail closed on unexpected baseline ignored mutations.
|
|
224
|
+
if (changedBaselineIgnored.length > 0 || missingBaselineIgnored.length > 0) {
|
|
225
|
+
throw new Error(`pre-existing ignored file changed during Git transaction: ${[...changedBaselineIgnored, ...missingBaselineIgnored].join(", ")}`);
|
|
226
|
+
}
|
|
202
227
|
const artifactDir = path.join(path.dirname(transaction.recordPath), "failures", outcome.workerRunId);
|
|
203
228
|
await captureFailureArtifacts(transaction.repoRoot, artifactDir, changes, newIgnored, outcome);
|
|
204
229
|
if (outcome.keepFailedDiff) {
|
|
@@ -284,11 +284,15 @@ export async function validateDeliveryPackage(input) {
|
|
|
284
284
|
blockers.push(`Acceptance coverage status is not derivable: ${ac.id}`);
|
|
285
285
|
if (JSON.stringify([...item.blockedBy].sort()) !== JSON.stringify([...blockedBy].sort()))
|
|
286
286
|
blockers.push(`Acceptance blockers changed: ${ac.id}`);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
287
|
+
// partial / missing / blocked should-ACs intentionally omit full evidence lists
|
|
288
|
+
// (only must ACs covered by QA require run+QA hash binding).
|
|
289
|
+
if (expectedStatus === "covered") {
|
|
290
|
+
for (const ref of expectedRefs)
|
|
291
|
+
if (!item.evidence.some((candidate) => candidate.path === ref.path && candidate.sha256 === ref.sha256))
|
|
292
|
+
blockers.push(`Acceptance run evidence is missing: ${ac.id}`);
|
|
293
|
+
if (qa?.data.acIds.includes(ac.id) && !item.evidence.some((candidate) => candidate.path === qa.ref.path && candidate.sha256 === qa.ref.sha256))
|
|
294
|
+
blockers.push(`Acceptance QA evidence is missing: ${ac.id}`);
|
|
295
|
+
}
|
|
292
296
|
for (const ref of item.evidence)
|
|
293
297
|
if (!await hashedRefValid(repoRoot, ref))
|
|
294
298
|
blockers.push(`Acceptance evidence changed: ${item.acId}`);
|
|
@@ -404,7 +404,23 @@ export async function verifyBundleOutcomes(repoRoot, bundle) {
|
|
|
404
404
|
return false;
|
|
405
405
|
if (JSON.stringify(verified.artifacts) !== JSON.stringify(outcome.artifacts))
|
|
406
406
|
return false;
|
|
407
|
-
|
|
407
|
+
// Rehash only durable typed evidence. run_record/dag_json may be rewritten
|
|
408
|
+
// after outcome projection (worker-run-record appends report commands).
|
|
409
|
+
const evidenceArtifacts = outcome.artifacts.filter((artifact) => {
|
|
410
|
+
const kind = artifact.kind ?? "";
|
|
411
|
+
if (kind === "run_record" || kind === "dag_json")
|
|
412
|
+
return false;
|
|
413
|
+
if (kind === "backend-test-result" ||
|
|
414
|
+
kind === "frontend-test-result" ||
|
|
415
|
+
kind.includes("backend-test-result") ||
|
|
416
|
+
kind.includes("frontend-test-result")) {
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
// Unknown kinds: still rehash unless clearly infrastructure paths.
|
|
420
|
+
return (!artifact.path.includes("worker-run-record") &&
|
|
421
|
+
!artifact.path.includes("-dag.json"));
|
|
422
|
+
});
|
|
423
|
+
if (!await verifyArtifactRefs(repoRoot, evidenceArtifacts))
|
|
408
424
|
return false;
|
|
409
425
|
}
|
|
410
426
|
return true;
|
|
@@ -417,9 +433,12 @@ export function verifyBundleTaskSpecBindings(bundle, specs) {
|
|
|
417
433
|
return false;
|
|
418
434
|
taskIds.add(outcome.taskId);
|
|
419
435
|
const spec = specs.get(outcome.taskId);
|
|
436
|
+
// Typed verification tasks may be qa-execute (frontend-test) or qa-testcode
|
|
437
|
+
// (backend-test Markdown-first). Delivery binds on feature_id + workflow.
|
|
438
|
+
const typedVerificationTypes = new Set(["qa-execute", "qa-testcode"]);
|
|
420
439
|
if (!spec ||
|
|
421
440
|
spec.feature_id !== bundle.featureId ||
|
|
422
|
-
spec.type
|
|
441
|
+
!typedVerificationTypes.has(spec.type) ||
|
|
423
442
|
spec.execution?.workflow !== outcome.workflow)
|
|
424
443
|
return false;
|
|
425
444
|
}
|
|
@@ -460,9 +479,11 @@ async function verifyArtifactRefs(repoRoot, artifacts) {
|
|
|
460
479
|
return false;
|
|
461
480
|
}
|
|
462
481
|
for (const artifact of artifacts) {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const lexical = path.
|
|
482
|
+
// Accept absolute paths only when they resolve inside the repo (legacy
|
|
483
|
+
// envelopes may store abs paths). Prefer relative for new projections.
|
|
484
|
+
const lexical = path.isAbsolute(artifact.path)
|
|
485
|
+
? path.resolve(artifact.path)
|
|
486
|
+
: path.resolve(canonicalRepoRoot, artifact.path);
|
|
466
487
|
let resolved;
|
|
467
488
|
let content;
|
|
468
489
|
try {
|