@tea-agent/loop-agent 0.16.26 → 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 (103) hide show
  1. package/CHANGELOG.md +38 -1
  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 +55 -7
  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 +55 -8
  14. package/dist/executors/shell-presets.js +16 -3
  15. package/dist/shared/operator/capabilities.js +255 -0
  16. package/dist/shared/operator/envelope.js +59 -0
  17. package/dist/shared/operator/index.js +4 -0
  18. package/dist/shared/operator/registry.js +38 -0
  19. package/dist/shared/operator/types.js +5 -0
  20. package/dist/task/contract/adopt.js +166 -0
  21. package/dist/task/contract/apply.js +326 -0
  22. package/dist/task/contract/canonicalize.js +60 -0
  23. package/dist/task/contract/constants.js +29 -0
  24. package/dist/task/contract/diff.js +177 -0
  25. package/dist/task/contract/hash.js +42 -0
  26. package/dist/task/contract/import-revision.js +96 -0
  27. package/dist/task/contract/index.js +17 -0
  28. package/dist/task/contract/journal.js +155 -0
  29. package/dist/task/contract/lock.js +153 -0
  30. package/dist/task/contract/observe.js +296 -0
  31. package/dist/task/contract/paths.js +19 -0
  32. package/dist/task/contract/project.js +170 -0
  33. package/dist/task/contract/recover.js +312 -0
  34. package/dist/task/contract/request-ledger.js +37 -0
  35. package/dist/task/contract/schema.js +151 -0
  36. package/dist/task/contract/transaction.js +160 -0
  37. package/dist/task/contract/types.js +1 -0
  38. package/dist/task/contract/validate-draft.js +106 -0
  39. package/dist/task/index.js +3 -0
  40. package/dist/task/operator/capabilities.js +6 -0
  41. package/dist/task/operator/envelope.js +2 -0
  42. package/dist/task/operator/index.js +5 -0
  43. package/dist/task/operator/registry.js +2 -0
  44. package/dist/task/operator/types.js +1 -0
  45. package/dist/task/runtime.js +5 -1
  46. package/dist/task/source-references.js +7 -0
  47. package/dist/worker/cli.js +150 -32
  48. package/dist/worker/console/app-data.js +185 -0
  49. package/dist/worker/console/dag-confirmation.js +313 -0
  50. package/dist/worker/console/doctor.js +169 -0
  51. package/dist/worker/console/draft-store.js +80 -0
  52. package/dist/worker/console/index.js +15 -0
  53. package/dist/worker/console/interview/assessment.js +67 -0
  54. package/dist/worker/console/interview/session.js +100 -0
  55. package/dist/worker/console/interview/tools.js +109 -0
  56. package/dist/worker/console/loopback.js +16 -0
  57. package/dist/worker/console/observe-health-match.js +174 -0
  58. package/dist/worker/console/observe-link.js +33 -0
  59. package/dist/worker/console/operation-runner.js +166 -0
  60. package/dist/worker/console/operation-sse.js +158 -0
  61. package/dist/worker/console/operation-store.js +147 -0
  62. package/dist/worker/console/operator-actions.js +769 -0
  63. package/dist/worker/console/pi-readiness.js +94 -0
  64. package/dist/worker/console/recovery-cta.js +133 -0
  65. package/dist/worker/console/repo-fingerprint.js +29 -0
  66. package/dist/worker/console/resource-loader.js +95 -0
  67. package/dist/worker/console/routes.js +368 -0
  68. package/dist/worker/console/security.js +126 -0
  69. package/dist/worker/console/server.js +149 -0
  70. package/dist/worker/console/sibling-controller.js +28 -0
  71. package/dist/worker/console/static/assets/index-CbnMgdWa.js +9 -0
  72. package/dist/worker/console/static/assets/index-Dnj0RVs8.css +1 -0
  73. package/dist/worker/console/static/index.html +13 -0
  74. package/dist/worker/console/vite.config.js +27 -0
  75. package/dist/worker/observe/health.js +57 -0
  76. package/dist/worker/observe/routes.js +16 -9
  77. package/dist/workflows/dag/backend-test-analysis-contract.js +34 -9
  78. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -0
  79. package/dist/workflows/dag/frontend-repair.js +1 -10
  80. package/dist/workflows/dag/init-hybrid.js +269 -116
  81. package/dist/workflows/dag/node-execution.js +25 -1
  82. package/dist/workflows/dag/retry-policy.js +16 -1
  83. package/dist/workflows/dag/runner.js +9 -0
  84. package/dist/workflows/dag/task-contract-binding.js +138 -0
  85. package/dist/workflows/dag/types.js +74 -10
  86. package/dist/workflows/dag/validate.js +25 -7
  87. package/docs/README.md +2 -0
  88. package/docs/architecture/evolution.md +2 -0
  89. package/docs/architecture/system-overview.md +6 -0
  90. package/docs/architecture/worker-and-feature.md +7 -0
  91. package/docs/templates/agent-dag.schema.json +49 -2
  92. package/docs/templates/backend-test-dag.classify.prompt.md +1 -1
  93. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +5 -5
  94. package/docs/templates/backend-test-dag.json +26 -154
  95. package/docs/templates/backend-test-dag.retrospect.prompt.md +1 -1
  96. package/docs/templates/backend-test-dag.review-cases.prompt.md +2 -2
  97. package/package.json +8 -2
  98. package/skills/agent-worker/SKILL.md +1 -0
  99. package/skills/agent-worker/references/agent-worker-operator.md +3 -2
  100. package/skills/frontend-design-review/SKILL.md +25 -16
  101. package/skills/frontend-implementation/references/node-contracts.md +5 -5
  102. package/skills/loop-agent/references/command-reference.md +48 -1
  103. package/skills/loop-agent/references/hybrid-dag.md +4 -4
