@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.
Files changed (115) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +14 -3
  3. package/dist/cli/command-definitions.js +43 -0
  4. package/dist/cli/program.js +26 -0
  5. package/dist/commands/dag-approve.js +4 -0
  6. package/dist/commands/dag-resume.js +1 -0
  7. package/dist/commands/dag-validate.js +6 -0
  8. package/dist/commands/operator.js +44 -0
  9. package/dist/commands/task-contract.js +271 -0
  10. package/dist/executors/dag-pi-executor.js +118 -16
  11. package/dist/executors/pi-executor.js +206 -13
  12. package/dist/executors/pi-sdk-executor.js +21 -6
  13. package/dist/executors/shell-executor.js +85 -8
  14. package/dist/executors/shell-presets.js +16 -3
  15. package/dist/executors/shell-write-guard.js +64 -2
  16. package/dist/shared/operator/capabilities.js +255 -0
  17. package/dist/shared/operator/envelope.js +59 -0
  18. package/dist/shared/operator/index.js +4 -0
  19. package/dist/shared/operator/registry.js +38 -0
  20. package/dist/shared/operator/types.js +5 -0
  21. package/dist/task/contract/adopt.js +166 -0
  22. package/dist/task/contract/apply.js +326 -0
  23. package/dist/task/contract/canonicalize.js +60 -0
  24. package/dist/task/contract/constants.js +29 -0
  25. package/dist/task/contract/diff.js +177 -0
  26. package/dist/task/contract/hash.js +42 -0
  27. package/dist/task/contract/import-revision.js +96 -0
  28. package/dist/task/contract/index.js +17 -0
  29. package/dist/task/contract/journal.js +155 -0
  30. package/dist/task/contract/lock.js +153 -0
  31. package/dist/task/contract/observe.js +296 -0
  32. package/dist/task/contract/paths.js +19 -0
  33. package/dist/task/contract/project.js +170 -0
  34. package/dist/task/contract/recover.js +312 -0
  35. package/dist/task/contract/request-ledger.js +37 -0
  36. package/dist/task/contract/schema.js +151 -0
  37. package/dist/task/contract/transaction.js +160 -0
  38. package/dist/task/contract/types.js +1 -0
  39. package/dist/task/contract/validate-draft.js +106 -0
  40. package/dist/task/index.js +3 -0
  41. package/dist/task/operator/capabilities.js +6 -0
  42. package/dist/task/operator/envelope.js +2 -0
  43. package/dist/task/operator/index.js +5 -0
  44. package/dist/task/operator/registry.js +2 -0
  45. package/dist/task/operator/types.js +1 -0
  46. package/dist/task/runtime.js +5 -1
  47. package/dist/task/source-references.js +7 -0
  48. package/dist/worker/cli.js +150 -32
  49. package/dist/worker/console/app-data.js +185 -0
  50. package/dist/worker/console/dag-confirmation.js +313 -0
  51. package/dist/worker/console/doctor.js +169 -0
  52. package/dist/worker/console/draft-store.js +80 -0
  53. package/dist/worker/console/index.js +15 -0
  54. package/dist/worker/console/interview/assessment.js +67 -0
  55. package/dist/worker/console/interview/session.js +100 -0
  56. package/dist/worker/console/interview/tools.js +109 -0
  57. package/dist/worker/console/loopback.js +16 -0
  58. package/dist/worker/console/observe-health-match.js +174 -0
  59. package/dist/worker/console/observe-link.js +33 -0
  60. package/dist/worker/console/operation-runner.js +166 -0
  61. package/dist/worker/console/operation-sse.js +158 -0
  62. package/dist/worker/console/operation-store.js +147 -0
  63. package/dist/worker/console/operator-actions.js +769 -0
  64. package/dist/worker/console/pi-readiness.js +94 -0
  65. package/dist/worker/console/recovery-cta.js +133 -0
  66. package/dist/worker/console/repo-fingerprint.js +29 -0
  67. package/dist/worker/console/resource-loader.js +95 -0
  68. package/dist/worker/console/routes.js +368 -0
  69. package/dist/worker/console/security.js +126 -0
  70. package/dist/worker/console/server.js +149 -0
  71. package/dist/worker/console/sibling-controller.js +28 -0
  72. package/dist/worker/console/static/assets/index-CbnMgdWa.js +9 -0
  73. package/dist/worker/console/static/assets/index-Dnj0RVs8.css +1 -0
  74. package/dist/worker/console/static/index.html +13 -0
  75. package/dist/worker/console/vite.config.js +27 -0
  76. package/dist/worker/delivery/git-transaction.js +43 -8
  77. package/dist/worker/observe/health.js +57 -0
  78. package/dist/worker/observe/paths.js +81 -0
  79. package/dist/worker/observe/routes.js +142 -27
  80. package/dist/worker/observe/spec-evidence.js +84 -0
  81. package/dist/worker/observe/static/api.js +23 -0
  82. package/dist/worker/observe/static/state.js +26 -0
  83. package/dist/worker/observe/static/styles.css +10 -0
  84. package/dist/worker/observe/static/views/dag-inspector.js +173 -6
  85. package/dist/workflows/dag/backend-test-analysis-contract.js +34 -9
  86. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -0
  87. package/dist/workflows/dag/frontend-repair.js +1 -10
  88. package/dist/workflows/dag/init-hybrid.js +372 -115
  89. package/dist/workflows/dag/node-execution.js +57 -6
  90. package/dist/workflows/dag/project-governance-context.js +508 -0
  91. package/dist/workflows/dag/prompt.js +46 -1
  92. package/dist/workflows/dag/retry-policy.js +16 -1
  93. package/dist/workflows/dag/runner.js +9 -0
  94. package/dist/workflows/dag/skill-snapshot.js +1 -0
  95. package/dist/workflows/dag/task-contract-binding.js +138 -0
  96. package/dist/workflows/dag/types.js +84 -10
  97. package/dist/workflows/dag/validate.js +53 -7
  98. package/docs/README.md +2 -0
  99. package/docs/architecture/evolution.md +2 -0
  100. package/docs/architecture/system-overview.md +6 -0
  101. package/docs/architecture/worker-and-feature.md +7 -0
  102. package/docs/templates/agent-dag.schema.json +64 -2
  103. package/docs/templates/agent-dag.supervised-implementation.json +1 -0
  104. package/docs/templates/backend-test-dag.classify.prompt.md +1 -1
  105. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +5 -5
  106. package/docs/templates/backend-test-dag.json +26 -154
  107. package/docs/templates/backend-test-dag.retrospect.prompt.md +1 -1
  108. package/docs/templates/backend-test-dag.review-cases.prompt.md +2 -2
  109. package/package.json +8 -2
  110. package/skills/agent-worker/SKILL.md +1 -0
  111. package/skills/agent-worker/references/agent-worker-operator.md +3 -2
  112. package/skills/frontend-design-review/SKILL.md +25 -16
  113. package/skills/frontend-implementation/references/node-contracts.md +5 -5
  114. package/skills/loop-agent/references/command-reference.md +48 -1
  115. 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: gate.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: gate.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: gate.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: gate.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: gate.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: gate.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: gate.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 fileArgs = gate.fromNodeIds.map((nodeId) => `"\${HARNESS_DAG_RUN_DIR}/${nodeId}.json"`).join(" ");
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 process.argv.slice(2)){",
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}' ${fileArgs}`;
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 || beforeCode !== afterCode) {
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,4 @@
1
+ export * from "./types.js";
2
+ export * from "./registry.js";
3
+ export * from "./envelope.js";
4
+ export * from "./capabilities.js";
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Operator machine envelope types (design §5.1).
3
+ * Physical path: src/shared/operator (allowed via src/shared/**).
4
+ */
5
+ export {};