@tea-agent/loop-agent 0.16.25 → 0.17.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 +46 -0
- package/README.md +14 -3
- package/dist/cli/command-definitions.js +43 -0
- package/dist/cli/program.js +26 -0
- package/dist/commands/dag-approve.js +4 -0
- package/dist/commands/dag-resume.js +1 -0
- package/dist/commands/dag-validate.js +6 -0
- package/dist/commands/operator.js +44 -0
- package/dist/commands/task-contract.js +271 -0
- package/dist/executors/dag-pi-executor.js +118 -16
- package/dist/executors/pi-executor.js +206 -13
- package/dist/executors/pi-sdk-executor.js +21 -6
- package/dist/executors/shell-executor.js +85 -8
- package/dist/executors/shell-presets.js +16 -3
- package/dist/executors/shell-write-guard.js +64 -2
- package/dist/shared/operator/capabilities.js +255 -0
- package/dist/shared/operator/envelope.js +59 -0
- package/dist/shared/operator/index.js +4 -0
- package/dist/shared/operator/registry.js +38 -0
- package/dist/shared/operator/types.js +5 -0
- package/dist/task/contract/adopt.js +166 -0
- package/dist/task/contract/apply.js +326 -0
- package/dist/task/contract/canonicalize.js +60 -0
- package/dist/task/contract/constants.js +29 -0
- package/dist/task/contract/diff.js +177 -0
- package/dist/task/contract/hash.js +42 -0
- package/dist/task/contract/import-revision.js +96 -0
- package/dist/task/contract/index.js +17 -0
- package/dist/task/contract/journal.js +155 -0
- package/dist/task/contract/lock.js +153 -0
- package/dist/task/contract/observe.js +296 -0
- package/dist/task/contract/paths.js +19 -0
- package/dist/task/contract/project.js +170 -0
- package/dist/task/contract/recover.js +312 -0
- package/dist/task/contract/request-ledger.js +37 -0
- package/dist/task/contract/schema.js +151 -0
- package/dist/task/contract/transaction.js +160 -0
- package/dist/task/contract/types.js +1 -0
- package/dist/task/contract/validate-draft.js +106 -0
- package/dist/task/index.js +3 -0
- package/dist/task/operator/capabilities.js +6 -0
- package/dist/task/operator/envelope.js +2 -0
- package/dist/task/operator/index.js +5 -0
- package/dist/task/operator/registry.js +2 -0
- package/dist/task/operator/types.js +1 -0
- package/dist/task/runtime.js +5 -1
- package/dist/task/source-references.js +7 -0
- package/dist/worker/cli.js +150 -32
- package/dist/worker/console/app-data.js +185 -0
- package/dist/worker/console/dag-confirmation.js +313 -0
- package/dist/worker/console/doctor.js +169 -0
- package/dist/worker/console/draft-store.js +80 -0
- package/dist/worker/console/index.js +15 -0
- package/dist/worker/console/interview/assessment.js +67 -0
- package/dist/worker/console/interview/session.js +100 -0
- package/dist/worker/console/interview/tools.js +109 -0
- package/dist/worker/console/loopback.js +16 -0
- package/dist/worker/console/observe-health-match.js +174 -0
- package/dist/worker/console/observe-link.js +33 -0
- package/dist/worker/console/operation-runner.js +166 -0
- package/dist/worker/console/operation-sse.js +158 -0
- package/dist/worker/console/operation-store.js +147 -0
- package/dist/worker/console/operator-actions.js +769 -0
- package/dist/worker/console/pi-readiness.js +94 -0
- package/dist/worker/console/recovery-cta.js +133 -0
- package/dist/worker/console/repo-fingerprint.js +29 -0
- package/dist/worker/console/resource-loader.js +95 -0
- package/dist/worker/console/routes.js +368 -0
- package/dist/worker/console/security.js +126 -0
- package/dist/worker/console/server.js +149 -0
- package/dist/worker/console/sibling-controller.js +28 -0
- package/dist/worker/console/static/assets/index-CbnMgdWa.js +9 -0
- package/dist/worker/console/static/assets/index-Dnj0RVs8.css +1 -0
- package/dist/worker/console/static/index.html +13 -0
- package/dist/worker/console/vite.config.js +27 -0
- package/dist/worker/delivery/git-transaction.js +43 -8
- package/dist/worker/observe/health.js +57 -0
- package/dist/worker/observe/paths.js +81 -0
- package/dist/worker/observe/routes.js +142 -27
- package/dist/worker/observe/spec-evidence.js +84 -0
- package/dist/worker/observe/static/api.js +23 -0
- package/dist/worker/observe/static/state.js +26 -0
- package/dist/worker/observe/static/styles.css +10 -0
- package/dist/worker/observe/static/views/dag-inspector.js +173 -6
- package/dist/workflows/dag/backend-test-analysis-contract.js +34 -9
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -0
- package/dist/workflows/dag/frontend-repair.js +1 -10
- package/dist/workflows/dag/init-hybrid.js +372 -115
- package/dist/workflows/dag/node-execution.js +57 -6
- package/dist/workflows/dag/project-governance-context.js +508 -0
- package/dist/workflows/dag/prompt.js +46 -1
- package/dist/workflows/dag/retry-policy.js +16 -1
- package/dist/workflows/dag/runner.js +9 -0
- package/dist/workflows/dag/skill-snapshot.js +1 -0
- package/dist/workflows/dag/task-contract-binding.js +138 -0
- package/dist/workflows/dag/types.js +84 -10
- package/dist/workflows/dag/validate.js +53 -7
- package/docs/README.md +2 -0
- package/docs/architecture/evolution.md +2 -0
- package/docs/architecture/system-overview.md +6 -0
- package/docs/architecture/worker-and-feature.md +7 -0
- package/docs/templates/agent-dag.schema.json +64 -2
- package/docs/templates/agent-dag.supervised-implementation.json +1 -0
- package/docs/templates/backend-test-dag.classify.prompt.md +1 -1
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +5 -5
- package/docs/templates/backend-test-dag.json +26 -154
- package/docs/templates/backend-test-dag.retrospect.prompt.md +1 -1
- package/docs/templates/backend-test-dag.review-cases.prompt.md +2 -2
- package/package.json +8 -2
- package/skills/agent-worker/SKILL.md +1 -0
- package/skills/agent-worker/references/agent-worker-operator.md +3 -2
- package/skills/frontend-design-review/SKILL.md +25 -16
- package/skills/frontend-implementation/references/node-contracts.md +5 -5
- package/skills/loop-agent/references/command-reference.md +48 -1
- package/skills/loop-agent/references/hybrid-dag.md +4 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { readFile, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { writeDagNodeTextArtifact } from "../infrastructure/harness/artifact-store.js";
|
|
6
6
|
import { truncateOutput } from "../shared/output-truncation.js";
|
|
@@ -19,6 +19,8 @@ import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBack
|
|
|
19
19
|
import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
|
|
20
20
|
import { pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
21
21
|
import { buildShellProcessEnv } from "./shell-verification.js";
|
|
22
|
+
import { readRunState } from "../workflows/dag/run-store.js";
|
|
23
|
+
import { readProjectGovernanceContext } from "../workflows/dag/project-governance-context.js";
|
|
22
24
|
const DEFAULT_SHELL_TIMEOUT_MS = 300_000;
|
|
23
25
|
const SUMMARY_STDOUT_MAX = 4_000;
|
|
24
26
|
const SUMMARY_STDERR_MAX = 2_000;
|
|
@@ -52,6 +54,28 @@ export function resolveShellCwd(root, requestedCwd) {
|
|
|
52
54
|
}
|
|
53
55
|
return resolved;
|
|
54
56
|
}
|
|
57
|
+
async function resolveJsonArtifactSourceNodeId(input) {
|
|
58
|
+
const candidates = [
|
|
59
|
+
input.fromNodeId,
|
|
60
|
+
...new Set((input.fallbackFromNodeIds ?? []).filter((nodeId) => nodeId !== input.fromNodeId)),
|
|
61
|
+
];
|
|
62
|
+
for (const nodeId of candidates) {
|
|
63
|
+
try {
|
|
64
|
+
await stat(path.join(input.runDir, `${nodeId}.json`));
|
|
65
|
+
return nodeId;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error &&
|
|
69
|
+
typeof error === "object" &&
|
|
70
|
+
"code" in error &&
|
|
71
|
+
error.code === "ENOENT") {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`missing jsonArtifactGate node output (tried: ${candidates.join(", ")})`);
|
|
78
|
+
}
|
|
55
79
|
/** Preset commands first, then explicit shell.commands, then verdictGate (deterministic ordering). */
|
|
56
80
|
export function resolveShellCommands(shell) {
|
|
57
81
|
const fromPreset = shell.preset ? expandShellPreset(shell.preset) : [];
|
|
@@ -419,12 +443,30 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
419
443
|
const selected = await executePipelineCommands(input, meta, [buildBackendTestCanonicalResultFromInitialShellSnippet()]);
|
|
420
444
|
if (!selected.every((result) => result.ok))
|
|
421
445
|
throw new Error(selected.find((result) => !result.ok)?.stderr || "canonical result materialization failed");
|
|
446
|
+
const readNodeOutput = async (nodeId) => {
|
|
447
|
+
const record = JSON.parse(await readFile(path.join(meta.runDir, `${nodeId}.json`), "utf8"));
|
|
448
|
+
const text = record.assistantText?.trim() || record.stdout?.trim() || "";
|
|
449
|
+
if (!text)
|
|
450
|
+
throw new Error(`missing auditable node output: ${nodeId}`);
|
|
451
|
+
return text;
|
|
452
|
+
};
|
|
453
|
+
const caseReview = await readNodeOutput("review-backend-cases-pi");
|
|
454
|
+
const reviewVerdicts = caseReview.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("VERDICT:"));
|
|
455
|
+
if (reviewVerdicts.length !== 1 || !["VERDICT: pass", "VERDICT: request-revision"].includes(reviewVerdicts[0])) {
|
|
456
|
+
throw new Error("invalid advisory case review evidence: expected exactly one auditable VERDICT line");
|
|
457
|
+
}
|
|
458
|
+
const traceability = await readNodeOutput("backend-test-traceability-gate-shell");
|
|
459
|
+
if (!traceability.includes("Traceability gate: pass") || !/Matched symbols:\s*\d+/.test(traceability)) {
|
|
460
|
+
throw new Error("invalid traceability evidence: deterministic pass summary is missing");
|
|
461
|
+
}
|
|
422
462
|
const contracts = path.join(meta.runDir, "contracts");
|
|
423
463
|
const context = {
|
|
424
464
|
schemaVersion: 1,
|
|
425
465
|
result: JSON.parse(await readFile(path.join(contracts, "backend-test-result.json"), "utf8")),
|
|
426
466
|
manifest: JSON.parse(await readFile(path.join(contracts, "backend-test-case-manifest.json"), "utf8")),
|
|
427
467
|
classification: JSON.parse(await readFile(path.join(contracts, "backend-test-classification.json"), "utf8")),
|
|
468
|
+
caseReview: { verdict: reviewVerdicts[0].slice("VERDICT: ".length), evidence: caseReview },
|
|
469
|
+
traceability: { status: "pass", evidence: traceability },
|
|
428
470
|
};
|
|
429
471
|
outputs.push(`classification=${classification.path}`, ...selected.map((result) => result.stdout), JSON.stringify(context, null, 2));
|
|
430
472
|
}
|
|
@@ -508,13 +550,20 @@ export async function executeDagShellNode(input, meta) {
|
|
|
508
550
|
const started = Date.now();
|
|
509
551
|
try {
|
|
510
552
|
const gate = shell.jsonArtifactGate;
|
|
553
|
+
const sourceNodeId = gate.fallbackFromNodeIds?.length
|
|
554
|
+
? await resolveJsonArtifactSourceNodeId({
|
|
555
|
+
runDir: meta.runDir,
|
|
556
|
+
fromNodeId: gate.fromNodeId,
|
|
557
|
+
fallbackFromNodeIds: gate.fallbackFromNodeIds,
|
|
558
|
+
})
|
|
559
|
+
: gate.fromNodeId;
|
|
511
560
|
let artifact;
|
|
512
561
|
switch (gate.schemaId) {
|
|
513
562
|
case "backend-test-analysis-v1":
|
|
514
563
|
case "backend-test-analysis-v2":
|
|
515
564
|
artifact = await materializeBackendTestAnalysisContract({
|
|
516
565
|
runDir: meta.runDir,
|
|
517
|
-
fromNodeId:
|
|
566
|
+
fromNodeId: sourceNodeId,
|
|
518
567
|
artifactName: gate.artifactName,
|
|
519
568
|
outputDir: gate.outputDir,
|
|
520
569
|
sourceBinding: meta.spec.sourceBinding,
|
|
@@ -523,7 +572,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
523
572
|
case "backend-test-execution-v1":
|
|
524
573
|
artifact = await materializeBackendTestExecutionContract({
|
|
525
574
|
runDir: meta.runDir,
|
|
526
|
-
fromNodeId:
|
|
575
|
+
fromNodeId: sourceNodeId,
|
|
527
576
|
artifactName: gate.artifactName,
|
|
528
577
|
outputDir: gate.outputDir,
|
|
529
578
|
});
|
|
@@ -531,7 +580,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
531
580
|
case "backend-test-result-v1":
|
|
532
581
|
artifact = await materializeBackendTestResultFromRunDir({
|
|
533
582
|
runDir: meta.runDir,
|
|
534
|
-
fromNodeId:
|
|
583
|
+
fromNodeId: sourceNodeId,
|
|
535
584
|
artifactName: gate.artifactName,
|
|
536
585
|
outputDir: gate.outputDir,
|
|
537
586
|
junitRelativePath: gate.junitRelativePath,
|
|
@@ -540,7 +589,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
540
589
|
case "backend-test-classification-v1":
|
|
541
590
|
artifact = await materializeBackendTestClassification({
|
|
542
591
|
runDir: meta.runDir,
|
|
543
|
-
fromNodeId:
|
|
592
|
+
fromNodeId: sourceNodeId,
|
|
544
593
|
artifactName: gate.artifactName,
|
|
545
594
|
outputDir: gate.outputDir,
|
|
546
595
|
});
|
|
@@ -548,7 +597,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
548
597
|
case "backend-test-semantic-review-v1":
|
|
549
598
|
artifact = await materializeBackendTestSemanticReview({
|
|
550
599
|
runDir: meta.runDir,
|
|
551
|
-
fromNodeId:
|
|
600
|
+
fromNodeId: sourceNodeId,
|
|
552
601
|
artifactName: gate.artifactName,
|
|
553
602
|
outputDir: gate.outputDir,
|
|
554
603
|
});
|
|
@@ -556,7 +605,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
556
605
|
case "backend-test-case-manifest-v1":
|
|
557
606
|
artifact = await materializeBackendTestCaseManifest({
|
|
558
607
|
runDir: meta.runDir,
|
|
559
|
-
fromNodeId:
|
|
608
|
+
fromNodeId: sourceNodeId,
|
|
560
609
|
artifactName: gate.artifactName,
|
|
561
610
|
outputDir: gate.outputDir,
|
|
562
611
|
sourceBinding: meta.spec.sourceBinding,
|
|
@@ -565,7 +614,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
565
614
|
case "frontend-implementation-contract-v1":
|
|
566
615
|
artifact = await materializeFrontendImplementationContract({
|
|
567
616
|
runDir: meta.runDir,
|
|
568
|
-
fromNodeId:
|
|
617
|
+
fromNodeId: sourceNodeId,
|
|
569
618
|
artifactName: gate.artifactName,
|
|
570
619
|
outputDir: gate.outputDir,
|
|
571
620
|
sourceBinding: meta.spec.sourceBinding,
|
|
@@ -733,6 +782,34 @@ export async function executeDagShellNode(input, meta) {
|
|
|
733
782
|
};
|
|
734
783
|
}
|
|
735
784
|
}
|
|
785
|
+
if (shell?.projectGovernanceGate) {
|
|
786
|
+
const started = Date.now();
|
|
787
|
+
try {
|
|
788
|
+
const state = await readRunState(meta.runDir);
|
|
789
|
+
if (!state.projectGovernanceContextRef) {
|
|
790
|
+
throw new Error("project governance gate requires projectGovernanceContextRef in run state");
|
|
791
|
+
}
|
|
792
|
+
const context = await readProjectGovernanceContext(meta.runDir, state.projectGovernanceContextRef, { expectedRunId: meta.runId });
|
|
793
|
+
if (!context.applicable) {
|
|
794
|
+
return {
|
|
795
|
+
ok: true,
|
|
796
|
+
stdout: "Project governance gate: not applicable for this writer changeset.",
|
|
797
|
+
stderr: "",
|
|
798
|
+
failureCategory: "success",
|
|
799
|
+
durationMs: Date.now() - started,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
return {
|
|
805
|
+
ok: false,
|
|
806
|
+
stdout: "",
|
|
807
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
808
|
+
failureCategory: "invalid-output",
|
|
809
|
+
durationMs: Date.now() - started,
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
}
|
|
736
813
|
const commands = shell ? resolveShellCommands(shell) : [];
|
|
737
814
|
if (!shell || commands.length === 0) {
|
|
738
815
|
throw new Error(`shell task ${input.task.id} requires shell.preset, shell.verdictGate, and/or non-empty shell.commands`);
|
|
@@ -60,12 +60,25 @@ export function buildVerdictGateShellCommand(gate) {
|
|
|
60
60
|
export function buildRequirementCoverageGateShellCommand(gate) {
|
|
61
61
|
const label = gate.label ?? "requirement coverage";
|
|
62
62
|
const requiredJson = escapeShellSingleQuoted(JSON.stringify(gate.requiredIds.map((id) => id.toUpperCase())));
|
|
63
|
-
const
|
|
63
|
+
const sourceConfig = {
|
|
64
|
+
primary: gate.fromNodeIds,
|
|
65
|
+
fallback: [
|
|
66
|
+
...new Set((gate.fallbackFromNodeIds ?? []).filter((nodeId) => !gate.fromNodeIds.includes(nodeId))),
|
|
67
|
+
],
|
|
68
|
+
};
|
|
64
69
|
const program = [
|
|
65
70
|
'const fs=require("fs");',
|
|
71
|
+
'const path=require("path");',
|
|
66
72
|
"const required=JSON.parse(process.argv[1]);",
|
|
73
|
+
`const sources=${JSON.stringify(sourceConfig)};`,
|
|
74
|
+
"let files=sources.primary.map((id)=>path.join(process.env.HARNESS_DAG_RUN_DIR,id+'.json'));",
|
|
75
|
+
"if(sources.fallback.length){",
|
|
76
|
+
" files=[]; const candidates=[sources.primary[0],...sources.fallback];",
|
|
77
|
+
" for(const id of candidates){const file=path.join(process.env.HARNESS_DAG_RUN_DIR,id+'.json'); try{fs.statSync(file);files=[file];break;}catch(error){if(error&&error.code==='ENOENT')continue;throw error;}}",
|
|
78
|
+
` if(!files.length){console.error(${JSON.stringify(`${label} gate blocked: missing node output (tried: `)}+[sources.primary[0],...sources.fallback].join(', ')+')');process.exit(1);}`,
|
|
79
|
+
"}",
|
|
67
80
|
"let text='';",
|
|
68
|
-
"for(const file of
|
|
81
|
+
"for(const file of files){",
|
|
69
82
|
" if(!fs.existsSync(file)){console.error('missing requirement coverage node output: '+file);process.exit(1);}",
|
|
70
83
|
" const raw=JSON.parse(fs.readFileSync(file,'utf8')); text+='\\n'+String(raw.assistantText ?? raw.stdout ?? '');",
|
|
71
84
|
"}",
|
|
@@ -74,5 +87,5 @@ export function buildRequirementCoverageGateShellCommand(gate) {
|
|
|
74
87
|
`if(missing.length){console.error(${JSON.stringify(`${label} gate blocked: missing `)}+missing.join(', '));process.exit(1);}`,
|
|
75
88
|
`console.log(${JSON.stringify(`${label} gate passed: `)}+required.join(', '));`,
|
|
76
89
|
].join("");
|
|
77
|
-
return `test -n "\${HARNESS_DAG_RUN_DIR:-}" || { echo "missing HARNESS_DAG_RUN_DIR for ${label} gate" >&2; exit 1; }; node -e '${escapeShellSingleQuoted(program)}' '${requiredJson}'
|
|
90
|
+
return `test -n "\${HARNESS_DAG_RUN_DIR:-}" || { echo "missing HARNESS_DAG_RUN_DIR for ${label} gate" >&2; exit 1; }; node -e '${escapeShellSingleQuoted(program)}' '${requiredJson}'`;
|
|
78
91
|
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { createReadStream } from "node:fs";
|
|
4
|
+
import { lstat, readlink } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
2
6
|
import { pathMatchesPattern } from "../shared/git-progress.js";
|
|
7
|
+
async function sha256File(filePath) {
|
|
8
|
+
const hash = createHash("sha256");
|
|
9
|
+
for await (const chunk of createReadStream(filePath)) {
|
|
10
|
+
hash.update(chunk);
|
|
11
|
+
}
|
|
12
|
+
return hash.digest("hex");
|
|
13
|
+
}
|
|
3
14
|
export function snapshotGitStatusPorcelain(porcelain) {
|
|
4
15
|
const snapshot = new Map();
|
|
5
16
|
for (const line of porcelain.split("\n")) {
|
|
@@ -14,11 +25,15 @@ export function snapshotGitStatusPorcelain(porcelain) {
|
|
|
14
25
|
}
|
|
15
26
|
return snapshot;
|
|
16
27
|
}
|
|
17
|
-
export function pathsChangedDuringRun(before, after) {
|
|
28
|
+
export function pathsChangedDuringRun(before, after, beforeFingerprints, afterFingerprints) {
|
|
18
29
|
const changed = new Set();
|
|
19
30
|
for (const [filePath, afterCode] of after) {
|
|
20
31
|
const beforeCode = before.get(filePath);
|
|
21
|
-
if (beforeCode === undefined ||
|
|
32
|
+
if (beforeCode === undefined ||
|
|
33
|
+
beforeCode !== afterCode ||
|
|
34
|
+
(beforeFingerprints !== undefined &&
|
|
35
|
+
afterFingerprints !== undefined &&
|
|
36
|
+
beforeFingerprints.get(filePath) !== afterFingerprints.get(filePath))) {
|
|
22
37
|
changed.add(filePath);
|
|
23
38
|
}
|
|
24
39
|
}
|
|
@@ -29,6 +44,53 @@ export function pathsChangedDuringRun(before, after) {
|
|
|
29
44
|
}
|
|
30
45
|
return Array.from(changed).sort();
|
|
31
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Capture content-aware fingerprints for every path already present in a
|
|
49
|
+
* porcelain snapshot. Status codes alone cannot detect a writer editing a file
|
|
50
|
+
* that was `M` or `??` before the node and remains so afterwards.
|
|
51
|
+
*
|
|
52
|
+
* Symlinks are hashed by link target without following them, keeping the
|
|
53
|
+
* snapshot repository-contained. Missing paths and non-files receive stable
|
|
54
|
+
* sentinels so deletions and type changes remain observable.
|
|
55
|
+
*/
|
|
56
|
+
export async function snapshotGitStatusPathFingerprints(cwd, status) {
|
|
57
|
+
const root = path.resolve(cwd);
|
|
58
|
+
const entries = await Promise.all([...status.keys()].map(async (filePath) => {
|
|
59
|
+
const candidate = path.resolve(root, filePath);
|
|
60
|
+
const relative = path.relative(root, candidate);
|
|
61
|
+
if (relative.startsWith("..") ||
|
|
62
|
+
path.isAbsolute(relative)) {
|
|
63
|
+
return [filePath, "outside-repository"];
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const info = await lstat(candidate);
|
|
67
|
+
if (info.isSymbolicLink()) {
|
|
68
|
+
const target = await readlink(candidate);
|
|
69
|
+
return [
|
|
70
|
+
filePath,
|
|
71
|
+
`symlink:${createHash("sha256").update(target).digest("hex")}`,
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
if (info.isFile()) {
|
|
75
|
+
return [
|
|
76
|
+
filePath,
|
|
77
|
+
`file:${await sha256File(candidate)}`,
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
return [
|
|
81
|
+
filePath,
|
|
82
|
+
`other:${info.mode}:${info.size}:${info.mtimeMs}`,
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const code = error && typeof error === "object" && "code" in error
|
|
87
|
+
? String(error.code)
|
|
88
|
+
: "unknown";
|
|
89
|
+
return [filePath, `unreadable:${code}`];
|
|
90
|
+
}
|
|
91
|
+
}));
|
|
92
|
+
return new Map(entries);
|
|
93
|
+
}
|
|
32
94
|
const DAG_RUNS_PREFIX = ".harness/dag-runs/";
|
|
33
95
|
/**
|
|
34
96
|
* Ephemeral interpreter/tool caches that writers may create while drafting tests.
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { OPERATOR_ERROR_CODES_V1, OPERATOR_OUTCOMES_V1 } from "./registry.js";
|
|
2
|
+
/** Shared constants so dag + dagSpecVersions cannot diverge. */
|
|
3
|
+
const DAG_SPEC_SUPPORTED_PARSE = [1, 2, 3, 4];
|
|
4
|
+
const DAG_SPEC_NEW_WRITER_DEFAULT = 4;
|
|
5
|
+
const DAG_SPEC_HISTORICAL_TERMINAL_READ_ONLY = [1, 2, 3];
|
|
6
|
+
const TASK_CONTRACT_BINDING_SCHEMA_VERSION = 1;
|
|
7
|
+
const REQUIRES_TASK_CONTRACT_BINDING = true;
|
|
8
|
+
const COMMON_MUTATION_ERRORS = [
|
|
9
|
+
"INVALID_INPUT",
|
|
10
|
+
"REQUEST_ID_REUSE_CONFLICT",
|
|
11
|
+
"REVISION_CONFLICT",
|
|
12
|
+
"OBSERVED_HASH_CONFLICT",
|
|
13
|
+
"LEASE_HELD",
|
|
14
|
+
"TRANSACTION_INCOMPLETE",
|
|
15
|
+
"NOT_FOUND",
|
|
16
|
+
];
|
|
17
|
+
export function buildOperatorCapabilitiesDocument() {
|
|
18
|
+
const actions = [
|
|
19
|
+
{
|
|
20
|
+
action: "operatorCapabilities",
|
|
21
|
+
cli: "loop-agent operator capabilities --json",
|
|
22
|
+
kind: "read",
|
|
23
|
+
inputSchemaVersion: 1,
|
|
24
|
+
resultSchemaVersion: 1,
|
|
25
|
+
envelopeSchemaVersion: 1,
|
|
26
|
+
requiredErrorCodes: ["UNSUPPORTED_SCHEMA_VERSION", "INVALID_INPUT"],
|
|
27
|
+
description: "Publish action/schema/error registry for Console negotiation.",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
action: "contractShow",
|
|
31
|
+
cli: "loop-agent task contract show <taskId> --json",
|
|
32
|
+
kind: "read",
|
|
33
|
+
inputSchemaVersion: 1,
|
|
34
|
+
resultSchemaVersion: 1,
|
|
35
|
+
envelopeSchemaVersion: 1,
|
|
36
|
+
requiredErrorCodes: ["NOT_FOUND", "INVALID_INPUT"],
|
|
37
|
+
description: "Return TaskContractStateV1 + observed hashes.",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
action: "contractValidate",
|
|
41
|
+
cli: "loop-agent task contract validate --task <id> --input <path> --json",
|
|
42
|
+
kind: "read",
|
|
43
|
+
inputSchemaVersion: 1,
|
|
44
|
+
resultSchemaVersion: 1,
|
|
45
|
+
envelopeSchemaVersion: 1,
|
|
46
|
+
requiredErrorCodes: ["INVALID_INPUT", "NOT_FOUND", "PATH_OUTSIDE_REPO"],
|
|
47
|
+
description: "Schema/path/traceability validation for a Draft (read-only).",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
action: "contractDiff",
|
|
51
|
+
cli: "loop-agent task contract diff --task <id> --input <path> --json",
|
|
52
|
+
kind: "read",
|
|
53
|
+
inputSchemaVersion: 1,
|
|
54
|
+
resultSchemaVersion: 1,
|
|
55
|
+
envelopeSchemaVersion: 1,
|
|
56
|
+
requiredErrorCodes: ["INVALID_INPUT", "NOT_FOUND", "PATH_OUTSIDE_REPO"],
|
|
57
|
+
description: "Risk-classified diff against current managed/legacy state.",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
action: "contractApply",
|
|
61
|
+
cli: "loop-agent task contract apply --task <id> --input <path> --expected-revision <n> --expected-observed-hash <sha256> --request-id <id> --request-payload-sha256 <sha256> --json",
|
|
62
|
+
kind: "mutation",
|
|
63
|
+
inputSchemaVersion: 1,
|
|
64
|
+
resultSchemaVersion: 1,
|
|
65
|
+
envelopeSchemaVersion: 1,
|
|
66
|
+
requiredErrorCodes: [...COMMON_MUTATION_ERRORS, "EXTERNALLY_MODIFIED"],
|
|
67
|
+
description: "Journaled apply of Draft to canonical task facts.",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
action: "newTask",
|
|
71
|
+
cli: "loop-agent new-task <id> <title> --json",
|
|
72
|
+
kind: "mutation",
|
|
73
|
+
inputSchemaVersion: 1,
|
|
74
|
+
resultSchemaVersion: 1,
|
|
75
|
+
envelopeSchemaVersion: 1,
|
|
76
|
+
requiredErrorCodes: [...COMMON_MUTATION_ERRORS],
|
|
77
|
+
description: "Create a new task harness skeleton via sibling CLI.",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
action: "importPrd",
|
|
81
|
+
cli: "loop-agent import-prd <id> --file <server-staged-path> --json",
|
|
82
|
+
kind: "mutation",
|
|
83
|
+
inputSchemaVersion: 1,
|
|
84
|
+
resultSchemaVersion: 1,
|
|
85
|
+
envelopeSchemaVersion: 1,
|
|
86
|
+
requiredErrorCodes: [...COMMON_MUTATION_ERRORS, "PATH_OUTSIDE_REPO"],
|
|
87
|
+
description: "Import immutable PRD/source reference via sibling CLI (server-staged file).",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
action: "sourceInstructions",
|
|
91
|
+
cli: "loop-agent instructions source --task <id> --json",
|
|
92
|
+
kind: "read",
|
|
93
|
+
inputSchemaVersion: 1,
|
|
94
|
+
resultSchemaVersion: 1,
|
|
95
|
+
envelopeSchemaVersion: 1,
|
|
96
|
+
requiredErrorCodes: ["NOT_FOUND", "INVALID_INPUT"],
|
|
97
|
+
description: "Read source instruction contract for a task.",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
action: "contractDraftSave",
|
|
101
|
+
cli: "console application-data draft save (non-canonical)",
|
|
102
|
+
kind: "mutation",
|
|
103
|
+
inputSchemaVersion: 1,
|
|
104
|
+
resultSchemaVersion: 1,
|
|
105
|
+
envelopeSchemaVersion: 1,
|
|
106
|
+
requiredErrorCodes: ["INVALID_INPUT"],
|
|
107
|
+
description: "Save TaskContractDraftV1 to Console application data only (not repo .harness).",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
action: "prepareDagConfirmation",
|
|
111
|
+
cli: "console aggregate confirmation receipt (server-side)",
|
|
112
|
+
kind: "read",
|
|
113
|
+
inputSchemaVersion: 1,
|
|
114
|
+
resultSchemaVersion: 1,
|
|
115
|
+
envelopeSchemaVersion: 1,
|
|
116
|
+
requiredErrorCodes: [
|
|
117
|
+
"INVALID_INPUT",
|
|
118
|
+
"BINDING_DRIFT",
|
|
119
|
+
"HUMAN_CONFIRMATION_REQUIRED",
|
|
120
|
+
],
|
|
121
|
+
description: "Prepare server-side DagConfirmationV1 receipt for human challenge.",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
action: "confirmDagConfirmation",
|
|
125
|
+
cli: "console aggregate confirmation human challenge (server-side)",
|
|
126
|
+
kind: "mutation",
|
|
127
|
+
inputSchemaVersion: 1,
|
|
128
|
+
resultSchemaVersion: 1,
|
|
129
|
+
envelopeSchemaVersion: 1,
|
|
130
|
+
requiredErrorCodes: [
|
|
131
|
+
"INVALID_INPUT",
|
|
132
|
+
"NOT_FOUND",
|
|
133
|
+
"CONFIRMATION_STALE",
|
|
134
|
+
"CONFIRMATION_EXPIRED",
|
|
135
|
+
"CONFIRMATION_CONSUMED",
|
|
136
|
+
"HUMAN_CONFIRMATION_REQUIRED",
|
|
137
|
+
],
|
|
138
|
+
description: "Human challenge confirm of DagConfirmationV1 using server-derived current bindings.",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
action: "observeLink",
|
|
142
|
+
cli: "console observe deep-link builder (read-only navigation)",
|
|
143
|
+
kind: "read",
|
|
144
|
+
inputSchemaVersion: 1,
|
|
145
|
+
resultSchemaVersion: 1,
|
|
146
|
+
envelopeSchemaVersion: 1,
|
|
147
|
+
requiredErrorCodes: ["INVALID_INPUT"],
|
|
148
|
+
description: "Build Observe deep link from stable object identity.",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
action: "inspect",
|
|
152
|
+
cli: "loop-agent inspect --json",
|
|
153
|
+
kind: "read",
|
|
154
|
+
inputSchemaVersion: 1,
|
|
155
|
+
resultSchemaVersion: 1,
|
|
156
|
+
envelopeSchemaVersion: 1,
|
|
157
|
+
requiredErrorCodes: ["INVALID_INPUT"],
|
|
158
|
+
description: "Repo preflight (registry entry; envelope migration follow-up).",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
action: "doctor",
|
|
162
|
+
cli: "loop-agent doctor --json",
|
|
163
|
+
kind: "read",
|
|
164
|
+
inputSchemaVersion: 1,
|
|
165
|
+
resultSchemaVersion: 1,
|
|
166
|
+
envelopeSchemaVersion: 1,
|
|
167
|
+
requiredErrorCodes: ["INVALID_INPUT", "OPERATION_NEEDS_RECONCILE"],
|
|
168
|
+
description: "Repo doctor (registry entry; envelope migration follow-up).",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
action: "status",
|
|
172
|
+
cli: "loop-agent status <taskId> --json",
|
|
173
|
+
kind: "read",
|
|
174
|
+
inputSchemaVersion: 1,
|
|
175
|
+
resultSchemaVersion: 1,
|
|
176
|
+
envelopeSchemaVersion: 1,
|
|
177
|
+
requiredErrorCodes: ["NOT_FOUND", "INVALID_INPUT"],
|
|
178
|
+
description: "Task status (registry entry).",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
action: "spineCheck",
|
|
182
|
+
cli: "loop-agent spine audit <taskId> --json",
|
|
183
|
+
kind: "read",
|
|
184
|
+
inputSchemaVersion: 1,
|
|
185
|
+
resultSchemaVersion: 1,
|
|
186
|
+
envelopeSchemaVersion: 1,
|
|
187
|
+
requiredErrorCodes: ["NOT_FOUND", "INVALID_INPUT"],
|
|
188
|
+
description: "Spine audit (registry entry).",
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
action: "dagRunTask",
|
|
192
|
+
cli: "loop-agent dag run-task <taskId> --profile auto",
|
|
193
|
+
kind: "long-running",
|
|
194
|
+
inputSchemaVersion: 1,
|
|
195
|
+
resultSchemaVersion: 1,
|
|
196
|
+
envelopeSchemaVersion: 1,
|
|
197
|
+
requiredErrorCodes: [
|
|
198
|
+
"NOT_MANAGED",
|
|
199
|
+
"BINDING_DRIFT",
|
|
200
|
+
"EXTERNALLY_MODIFIED",
|
|
201
|
+
"INVALID_INPUT",
|
|
202
|
+
],
|
|
203
|
+
description: "Generate DagSpec v4 with taskContractBinding (managed required).",
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
action: "dagValidate",
|
|
207
|
+
cli: "loop-agent dag validate --dag <path> --json",
|
|
208
|
+
kind: "read",
|
|
209
|
+
inputSchemaVersion: 1,
|
|
210
|
+
resultSchemaVersion: 1,
|
|
211
|
+
envelopeSchemaVersion: 1,
|
|
212
|
+
requiredErrorCodes: ["INVALID_INPUT", "BINDING_DRIFT"],
|
|
213
|
+
description: "Validate DAG including taskContractBinding drift checks.",
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
action: "runDag",
|
|
217
|
+
cli: "loop-agent run-dag --dag <path>",
|
|
218
|
+
kind: "long-running",
|
|
219
|
+
inputSchemaVersion: 1,
|
|
220
|
+
resultSchemaVersion: 1,
|
|
221
|
+
envelopeSchemaVersion: 1,
|
|
222
|
+
requiredErrorCodes: [
|
|
223
|
+
"BINDING_DRIFT",
|
|
224
|
+
"CONTROLLER_MISMATCH",
|
|
225
|
+
"INVALID_INPUT",
|
|
226
|
+
],
|
|
227
|
+
description: "Execute reviewed DAG; revalidate binding before writer start.",
|
|
228
|
+
},
|
|
229
|
+
];
|
|
230
|
+
return {
|
|
231
|
+
schemaVersion: 1,
|
|
232
|
+
envelopeSchemaVersion: 1,
|
|
233
|
+
outcomes: OPERATOR_OUTCOMES_V1,
|
|
234
|
+
errorCodes: OPERATOR_ERROR_CODES_V1,
|
|
235
|
+
dagSpecVersions: {
|
|
236
|
+
supportedParse: DAG_SPEC_SUPPORTED_PARSE,
|
|
237
|
+
newWriterDefault: DAG_SPEC_NEW_WRITER_DEFAULT,
|
|
238
|
+
historicalTerminalReadOnly: DAG_SPEC_HISTORICAL_TERMINAL_READ_ONLY,
|
|
239
|
+
},
|
|
240
|
+
dag: {
|
|
241
|
+
supportedSpecVersions: DAG_SPEC_SUPPORTED_PARSE,
|
|
242
|
+
generatedSpecVersion: DAG_SPEC_NEW_WRITER_DEFAULT,
|
|
243
|
+
requiresTaskContractBinding: REQUIRES_TASK_CONTRACT_BINDING,
|
|
244
|
+
taskContractBindingSchemaVersion: TASK_CONTRACT_BINDING_SCHEMA_VERSION,
|
|
245
|
+
},
|
|
246
|
+
taskContract: {
|
|
247
|
+
draftSchemaVersion: 1,
|
|
248
|
+
refSchemaVersion: 1,
|
|
249
|
+
projectionVersion: 1,
|
|
250
|
+
canonicalizerVersion: 1,
|
|
251
|
+
taskConfigSchemaVersion: 1,
|
|
252
|
+
},
|
|
253
|
+
actions,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function buildOperatorResult(input) {
|
|
2
|
+
const envelope = {
|
|
3
|
+
schemaVersion: 1,
|
|
4
|
+
command: input.command,
|
|
5
|
+
ok: input.ok,
|
|
6
|
+
outcome: input.outcome,
|
|
7
|
+
warnings: input.warnings ?? [],
|
|
8
|
+
};
|
|
9
|
+
if (input.result !== undefined) {
|
|
10
|
+
envelope.result = input.result;
|
|
11
|
+
}
|
|
12
|
+
if (input.error !== undefined) {
|
|
13
|
+
envelope.error = input.error;
|
|
14
|
+
}
|
|
15
|
+
return envelope;
|
|
16
|
+
}
|
|
17
|
+
export function operatorSucceeded(command, result, warnings = []) {
|
|
18
|
+
return buildOperatorResult({
|
|
19
|
+
command,
|
|
20
|
+
ok: true,
|
|
21
|
+
outcome: "succeeded",
|
|
22
|
+
result,
|
|
23
|
+
warnings,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export function operatorFailed(input) {
|
|
27
|
+
return buildOperatorResult({
|
|
28
|
+
command: input.command,
|
|
29
|
+
ok: false,
|
|
30
|
+
outcome: input.outcome,
|
|
31
|
+
warnings: input.warnings ?? [],
|
|
32
|
+
error: {
|
|
33
|
+
code: input.code,
|
|
34
|
+
message: input.message,
|
|
35
|
+
...(input.details ? { details: input.details } : {}),
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Write a single complete JSON document to stdout (never JSONL). */
|
|
40
|
+
export function writeOperatorJson(result) {
|
|
41
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
42
|
+
}
|
|
43
|
+
export function processExitCodeForOutcome(outcome) {
|
|
44
|
+
switch (outcome) {
|
|
45
|
+
case "succeeded":
|
|
46
|
+
return 0;
|
|
47
|
+
case "not-found":
|
|
48
|
+
return 2;
|
|
49
|
+
case "invalid":
|
|
50
|
+
case "rejected":
|
|
51
|
+
case "conflict":
|
|
52
|
+
case "blocked":
|
|
53
|
+
case "timed-out":
|
|
54
|
+
case "needs-reconcile":
|
|
55
|
+
return 1;
|
|
56
|
+
default:
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const OPERATOR_OUTCOMES_V1 = [
|
|
2
|
+
"succeeded",
|
|
3
|
+
"rejected",
|
|
4
|
+
"conflict",
|
|
5
|
+
"blocked",
|
|
6
|
+
"not-found",
|
|
7
|
+
"invalid",
|
|
8
|
+
"timed-out",
|
|
9
|
+
"needs-reconcile",
|
|
10
|
+
];
|
|
11
|
+
export const OPERATOR_ERROR_CODES_V1 = [
|
|
12
|
+
"UNSUPPORTED_SCHEMA_VERSION",
|
|
13
|
+
"CAPABILITY_MISMATCH",
|
|
14
|
+
"INVALID_INPUT",
|
|
15
|
+
"PATH_OUTSIDE_REPO",
|
|
16
|
+
"REQUEST_ID_REUSE_CONFLICT",
|
|
17
|
+
"REVISION_CONFLICT",
|
|
18
|
+
"OBSERVED_HASH_CONFLICT",
|
|
19
|
+
"LEASE_HELD",
|
|
20
|
+
"TRANSACTION_INCOMPLETE",
|
|
21
|
+
"BINDING_DRIFT",
|
|
22
|
+
"CONTROLLER_MISMATCH",
|
|
23
|
+
"HUMAN_CONFIRMATION_REQUIRED",
|
|
24
|
+
"OPERATION_NEEDS_RECONCILE",
|
|
25
|
+
"EVENT_CURSOR_EXPIRED",
|
|
26
|
+
"NOT_FOUND",
|
|
27
|
+
"NOT_MANAGED",
|
|
28
|
+
"EXTERNALLY_MODIFIED",
|
|
29
|
+
"INTERNAL_ERROR",
|
|
30
|
+
"PI_SETUP_REQUIRED",
|
|
31
|
+
"CONFIRMATION_EXPIRED",
|
|
32
|
+
"CONFIRMATION_STALE",
|
|
33
|
+
"CONFIRMATION_CONSUMED",
|
|
34
|
+
];
|
|
35
|
+
export const OPERATOR_ERROR_CODE_SET = new Set(OPERATOR_ERROR_CODES_V1);
|
|
36
|
+
export function isOperatorErrorCode(code) {
|
|
37
|
+
return OPERATOR_ERROR_CODE_SET.has(code);
|
|
38
|
+
}
|