package/CHANGELOG.md CHANGED
@@ -1,10 +1,45 @@
1
+ # 更新日志
2
+
3
+ ## [0.17.0] - 2026-07-22
4
+
5
+ ### 新增
6
+
7
+ - streamline report-first workflow (#53)
8
+ - add operator console, task contract, and harden bindings (#51)
9
+ - condition DAG design and repair branches
10
+
11
+ ### 修复
12
+
13
+ - synchronize console frontend lockfile (#52)
14
+ - tolerate mock strategy format drift
15
+ - bound executor output
16
+
1
17
  ## [Unreleased]
2
18
 
19
+ - 将 `backend-test` runtime 与打包模板收敛为报告优先的 12 节点 DAG:case review 改为 advisory evidence,移除 pytest semantic review/gates 与最终 outcome gate,保留 pytest 前确定性 traceability hard gate、单次 pytest、canonical context/retrospective/L-5;Analysis v2 对 string evidence gap 与 `id` alias 做窄 canonicalize,同时继续严格拒绝未知键、错类型、secret 与多 JSON envelope。
20
+
21
+ ### 修复
22
+
23
+ - DAG runtime 的 task-contract 启动门禁现在只将 `writePolicy: "exclusive"` 视为 writer execution:历史只读/static/shell DAG 可继续执行,legacy writer DAG 仍必须升级为 DagSpec v4 并绑定 managed Task Contract;真实 DAG 生成会冻结当前 managed binding。测试执行 fixture、Worker materializer 和 Windows npm/tmp 路径也已同步适配该契约。
24
+ - Pi CLI / SDK executor 不再把单个节点的完整 stdout 累积为一个 JS 字符串;长 `frontend-plan-pi` 或大段结构化输出会保留有界预览并继续流式解析最终 assistant 事件,避免触发 V8 `Invalid string length` 或卡死。
25
+ - DAG 节点新增 `outputMode: "structured-required"`;前端实现计划/修订节点显式使用该模式,并在 `output-too-large` 时用紧凑结构化输出指令重试,避免直接截断计划后继续执行,也减少长计划输出阻断后续流程的概率。
26
+ - `frontend-mock-assess-pi` 的 `MOCK_STRATEGY:` 协议行规范化现在可处理列表、引用、inline code、中文冒号、`Mock strategy` 标签和标签/值分行等常见格式漂移;仍只接受明确 Mock strategy 标签和五态白名单,不从普通正文猜默认策略。
27
+
3
28
  ### 改进
4
29
 
5
- - Observe 节点检查器“规范证据”页签新增安全文件预览:已绑定任务源和已读取规范文件条目现以可聚焦按钮呈现,点击后在检查器内下钻查看内容(不新增弹窗),提供“返回规范证据”按钮并恢复触发行焦点。新增只读接口 `GET /api/dag-runs/:runId/nodes/:nodeId/spec-evidence/file?source=binding|read&path=...`,服务端重新校验证据成员身份(binding 必须在 `sourceBinding.sources`,read 必须有成功配对 read),拒 `..`/绝对/目录/二进制/仓库外符号链接;正文上限 64KB 并返回截断标志,统一走 Observe 脱敏规则;binding 返回 `sha256`/`currentSha256`/`hashMatch`,read 返回 session event 中实际读取正文与 `readAt`(不以当前工作区文件替代)。
30
+ - **Console Phase 3(Observe 协作)**:
31
+ - Observe `GET /api/health` 升级为 versioned DTO(`schemaVersion: 1`、`repoFingerprint` 复用 Console `repoFingerprintV1`、package name/version、由 ROUTES 派生的 `routeCapabilities`)。
32
+ - Console `observeLink` fail-closed:仅 health **match**(同仓 fingerprint + schema + 目标 kind 所需 capability)时返回深链;offline / mismatch 返回诊断与 `agent-worker observe serve --repo . --port 8787`,不打开错误 URL。
33
+ - Doctor 的 Observe 探测扩展 match / mismatch / unavailable(Observe 仍非阻塞,doctor `ok` 保持)。
34
+ - Recovery CTA 矩阵(report / doctor / decision / resume / reconcile / regenerate / observeLink):**无** Cancel、**无**主 CTA「直接改代码」。
35
+ - Console 与 Observe 仍为独立进程;不 mount、不 proxy、不复制第二套 read model。
36
+ - Phase 1 Console skeleton:`agent-worker console serve|doctor`(默认 loopback `127.0.0.1:8790`、非 loopback fail-closed)、Vite SPA 静态产物 + Node health/static server、boot capability cookie + mutation Host/Origin 门禁、封闭 ResourceLoader 骨架、sibling `loop-agent` identity doctor、`repoFingerprintV1` / Observe 深链 builder、以及 `UPSTREAM.md`(pi-web 源 commit)与预算 report。
37
+ - Phase 0.5 operator surface repair:`buildOperatorCapabilitiesDocument()` 在权威 `src/shared/operator` 上补齐 Console 协商块 `dag`(`generatedSpecVersion: 4`、`requiresTaskContractBinding: true`,与 `dagSpecVersions` 同源常量),并删除冲突的 `src/task/operator` 双实现;CLI `--json` 继续写 `process.stdout`,测试侧改为捕获 stdout。
6
38
 
39
+ - `frontend-implementation` 绿色路径由固定全链改为按需分支:删除重复的首轮 design shell gate,初审通过直接复用原计划,只有 `request-revision` 才执行计划修订与终审;只有可修复失败才执行 repair/reverify/retrace。常规全绿路径因此少执行 3 个 Pi 与 4 个 shell 节点,同时保留 pass-only 写入门禁、验证/评审证据和只读 `frontend-closeout-pi`。原计划/修订计划的结构化合同与需求覆盖使用同一有效来源,且只有主分支产物缺失时才回退;已存在但 malformed/schema-invalid 的产物继续 fail-closed。
40
+ - Observe 节点检查器“规范证据”页签新增安全文件预览:已绑定任务源和已读取规范文件条目现以可聚焦按钮呈现,点击后在检查器内下钻查看内容(不新增弹窗),提供“返回规范证据”按钮并恢复触发行焦点。新增只读接口 `GET /api/dag-runs/:runId/nodes/:nodeId/spec-evidence/file?source=binding|read&path=...`,服务端重新校验证据成员身份(binding 必须在 `sourceBinding.sources`,read 必须有成功配对 read),拒 `..`/绝对/目录/二进制/仓库外符号链接;正文上限 64KB 并返回截断标志,统一走 Observe 脱敏规则;binding 返回 `sha256`/`currentSha256`/`hashMatch`,read 返回 session event 中实际读取正文与 `readAt`(不以当前工作区文件替代)。
7
41
  - 通用 standard/reviewed/supervised 实现 DAG 现在会在目标仓库存在 `AGENTS.md` 时,按本次 writer 的实际变更解析适用的根级/嵌套指令和仓库内代码规范索引:standard 复用 `verify-pi` 与条件 shell gate,reviewed/supervised 复用现有 `review-pi`/review gate,不新增模型节点;无 `AGENTS.md` 或无适用规范时保持原行为,审查节点只读且不会直接改代码。专用测试、前端和知识类 DAG 不受影响。
42
+
8
43
  - GitHub Actions CI 降低私有仓分钟消耗:合并为单 job(避免两次 `npm ci`)、同分支/`PR` concurrency 取消旧 run、Draft PR 跳过完整 CI,且仅忽略 `docs/reports/**`、`docs/progress/**`、`docs/design/archive/**`、`docs/exec-plans/completed/**` 等纯运营文档路径;治理文档与代码门禁仍全量跑。
9
44
  - 明确 **Compatibility / Operator Assist**:openCode 等主会话只编排已发布 `loop-agent` / `agent-worker` CLI 与只读诊断,不得绕过 CLI 直接改业务实现,失败只走 doctor / reconcile / human gate / CLI 重跑。`loop-agent init` 写入的 `AGENTS.md` managed block、包内 skills 与 website 快速开始/治理说明已对齐;架构决策见 `docs/decisions/0005-governed-operator-surface.md`(accepted)。已有目标项目用 `loop-agent init check-update` / `init update --apply-safe` 或 `init reconcile` 刷新 managed block。
10
45
  - 前端测试 DAG 收紧体验:execution preflight 仅硬校验绝对非生产 `baseUrl`;用例 map 缩短为优先用 `playwright-cli` 执行;复盘合并执行证据审查且不依赖 outcome=pass,失败也能出报告。
@@ -22,6 +57,8 @@
22
57
 
23
58
  ### 修复
24
59
 
60
+ - Windows DAG Shell/verification 的 clean environment 现在保留 `APPDATA`,使 Python 3.12/pip 可按系统规则发现用户级 `site-packages` 与其中的 pytest;仍不会继承完整宿主环境或透传未授权 token/credential,backend-test 无需硬编码用户路径。
61
+
25
62
  - Backend-test Analysis v2 intake 现在可确定性归一化模型常见的外部依赖 `type` / `target` 与风险 `type` 字段:dependency `target` 映射为 canonical `name`,扩展字段不落盘;dependency/risk 仍使用类别化白名单,未知字段和 `risks[].target` 继续 fail-closed,避免节点 02 因合法近似形态过早中断。
26
63
 
27
64
  - Backend-test 唯一 JSON object 提取器在扫描裸对象前会等长忽略完整 Markdown 行内代码中的对象形状示例(如 `{error:{code,message,type}}`);唯一真实 envelope 可正常物化,多个真实 JSON、fence 外额外对象与未闭合对象继续 fail-closed。
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  发布包提供两个 CLI:
6
6
 
7
7
  | CLI | 职责 |
8
- |---|---|
8
+ | --- | --- |
9
9
  | `loop-agent` | 单仓库任务、Agent DAG、验证、恢复与治理 |
10
10
  | `agent-worker` | 可选的 Feature、TaskSpec、Task Pool 与 Observe 外层编排 |
11
11
 
@@ -85,13 +85,24 @@ loop-agent dag doctor --run-id <run-id> --markdown
85
85
  loop-agent dag report --run-id <run-id>
86
86
  ```
87
87
 
88
- 启动本地只读看板:
88
+ ### Official 本地操作面(CLI + Console + 独立 Observe)
89
+
90
+ | 入口 | 命令 | 角色 |
91
+ | --- | --- | --- |
92
+ | 受治理 CLI | `loop-agent …` / `agent-worker …` | 权威写入与诊断 |
93
+ | Loop Operator Console | `agent-worker console serve --repo .` | Official 本地控制面(默认 `127.0.0.1:8790`) |
94
+ | Observe | `agent-worker observe serve --repo . --port 8787` | 独立只读看板 |
89
95
 
90
96
  ```bash
97
+ # 终端 A:Console(任务/运行操作面;canonical mutation 只经 sibling loop-agent)
98
+ agent-worker console serve --repo .
99
+ agent-worker console doctor --repo .
100
+
101
+ # 终端 B:独立 Observe(不经 Console mount/proxy)
91
102
  agent-worker observe serve --repo . --port 8787
92
103
  ```
93
104
 
94
- 访问 <http://127.0.0.1:8787/>。Observe 只读取运行事实,不修改 DAG、Task Pool Feature 状态。
105
+ 访问 Console <http://127.0.0.1:8790/> 与 Observe <http://127.0.0.1:8787/>。Observe 只读运行事实;Console 通过 versioned health(同仓 `repoFingerprint` + route capability)fail-closed 构建深链。openCode 等主会话仍是 **Compatibility / Operator Assist**,不得升级为与 Official 同等保证。
95
106
 
96
107
  ## 核心边界
97
108
 
@@ -11,6 +11,8 @@ import { runInspect } from "../commands/inspect.js";
11
11
  import { runInstructions } from "../commands/instructions.js";
12
12
  import { runNewTask } from "../commands/new-task.js";
13
13
  import { runImportPrd } from "../commands/import-prd.js";
14
+ import { runTaskContract } from "../commands/task-contract.js";
15
+ import { runOperator } from "../commands/operator.js";
14
16
  import { runPlanList } from "../commands/plan-list.js";
15
17
  import { runPlanCheck, runPlanComplete, runPlanCreate, } from "../commands/plan.js";
16
18
  import { runPromoteRun } from "../commands/promote-run.js";
@@ -133,6 +135,17 @@ const DAG_SUBCOMMANDS = [
133
135
  "final-verification",
134
136
  "decision",
135
137
  ];
138
+ const TASK_SUBCOMMANDS = ["contract"];
139
+ const TASK_CONTRACT_SUBCOMMANDS = [
140
+ "show",
141
+ "validate",
142
+ "diff",
143
+ "apply",
144
+ "adopt",
145
+ "doctor",
146
+ "recover",
147
+ ];
148
+ const OPERATOR_SUBCOMMANDS = ["capabilities"];
136
149
  const WORKFLOW_SUBCOMMANDS = [
137
150
  "list",
138
151
  "inspect",
@@ -250,6 +263,36 @@ export const COMMAND_DEFINITIONS = [
250
263
  await runImportPrd(repoRoot, [subcommand, ...rest].filter(Boolean));
251
264
  },
252
265
  },
266
+ {
267
+ name: "task",
268
+ adapter: "required",
269
+ tier: "primary",
270
+ intent: "Task Contract application boundary: show/validate/diff/apply/adopt/doctor/recover managed contract facts.",
271
+ usage: "task contract <show|validate|diff|apply|adopt|doctor|recover> ...",
272
+ subcommands: [...TASK_SUBCOMMANDS],
273
+ handler: async ({ repoRoot, subcommand, rest }) => {
274
+ if (subcommand !== "contract") {
275
+ throw new Error(formatSubcommandUsageError("task", TASK_SUBCOMMANDS));
276
+ }
277
+ const [contractSub, ...contractRest] = rest;
278
+ if (!contractSub ||
279
+ !TASK_CONTRACT_SUBCOMMANDS.includes(contractSub)) {
280
+ throw new Error(`usage: task contract <${TASK_CONTRACT_SUBCOMMANDS.join("|")}> ...`);
281
+ }
282
+ await runTaskContract(repoRoot, [contractSub, ...contractRest]);
283
+ },
284
+ },
285
+ {
286
+ name: "operator",
287
+ adapter: "required",
288
+ tier: "operator",
289
+ intent: "Operator machine envelope capabilities registry for Console/CLI negotiation.",
290
+ usage: "operator capabilities [--json]",
291
+ subcommands: [...OPERATOR_SUBCOMMANDS],
292
+ handler: async ({ repoRoot, subcommand, rest }) => {
293
+ await runOperator(repoRoot, [subcommand, ...rest].filter((arg) => Boolean(arg)));
294
+ },
295
+ },
253
296
  {
254
297
  name: "status",
255
298
  adapter: "required",
@@ -38,6 +38,8 @@ import { runLoop } from "../commands/loop.js";
38
38
  import { runLoopBenchmark } from "../commands/loop-benchmark.js";
39
39
  import { runNewTask } from "../commands/new-task.js";
40
40
  import { runImportPrd } from "../commands/import-prd.js";
41
+ import { runTaskContract } from "../commands/task-contract.js";
42
+ import { runOperator } from "../commands/operator.js";
41
43
  import { runPiReuseBenchmark } from "../commands/pi-reuse-benchmark.js";
42
44
  import { parsePiPromptArgs, printPiPromptUsage, runPiPrompt, } from "../commands/pi-prompt.js";
43
45
  import { runPlanList } from "../commands/plan-list.js";
@@ -140,6 +142,30 @@ async function runCommanderAction(ctx, command, subcommand, rest) {
140
142
  case "import-prd":
141
143
  await runImportPrd(ctx.repoRoot, compactArgs([subcommand, ...rest]));
142
144
  return;
145
+ case "task": {
146
+ if (subcommand !== "contract") {
147
+ throw new Error("usage: task <contract> ...");
148
+ }
149
+ const [contractSub, ...contractRest] = rest;
150
+ const allowed = [
151
+ "show",
152
+ "validate",
153
+ "diff",
154
+ "apply",
155
+ "adopt",
156
+ "doctor",
157
+ "recover",
158
+ ];
159
+ if (!contractSub ||
160
+ !allowed.includes(contractSub)) {
161
+ throw new Error(`usage: task contract <${allowed.join("|")}> ...`);
162
+ }
163
+ await runTaskContract(ctx.repoRoot, [contractSub, ...contractRest]);
164
+ return;
165
+ }
166
+ case "operator":
167
+ await runOperator(ctx.repoRoot, compactArgs([subcommand, ...rest]));
168
+ return;
143
169
  case "status": {
144
170
  if (!subcommand)
145
171
  throw new Error("taskId required");
@@ -51,6 +51,10 @@ export function parseDagApproveArgs(args) {
51
51
  export async function runDagApprove(repoRoot, rawArgs) {
52
52
  const parsed = parseDagApproveArgs(rawArgs);
53
53
  const { runDir, state, spec } = await requirePausedDagRun(repoRoot, parsed.runId);
54
+ // Phase 0.5 migration matrix: paused v3 cannot approve new writer paths.
55
+ const { assertDagSpecAllowsNewWriterExecution, assertTaskContractBindingConsistent, } = await import("../workflows/dag/task-contract-binding.js");
56
+ assertDagSpecAllowsNewWriterExecution(spec);
57
+ await assertTaskContractBindingConsistent({ repoRoot, spec });
54
58
  const nodeId = state.pausedByNodeId;
55
59
  const node = state.nodes[nodeId];
56
60
  if (!node) {
@@ -26,6 +26,7 @@ export function parseDagResumeArgs(args) {
26
26
  }
27
27
  export async function runDagResume(repoRoot, rawArgs) {
28
28
  const parsed = parseDagResumeArgs(rawArgs);
29
+ // resumeDagRun loads the frozen run spec and revalidates taskContractBinding.
29
30
  const summary = await resumeDagRun({
30
31
  cwd: repoRoot,
31
32
  runId: parsed.runId,
@@ -7,6 +7,12 @@ export async function runDagValidate(repoRoot, rawArgs) {
7
7
  repoRoot,
8
8
  ...parsed,
9
9
  });
10
+ // Phase 0.5: revalidate live taskContractBinding for v4 specs.
11
+ // Fail closed on any binding load/check error — do not swallow unexpected exceptions.
12
+ const { loadDagSpecFromFile } = await import("../workflows/dag/runner.js");
13
+ const { assertTaskContractBindingConsistent } = await import("../workflows/dag/task-contract-binding.js");
14
+ const spec = await loadDagSpecFromFile(parsed.dagPath);
15
+ await assertTaskContractBindingConsistent({ repoRoot, spec });
10
16
  console.log(JSON.stringify({
11
17
  mode: "validate",
12
18
  ...result,
@@ -0,0 +1,44 @@
1
+ import { buildOperatorCapabilitiesDocument, operatorFailed, operatorSucceeded, processExitCodeForOutcome, writeOperatorJson, } from "../shared/operator/index.js";
2
+ export function parseOperatorArgs(args) {
3
+ const options = {};
4
+ const positional = [];
5
+ for (const token of args) {
6
+ if (token === "--json") {
7
+ options.json = true;
8
+ continue;
9
+ }
10
+ if (token.startsWith("-")) {
11
+ throw new Error(`unknown flag ${token}\nusage: operator capabilities --json`);
12
+ }
13
+ positional.push(token);
14
+ }
15
+ const subcommand = positional[0];
16
+ if (!subcommand) {
17
+ throw new Error("usage: operator <capabilities> --json");
18
+ }
19
+ return { subcommand, options };
20
+ }
21
+ export async function runOperator(_repoRoot, args) {
22
+ const { subcommand, options } = parseOperatorArgs(args);
23
+ if (subcommand !== "capabilities") {
24
+ const result = operatorFailed({
25
+ command: "operator",
26
+ outcome: "invalid",
27
+ code: "INVALID_INPUT",
28
+ message: `unknown operator subcommand: ${subcommand}`,
29
+ });
30
+ emit(result, options.json !== false);
31
+ process.exitCode = processExitCodeForOutcome(result.outcome);
32
+ return;
33
+ }
34
+ const doc = buildOperatorCapabilitiesDocument();
35
+ const result = operatorSucceeded("operator capabilities", doc);
36
+ emit(result, true);
37
+ }
38
+ function emit(result, json) {
39
+ if (json) {
40
+ writeOperatorJson(result);
41
+ return;
42
+ }
43
+ console.log(JSON.stringify(result, null, 2));
44
+ }
@@ -0,0 +1,271 @@
1
+ import { ContractMutationError, adoptTaskContract, applyTaskContract, diffTaskContract, doctorTaskContract, loadDraftFromPath, observeTaskContractState, recoverTaskContract, validateDraftForTask, } from "../task/contract/index.js";
2
+ import { operatorFailed, operatorSucceeded, processExitCodeForOutcome, writeOperatorJson, } from "../shared/operator/index.js";
3
+ const USAGE = `usage:
4
+ task contract show <taskId> [--json]
5
+ task contract validate --task <id> --input <path> [--json]
6
+ task contract diff --task <id> --input <path> [--json]
7
+ task contract apply --task <id> --input <path> --expected-revision <n> --expected-observed-hash <sha256> --request-id <id> --request-payload-sha256 <sha256> [--json]
8
+ task contract adopt --task <id> --expected-revision <n> --expected-observed-hash <sha256> --request-id <id> --request-payload-sha256 <sha256> [--json]
9
+ task contract doctor --task <id> [--json]
10
+ task contract recover --task <id> [--tx-id <id>] [--json]`;
11
+ export function parseTaskContractArgs(args) {
12
+ const options = {};
13
+ const positional = [];
14
+ for (let i = 0; i < args.length; i += 1) {
15
+ const token = args[i];
16
+ if (token === "--json") {
17
+ options.json = true;
18
+ continue;
19
+ }
20
+ if (token === "--task") {
21
+ options.task = args[i + 1];
22
+ i += 1;
23
+ continue;
24
+ }
25
+ if (token === "--input") {
26
+ options.input = args[i + 1];
27
+ i += 1;
28
+ continue;
29
+ }
30
+ if (token === "--expected-revision") {
31
+ const raw = args[i + 1];
32
+ const n = Number(raw);
33
+ if (!Number.isInteger(n) || n < 0) {
34
+ throw new Error(`invalid --expected-revision: ${raw}`);
35
+ }
36
+ options.expectedRevision = n;
37
+ i += 1;
38
+ continue;
39
+ }
40
+ if (token === "--expected-observed-hash") {
41
+ options.expectedObservedHash = args[i + 1];
42
+ i += 1;
43
+ continue;
44
+ }
45
+ if (token === "--request-id") {
46
+ options.requestId = args[i + 1];
47
+ i += 1;
48
+ continue;
49
+ }
50
+ if (token === "--request-payload-sha256") {
51
+ options.requestPayloadSha256 = args[i + 1];
52
+ i += 1;
53
+ continue;
54
+ }
55
+ if (token === "--tx-id") {
56
+ options.txId = args[i + 1];
57
+ i += 1;
58
+ continue;
59
+ }
60
+ if (token.startsWith("-")) {
61
+ throw new Error(`unknown flag ${token}\n${USAGE}`);
62
+ }
63
+ positional.push(token);
64
+ }
65
+ const subcommand = positional[0];
66
+ if (!subcommand) {
67
+ throw new Error(USAGE);
68
+ }
69
+ // show takes positional taskId; others prefer --task
70
+ let taskId = options.task;
71
+ if (subcommand === "show") {
72
+ taskId = positional[1] ?? options.task;
73
+ }
74
+ return { subcommand, taskId, options };
75
+ }
76
+ function mutationOutcome(code) {
77
+ switch (code) {
78
+ case "REVISION_CONFLICT":
79
+ case "OBSERVED_HASH_CONFLICT":
80
+ case "REQUEST_ID_REUSE_CONFLICT":
81
+ case "LEASE_HELD":
82
+ return "conflict";
83
+ case "NOT_FOUND":
84
+ return "not-found";
85
+ case "TRANSACTION_INCOMPLETE":
86
+ case "OPERATION_NEEDS_RECONCILE":
87
+ return "needs-reconcile";
88
+ case "EXTERNALLY_MODIFIED":
89
+ case "NOT_MANAGED":
90
+ case "BINDING_DRIFT":
91
+ return "blocked";
92
+ case "INVALID_INPUT":
93
+ case "PATH_OUTSIDE_REPO":
94
+ case "UNSUPPORTED_SCHEMA_VERSION":
95
+ return "invalid";
96
+ default:
97
+ return "rejected";
98
+ }
99
+ }
100
+ function emit(result, json) {
101
+ // Machine envelope is the default for task contract when --json is set;
102
+ // without --json still print the same document for dogfood consistency.
103
+ writeOperatorJson(result);
104
+ if (!json) {
105
+ // already wrote JSON; human path same for Phase 0.5
106
+ }
107
+ process.exitCode = processExitCodeForOutcome(result.outcome);
108
+ }
109
+ export async function runTaskContract(repoRoot, args) {
110
+ let subcommand;
111
+ let taskId;
112
+ let options;
113
+ try {
114
+ ({ subcommand, taskId, options } = parseTaskContractArgs(args));
115
+ }
116
+ catch (error) {
117
+ const result = operatorFailed({
118
+ command: "task contract",
119
+ outcome: "invalid",
120
+ code: "INVALID_INPUT",
121
+ message: error instanceof Error ? error.message : String(error),
122
+ });
123
+ emit(result, true);
124
+ return;
125
+ }
126
+ const command = `task contract ${subcommand}`;
127
+ try {
128
+ switch (subcommand) {
129
+ case "show": {
130
+ if (!taskId) {
131
+ throw new ContractMutationError("INVALID_INPUT", "task contract show requires <taskId>");
132
+ }
133
+ const state = await observeTaskContractState(repoRoot, taskId);
134
+ const result = operatorSucceeded(command, { taskId, state });
135
+ emit(result, options.json);
136
+ return;
137
+ }
138
+ case "validate": {
139
+ if (!taskId || !options.input) {
140
+ throw new ContractMutationError("INVALID_INPUT", "task contract validate requires --task and --input");
141
+ }
142
+ const { draft } = await loadDraftFromPath(options.input, repoRoot);
143
+ const validation = await validateDraftForTask({
144
+ repoRoot,
145
+ taskId,
146
+ draft,
147
+ });
148
+ if (!validation.ok) {
149
+ const result = operatorFailed({
150
+ command,
151
+ outcome: "invalid",
152
+ code: "INVALID_INPUT",
153
+ message: validation.errors.join("; "),
154
+ details: { errors: validation.errors },
155
+ });
156
+ emit(result, options.json);
157
+ return;
158
+ }
159
+ const result = operatorSucceeded(command, {
160
+ taskId,
161
+ valid: true,
162
+ warnings: validation.warnings,
163
+ });
164
+ emit(result, options.json);
165
+ return;
166
+ }
167
+ case "diff": {
168
+ if (!taskId || !options.input) {
169
+ throw new ContractMutationError("INVALID_INPUT", "task contract diff requires --task and --input");
170
+ }
171
+ const { draft } = await loadDraftFromPath(options.input, repoRoot);
172
+ const diff = await diffTaskContract({ repoRoot, taskId, draft });
173
+ const result = operatorSucceeded(command, diff);
174
+ emit(result, options.json);
175
+ return;
176
+ }
177
+ case "apply": {
178
+ if (!taskId ||
179
+ !options.input ||
180
+ options.expectedRevision === undefined ||
181
+ !options.expectedObservedHash ||
182
+ !options.requestId ||
183
+ !options.requestPayloadSha256) {
184
+ throw new ContractMutationError("INVALID_INPUT", "task contract apply requires --task --input --expected-revision --expected-observed-hash --request-id --request-payload-sha256");
185
+ }
186
+ const loaded = await loadDraftFromPath(options.input, repoRoot);
187
+ // Client-supplied payload hash must match file bytes
188
+ if (loaded.payloadSha256 !== options.requestPayloadSha256) {
189
+ throw new ContractMutationError("INVALID_INPUT", `request-payload-sha256 does not match input file hash (expected ${loaded.payloadSha256})`);
190
+ }
191
+ const applyResult = await applyTaskContract({
192
+ repoRoot,
193
+ taskId,
194
+ draft: loaded.draft,
195
+ expectedRevision: options.expectedRevision,
196
+ expectedObservedHash: options.expectedObservedHash,
197
+ requestId: options.requestId,
198
+ requestPayloadSha256: options.requestPayloadSha256,
199
+ rawDraftText: loaded.rawText,
200
+ });
201
+ const result = operatorSucceeded(command, applyResult);
202
+ emit(result, options.json);
203
+ return;
204
+ }
205
+ case "adopt": {
206
+ if (!taskId ||
207
+ options.expectedRevision === undefined ||
208
+ !options.expectedObservedHash ||
209
+ !options.requestId ||
210
+ !options.requestPayloadSha256) {
211
+ throw new ContractMutationError("INVALID_INPUT", "task contract adopt requires --task --expected-revision --expected-observed-hash --request-id --request-payload-sha256");
212
+ }
213
+ const adoptResult = await adoptTaskContract({
214
+ repoRoot,
215
+ taskId,
216
+ expectedRevision: options.expectedRevision,
217
+ expectedObservedHash: options.expectedObservedHash,
218
+ requestId: options.requestId,
219
+ requestPayloadSha256: options.requestPayloadSha256,
220
+ });
221
+ const result = operatorSucceeded(command, adoptResult);
222
+ emit(result, options.json);
223
+ return;
224
+ }
225
+ case "doctor": {
226
+ if (!taskId) {
227
+ throw new ContractMutationError("INVALID_INPUT", "task contract doctor requires --task");
228
+ }
229
+ const doctor = await doctorTaskContract({ repoRoot, taskId });
230
+ const result = operatorSucceeded(command, doctor);
231
+ emit(result, options.json);
232
+ return;
233
+ }
234
+ case "recover": {
235
+ if (!taskId) {
236
+ throw new ContractMutationError("INVALID_INPUT", "task contract recover requires --task");
237
+ }
238
+ const recover = await recoverTaskContract({
239
+ repoRoot,
240
+ taskId,
241
+ txId: options.txId,
242
+ });
243
+ const result = operatorSucceeded(command, recover);
244
+ emit(result, options.json);
245
+ return;
246
+ }
247
+ default:
248
+ throw new ContractMutationError("INVALID_INPUT", `unknown task contract subcommand: ${subcommand}\n${USAGE}`);
249
+ }
250
+ }
251
+ catch (error) {
252
+ if (error instanceof ContractMutationError) {
253
+ const result = operatorFailed({
254
+ command,
255
+ outcome: mutationOutcome(error.code),
256
+ code: error.code,
257
+ message: error.message,
258
+ details: error.details,
259
+ });
260
+ emit(result, options.json);
261
+ return;
262
+ }
263
+ const result = operatorFailed({
264
+ command,
265
+ outcome: "rejected",
266
+ code: "INTERNAL_ERROR",
267
+ message: error instanceof Error ? error.message : String(error),
268
+ });
269
+ emit(result, options.json);
270
+ }
271
+ }
@@ -353,11 +353,18 @@ function canonicalizeProtocolFirstLine(assistantText, firstProtocolLine) {
353
353
  return assistantText;
354
354
  const lines = assistantText.split(/\r?\n/);
355
355
  let protocolIndex = -1;
356
+ let protocolNextLineIndex = -1;
356
357
  let protocolLine = "";
357
358
  for (const [index, line] of lines.entries()) {
358
- const normalized = normalizeProtocolLine(line);
359
- if (normalized.startsWith(firstProtocolLine)) {
359
+ const nextLine = lines[index + 1];
360
+ const normalized = normalizeProtocolLine(line, firstProtocolLine, nextLine);
361
+ if (normalized !== undefined) {
360
362
  protocolIndex = index;
363
+ if (normalizeProtocolLine(line, firstProtocolLine) === undefined &&
364
+ nextLine !== undefined &&
365
+ normalizedProtocolValue(nextLine) !== "") {
366
+ protocolNextLineIndex = index + 1;
367
+ }
361
368
  protocolLine = normalized;
362
369
  break;
363
370
  }
@@ -365,10 +372,14 @@ function canonicalizeProtocolFirstLine(assistantText, firstProtocolLine) {
365
372
  if (protocolIndex < 0)
366
373
  return assistantText;
367
374
  if (protocolIndex === 0) {
368
- return [protocolLine, ...lines.slice(1)].join("\n");
375
+ const remaining = lines.slice(protocolNextLineIndex === 1 ? 2 : 1);
376
+ return [protocolLine, ...remaining].join("\n");
369
377
  }
370
378
  const before = lines.slice(0, protocolIndex);
371
379
  const after = lines.slice(protocolIndex + 1);
380
+ if (protocolNextLineIndex === protocolIndex + 1) {
381
+ after.shift();
382
+ }
372
383
  while (before.at(-1)?.trim() === "" &&
373
384
  after.at(0)?.trim() === "") {
374
385
  after.shift();
@@ -382,10 +393,47 @@ function canonicalizeProtocolFirstLine(assistantText, firstProtocolLine) {
382
393
  ? `${protocolLine}\n\n${bodyLines.join("\n")}`
383
394
  : protocolLine;
384
395
  }
385
- function normalizeProtocolLine(line) {
386
- const trimmed = line.trim();
387
- const emphasized = trimmed.match(/^(\*{1,3})\s*(.*?)\s*\1$/);
388
- return (emphasized?.[2] ?? trimmed).trim();
396
+ function normalizeProtocolLine(line, firstProtocolLine, nextLine) {
397
+ const normalizedLine = stripMarkdownLineDecorations(line);
398
+ if (!firstProtocolLine)
399
+ return normalizedLine;
400
+ const label = firstProtocolLine.replace(/[::]\s*$/, "").trim();
401
+ const labelPattern = label
402
+ .split(/[_\-\s]+/)
403
+ .filter(Boolean)
404
+ .map(escapeRegExp)
405
+ .join("[_\\-\\s]+");
406
+ if (!labelPattern)
407
+ return undefined;
408
+ const direct = normalizedLine.match(new RegExp(`^${labelPattern}\\s*[::]\\s*(.+)$`, "i"));
409
+ if (direct) {
410
+ const value = normalizedProtocolValue(direct[1] ?? "");
411
+ return value ? `${firstProtocolLine} ${value}` : undefined;
412
+ }
413
+ const splitValue = normalizedLine.match(new RegExp(`^${labelPattern}\\s*[::]?\\s*$`, "i"));
414
+ if (splitValue && nextLine !== undefined) {
415
+ const value = normalizedProtocolValue(nextLine);
416
+ return value ? `${firstProtocolLine} ${value}` : undefined;
417
+ }
418
+ return undefined;
419
+ }
420
+ function normalizedProtocolValue(value) {
421
+ let normalized = stripMarkdownLineDecorations(value);
422
+ normalized = normalized.replace(/^[::]\s*/, "").trim();
423
+ normalized = normalized.replace(/[。.!;;,,]+$/u, "").trim();
424
+ normalized = stripMarkdownLineDecorations(normalized);
425
+ return normalized;
426
+ }
427
+ function stripMarkdownLineDecorations(line) {
428
+ let trimmed = line.trim();
429
+ trimmed = trimmed.replace(/^(?:>\s*)+/, "");
430
+ trimmed = trimmed.replace(/^(?:[-*+]\s+|\d+[.)]\s+)/, "");
431
+ const emphasized = trimmed.match(/^(\*{1,3}|_{1,3}|`{1,3})\s*(.*?)\s*\1$/);
432
+ trimmed = (emphasized?.[2] ?? trimmed).trim();
433
+ return trimmed;
434
+ }
435
+ function escapeRegExp(value) {
436
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
389
437
  }
390
438
  /**
391
439
  * Validate the writer's observed diff against its declared write boundary.