@tea-agent/loop-agent 0.16.26 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/CHANGELOG.md +45 -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 +266 -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-BEIdBogJ.js +9 -0
  72. package/dist/worker/console/static/assets/index-Rt0TqimP.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/materialize/harness-task-materializer.js +34 -0
  76. package/dist/worker/observe/health.js +57 -0
  77. package/dist/worker/observe/routes.js +16 -9
  78. package/dist/workflows/dag/backend-test-analysis-contract.js +34 -9
  79. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -0
  80. package/dist/workflows/dag/frontend-repair.js +1 -10
  81. package/dist/workflows/dag/init-hybrid.js +269 -116
  82. package/dist/workflows/dag/node-execution.js +25 -1
  83. package/dist/workflows/dag/retry-policy.js +16 -1
  84. package/dist/workflows/dag/runner.js +9 -0
  85. package/dist/workflows/dag/task-contract-binding.js +138 -0
  86. package/dist/workflows/dag/types.js +74 -10
  87. package/dist/workflows/dag/validate.js +25 -7
  88. package/docs/README.md +2 -0
  89. package/docs/architecture/evolution.md +2 -0
  90. package/docs/architecture/system-overview.md +6 -0
  91. package/docs/architecture/worker-and-feature.md +7 -0
  92. package/docs/templates/agent-dag.schema.json +49 -2
  93. package/docs/templates/backend-test-dag.classify.prompt.md +1 -1
  94. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +5 -5
  95. package/docs/templates/backend-test-dag.json +26 -154
  96. package/docs/templates/backend-test-dag.retrospect.prompt.md +1 -1
  97. package/docs/templates/backend-test-dag.review-cases.prompt.md +2 -2
  98. package/package.json +8 -2
  99. package/skills/agent-worker/SKILL.md +1 -0
  100. package/skills/agent-worker/references/agent-worker-operator.md +3 -2
  101. package/skills/frontend-design-review/SKILL.md +25 -16
  102. package/skills/frontend-implementation/references/node-contracts.md +5 -5
  103. package/skills/loop-agent/references/command-reference.md +48 -1
  104. package/skills/loop-agent/references/hybrid-dag.md +4 -4
@@ -0,0 +1,769 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { buildOperatorCapabilitiesDocument } from "../../shared/operator/capabilities.js";
3
+ import { operatorFailed, operatorSucceeded, } from "../../shared/operator/envelope.js";
4
+ import { stageUtf8Text } from "./app-data.js";
5
+ import { ALL_CONFIRMATION_CHALLENGES, hashDagBytes, } from "./dag-confirmation.js";
6
+ import { capabilityForObserveTarget, DEFAULT_OBSERVE_BASE_URL, OBSERVE_START_COMMAND, probeAndClassifyObserve, } from "./observe-health-match.js";
7
+ import { buildObserveDeepLink, } from "./observe-link.js";
8
+ import { repoFingerprintV1 } from "./repo-fingerprint.js";
9
+ import { scheduleOperation, } from "./operation-runner.js";
10
+ import { readinessGateFailure, } from "./pi-readiness.js";
11
+ import { isAssessmentFreshForDraft } from "./interview/assessment.js";
12
+ const MUTATION_OR_LONG = new Set([
13
+ "newTask",
14
+ "importPrd",
15
+ "contractApply",
16
+ "contractAdopt",
17
+ "contractRecover",
18
+ "dagRunTask",
19
+ "runDag",
20
+ ]);
21
+ function paramsOf(req) {
22
+ return (req.actionParams ?? {});
23
+ }
24
+ function str(v) {
25
+ return typeof v === "string" && v.trim() ? v.trim() : undefined;
26
+ }
27
+ async function runReadCli(ctx, args, command) {
28
+ const run = ctx.runCommand ?? ((a, o) => ctx.client.run(a, o));
29
+ try {
30
+ const result = await run(args, {
31
+ cwd: ctx.repoRoot,
32
+ artifactName: `read-${command.replace(/\s+/g, "-")}`,
33
+ expectJson: true,
34
+ });
35
+ if (result.json && typeof result.json === "object") {
36
+ const env = result.json;
37
+ if (env.schemaVersion === 1 && typeof env.ok === "boolean") {
38
+ return env;
39
+ }
40
+ }
41
+ if (result.ok && result.exitCode === 0) {
42
+ return operatorSucceeded(command, {
43
+ stdout: result.stdout,
44
+ raw: result.json,
45
+ });
46
+ }
47
+ return operatorFailed({
48
+ command,
49
+ outcome: "rejected",
50
+ code: "INTERNAL_ERROR",
51
+ message: result.stderr || `exit ${result.exitCode}`,
52
+ });
53
+ }
54
+ catch (error) {
55
+ return operatorFailed({
56
+ command,
57
+ outcome: "needs-reconcile",
58
+ code: "OPERATION_NEEDS_RECONCILE",
59
+ message: error instanceof Error ? error.message : String(error),
60
+ });
61
+ }
62
+ }
63
+ async function acceptOperation(ctx, input) {
64
+ const created = await ctx.operations.create({
65
+ clientRequestId: input.clientRequestId,
66
+ action: input.action,
67
+ actionParams: input.actionParams,
68
+ taskId: input.taskId,
69
+ cliArgs: input.cliArgs,
70
+ });
71
+ if (created.kind === "conflict") {
72
+ return {
73
+ kind: "error",
74
+ status: 409,
75
+ body: operatorFailed({
76
+ command: input.action,
77
+ outcome: "conflict",
78
+ code: "REQUEST_ID_REUSE_CONFLICT",
79
+ message: created.message,
80
+ details: { operationId: created.existing.operationId },
81
+ }),
82
+ };
83
+ }
84
+ if (created.kind === "idempotent") {
85
+ return {
86
+ kind: "accepted",
87
+ status: 202,
88
+ body: {
89
+ operationId: created.operation.operationId,
90
+ state: created.operation.state,
91
+ clientRequestId: created.operation.clientRequestId,
92
+ action: created.operation.action,
93
+ },
94
+ };
95
+ }
96
+ const runnerDeps = {
97
+ store: ctx.operations,
98
+ events: ctx.events,
99
+ client: ctx.client,
100
+ repoRoot: ctx.repoRoot,
101
+ runCommand: ctx.runCommand,
102
+ };
103
+ scheduleOperation(created.operation.operationId, runnerDeps);
104
+ return {
105
+ kind: "accepted",
106
+ status: 202,
107
+ body: {
108
+ operationId: created.operation.operationId,
109
+ state: created.operation.state,
110
+ clientRequestId: created.operation.clientRequestId,
111
+ action: created.operation.action,
112
+ },
113
+ };
114
+ }
115
+ export async function dispatchOperatorAction(ctx, req) {
116
+ const action = req.action?.trim();
117
+ if (!action) {
118
+ return {
119
+ kind: "error",
120
+ status: 400,
121
+ body: operatorFailed({
122
+ command: "operator",
123
+ outcome: "invalid",
124
+ code: "INVALID_INPUT",
125
+ message: "action is required",
126
+ }),
127
+ };
128
+ }
129
+ const readiness = ctx.getReadiness?.() ?? ctx.readiness;
130
+ const blocked = readinessGateFailure(readiness, action);
131
+ if (blocked) {
132
+ return {
133
+ kind: "error",
134
+ status: 412,
135
+ body: operatorFailed({
136
+ command: action,
137
+ outcome: "blocked",
138
+ code: "PI_SETUP_REQUIRED",
139
+ message: blocked.message,
140
+ details: { state: blocked.state },
141
+ }),
142
+ };
143
+ }
144
+ const p = paramsOf(req);
145
+ switch (action) {
146
+ case "operatorCapabilities": {
147
+ const doc = buildOperatorCapabilitiesDocument();
148
+ return {
149
+ kind: "sync",
150
+ status: 200,
151
+ body: operatorSucceeded("operator capabilities", doc),
152
+ };
153
+ }
154
+ case "inspect": {
155
+ const body = await runReadCli(ctx, ["inspect", "--json"], "inspect");
156
+ return { kind: "sync", status: body.ok ? 200 : 400, body };
157
+ }
158
+ case "doctor": {
159
+ const body = await runReadCli(ctx, ["doctor", "--json"], "doctor");
160
+ return { kind: "sync", status: body.ok ? 200 : 400, body };
161
+ }
162
+ case "status": {
163
+ const taskId = str(p.taskId);
164
+ if (!taskId) {
165
+ return invalid(action, "taskId is required");
166
+ }
167
+ const body = await runReadCli(ctx, ["status", taskId, "--json"], "status");
168
+ return {
169
+ kind: "sync",
170
+ status: body.ok ? 200 : body.outcome === "not-found" ? 404 : 400,
171
+ body,
172
+ };
173
+ }
174
+ case "spineCheck": {
175
+ const taskId = str(p.taskId);
176
+ if (!taskId)
177
+ return invalid(action, "taskId is required");
178
+ const body = await runReadCli(ctx, ["spine", "audit", taskId, "--json"], "spine audit");
179
+ return { kind: "sync", status: body.ok ? 200 : 400, body };
180
+ }
181
+ case "sourceInstructions": {
182
+ const taskId = str(p.taskId);
183
+ if (!taskId)
184
+ return invalid(action, "taskId is required");
185
+ const body = await runReadCli(ctx, ["instructions", "source", "--task", taskId, "--json"], "instructions source");
186
+ return { kind: "sync", status: body.ok ? 200 : 400, body };
187
+ }
188
+ case "contractShow": {
189
+ const taskId = str(p.taskId);
190
+ if (!taskId)
191
+ return invalid(action, "taskId is required");
192
+ const body = await runReadCli(ctx, ["task", "contract", "show", taskId, "--json"], "task contract show");
193
+ return {
194
+ kind: "sync",
195
+ status: body.ok ? 200 : body.outcome === "not-found" ? 404 : 400,
196
+ body,
197
+ };
198
+ }
199
+ case "contractValidate":
200
+ case "contractDiff":
201
+ return dispatchContractValidateOrDiff(ctx, action, p);
202
+ case "contractDraftSave": {
203
+ const draft = p.draft;
204
+ const taskId = str(p.taskId) ?? str(draft?.taskId);
205
+ if (!taskId || !draft) {
206
+ return invalid(action, "taskId and draft are required");
207
+ }
208
+ const { schemaVersion: _sv, taskId: _tid, ...draftRest } = draft;
209
+ const saved = await ctx.drafts.save({
210
+ ...draftRest,
211
+ schemaVersion: 1,
212
+ taskId,
213
+ title: str(draft.title) ?? taskId,
214
+ });
215
+ return {
216
+ kind: "sync",
217
+ status: 200,
218
+ body: operatorSucceeded("contractDraftSave", {
219
+ taskId: saved.draft.taskId,
220
+ draftSha256: saved.draftSha256,
221
+ path: saved.path,
222
+ // never claim canonical
223
+ canonical: false,
224
+ storage: "console-application-data",
225
+ }),
226
+ };
227
+ }
228
+ case "observeLink": {
229
+ const baseUrl = ctx.observeBaseUrl ?? DEFAULT_OBSERVE_BASE_URL;
230
+ const dagRunId = str(p.dagRunId);
231
+ const taskId = str(p.taskId);
232
+ const featureId = str(p.featureId);
233
+ const workerRunId = str(p.workerRunId);
234
+ let target;
235
+ if (dagRunId) {
236
+ target = { kind: "dag", dagRunId };
237
+ }
238
+ else if (taskId) {
239
+ target = { kind: "task", taskId };
240
+ }
241
+ else if (featureId) {
242
+ target = { kind: "feature", featureId };
243
+ }
244
+ else if (workerRunId) {
245
+ target = { kind: "worker", workerRunId };
246
+ }
247
+ else {
248
+ return invalid(action, "dagRunId, taskId, featureId, or workerRunId required");
249
+ }
250
+ const requiredCapability = capabilityForObserveTarget(target);
251
+ let localFingerprint;
252
+ try {
253
+ localFingerprint = repoFingerprintV1(ctx.repoRoot);
254
+ }
255
+ catch (error) {
256
+ return {
257
+ kind: "sync",
258
+ status: 400,
259
+ body: operatorFailed({
260
+ command: "observeLink",
261
+ outcome: "rejected",
262
+ code: "OBSERVE_LINK_BLOCKED",
263
+ message: `cannot compute local repoFingerprint: ${error instanceof Error ? error.message : String(error)}`,
264
+ details: {
265
+ status: "mismatch",
266
+ startCommand: OBSERVE_START_COMMAND,
267
+ },
268
+ }),
269
+ };
270
+ }
271
+ const classified = await probeAndClassifyObserve({
272
+ baseUrl,
273
+ localFingerprint,
274
+ requiredCapability,
275
+ fetchImpl: ctx.fetchImpl ?? fetch,
276
+ });
277
+ if (classified.status !== "match") {
278
+ return {
279
+ kind: "sync",
280
+ status: classified.status === "unavailable" ? 503 : 409,
281
+ body: operatorFailed({
282
+ command: "observeLink",
283
+ outcome: classified.status === "unavailable" ? "blocked" : "conflict",
284
+ code: "OBSERVE_LINK_BLOCKED",
285
+ message: classified.message,
286
+ details: {
287
+ status: classified.status,
288
+ startCommand: OBSERVE_START_COMMAND,
289
+ requiredCapability,
290
+ healthUrl: classified.url,
291
+ localFingerprint,
292
+ remoteFingerprint: classified.remoteFingerprint,
293
+ },
294
+ }),
295
+ };
296
+ }
297
+ const link = buildObserveDeepLink(baseUrl, target);
298
+ return {
299
+ kind: "sync",
300
+ status: 200,
301
+ body: operatorSucceeded("observeLink", {
302
+ url: link,
303
+ status: "match",
304
+ requiredCapability,
305
+ }),
306
+ };
307
+ }
308
+ case "dagValidate": {
309
+ const dagPath = str(p.dagPath) ?? str(p.dagHandle);
310
+ const taskId = str(p.taskId);
311
+ if (!dagPath)
312
+ return invalid(action, "dagPath is required");
313
+ const args = ["dag", "validate", "--dag", dagPath, "--json"];
314
+ if (taskId)
315
+ args.push("--spine-task", taskId);
316
+ const body = await runReadCli(ctx, args, "dag validate");
317
+ return { kind: "sync", status: body.ok ? 200 : 400, body };
318
+ }
319
+ case "prepareDagConfirmation": {
320
+ const dagText = str(p.dagText) ?? str(p.dagJson);
321
+ const dagHandle = str(p.dagHandle) ?? "staged-dag.json";
322
+ const taskId = str(p.taskId);
323
+ if (!dagText)
324
+ return invalid(action, "dagText is required");
325
+ if (!taskId)
326
+ return invalid(action, "taskId is required");
327
+ const staged = await stageUtf8Text(ctx.appData, dagText, {
328
+ extension: ".json",
329
+ });
330
+ const controller = ctx.client.getIdentity?.()?.packageFingerprint?.value ??
331
+ "unknown-controller";
332
+ // Server-derived bindings only: never trust browser-supplied hashes as success defaults.
333
+ const draftRec = await ctx.drafts.get(taskId);
334
+ const sourceBindingSha256 = draftRec
335
+ ? draftRec.draftSha256
336
+ : hashDagBytes(`source:${taskId}:${staged.sha256}`);
337
+ const validateBody = await runReadCli(ctx, ["dag", "validate", "--dag", staged.path, "--json"], "dag validate");
338
+ if (!validateBody.ok) {
339
+ return {
340
+ kind: "error",
341
+ status: 400,
342
+ body: operatorFailed({
343
+ command: action,
344
+ outcome: "invalid",
345
+ code: "INVALID_INPUT",
346
+ message: "dag validate failed; cannot prepare confirmation for an invalid DAG",
347
+ details: {
348
+ validateError: validateBody.error ?? null,
349
+ dagSha256: staged.sha256,
350
+ },
351
+ }),
352
+ };
353
+ }
354
+ const validationResultSha256 = hashDagBytes(JSON.stringify({
355
+ ok: true,
356
+ command: "dag validate",
357
+ result: validateBody.result ?? null,
358
+ }));
359
+ // Prefer live managed contract observation over client-supplied binding fields.
360
+ const showBody = await runReadCli(ctx, ["task", "contract", "show", taskId, "--json"], "task contract show");
361
+ const showResult = (showBody.result ?? {});
362
+ const ref = showBody.ok ? showResult.state?.ref : undefined;
363
+ if (!ref?.canonicalHash || typeof ref.revision !== "number") {
364
+ return {
365
+ kind: "error",
366
+ status: 409,
367
+ body: operatorFailed({
368
+ command: action,
369
+ outcome: "blocked",
370
+ code: "BINDING_DRIFT",
371
+ message: "managed Task Contract binding unavailable; apply/adopt before prepareDagConfirmation",
372
+ details: {
373
+ effectiveStatus: showResult.state?.effectiveStatus,
374
+ showOk: showBody.ok,
375
+ },
376
+ }),
377
+ };
378
+ }
379
+ const taskContractBinding = {
380
+ taskId,
381
+ revision: ref.revision,
382
+ canonicalHash: ref.canonicalHash,
383
+ taskConfigSha256: ref.taskConfigSha256,
384
+ projectionVersion: ref.projectionVersion,
385
+ canonicalizerVersion: ref.canonicalizerVersion,
386
+ taskConfigSchemaVersion: ref.taskConfigSchemaVersion,
387
+ };
388
+ const confirmation = await ctx.confirmations.prepare({
389
+ operatorSessionId: ctx.operatorSessionId,
390
+ dagHandle,
391
+ dagBytes: dagText,
392
+ dagBytesPath: staged.path,
393
+ sourceBindingSha256,
394
+ taskContractBinding,
395
+ controllerFingerprint: controller,
396
+ validationResultSha256,
397
+ // Writer scopes must come from server-side review packet only; ignore client writeSet claims.
398
+ confirmedWriterScopes: [],
399
+ reviewPacket: {
400
+ taskId,
401
+ dagHandle,
402
+ validationOk: true,
403
+ },
404
+ });
405
+ return {
406
+ kind: "sync",
407
+ status: 200,
408
+ body: operatorSucceeded("prepareDagConfirmation", {
409
+ confirmation,
410
+ requiredChallenges: ALL_CONFIRMATION_CHALLENGES,
411
+ }),
412
+ };
413
+ }
414
+ case "confirmDagConfirmation": {
415
+ const confirmationId = str(p.confirmationId);
416
+ const challenges = p.challenges ?? [];
417
+ if (!confirmationId) {
418
+ return invalid(action, "confirmationId is required");
419
+ }
420
+ // Ignore client-supplied binding fields; re-derive current from store + staged bytes + live controller.
421
+ const conf = await ctx.confirmations.get(confirmationId);
422
+ if (!conf) {
423
+ return {
424
+ kind: "error",
425
+ status: 404,
426
+ body: operatorFailed({
427
+ command: action,
428
+ outcome: "not-found",
429
+ code: "NOT_FOUND",
430
+ message: `confirmation not found: ${confirmationId}`,
431
+ }),
432
+ };
433
+ }
434
+ let dagBytes;
435
+ try {
436
+ dagBytes = await readFile(conf.dagBytesPath, "utf8");
437
+ }
438
+ catch (error) {
439
+ return {
440
+ kind: "error",
441
+ status: 409,
442
+ body: operatorFailed({
443
+ command: action,
444
+ outcome: "conflict",
445
+ code: "CONFIRMATION_STALE",
446
+ message: `cannot re-read staged DAG bytes: ${error instanceof Error ? error.message : String(error)}`,
447
+ }),
448
+ };
449
+ }
450
+ const controller = ctx.client.getIdentity?.()?.packageFingerprint?.value ??
451
+ conf.controllerFingerprint;
452
+ const result = await ctx.confirmations.confirm({
453
+ confirmationId,
454
+ operatorSessionId: ctx.operatorSessionId,
455
+ challenges,
456
+ current: {
457
+ dagSha256: hashDagBytes(dagBytes),
458
+ sourceBindingSha256: conf.sourceBindingSha256,
459
+ controllerFingerprint: controller,
460
+ validationResultSha256: conf.validationResultSha256,
461
+ taskContractBinding: conf.taskContractBinding,
462
+ },
463
+ });
464
+ if (!result.ok) {
465
+ return {
466
+ kind: "error",
467
+ status: confirmationErrorStatus(result.code),
468
+ body: operatorFailed({
469
+ command: action,
470
+ outcome: "rejected",
471
+ code: result.code,
472
+ message: result.message,
473
+ }),
474
+ };
475
+ }
476
+ return {
477
+ kind: "sync",
478
+ status: 200,
479
+ body: operatorSucceeded(action, { confirmation: result.confirmation }),
480
+ };
481
+ }
482
+ case "newTask": {
483
+ const taskId = str(p.taskId);
484
+ const title = str(p.title) ?? taskId;
485
+ const clientRequestId = str(req.clientRequestId);
486
+ if (!taskId || !clientRequestId) {
487
+ return invalid(action, "taskId and clientRequestId are required");
488
+ }
489
+ return acceptOperation(ctx, {
490
+ action,
491
+ actionParams: p,
492
+ clientRequestId,
493
+ taskId,
494
+ cliArgs: ["new-task", taskId, title, "--json"],
495
+ });
496
+ }
497
+ case "importPrd": {
498
+ const taskId = str(p.taskId);
499
+ const content = str(p.content) ?? str(p.fileText);
500
+ const clientRequestId = str(req.clientRequestId);
501
+ if (!taskId || !content || !clientRequestId) {
502
+ return invalid(action, "taskId, content, and clientRequestId are required");
503
+ }
504
+ const staged = await stageUtf8Text(ctx.appData, content, {
505
+ extension: ".md",
506
+ });
507
+ return acceptOperation(ctx, {
508
+ action,
509
+ actionParams: { taskId, stagedSha256: staged.sha256 },
510
+ clientRequestId,
511
+ taskId,
512
+ cliArgs: ["import-prd", taskId, "--file", staged.path, "--json"],
513
+ });
514
+ }
515
+ case "contractApply": {
516
+ const taskId = str(p.taskId);
517
+ const clientRequestId = str(req.clientRequestId);
518
+ if (!taskId || !clientRequestId) {
519
+ return invalid(action, "taskId and clientRequestId are required");
520
+ }
521
+ // Require fresh complete assessment bound to current draft
522
+ const draftRec = await ctx.drafts.get(taskId);
523
+ if (!draftRec) {
524
+ return {
525
+ kind: "error",
526
+ status: 400,
527
+ body: operatorFailed({
528
+ command: action,
529
+ outcome: "invalid",
530
+ code: "INVALID_INPUT",
531
+ message: "no Console draft for task; save draft first",
532
+ }),
533
+ };
534
+ }
535
+ const assessmentId = str(p.assessmentId) ??
536
+ draftRec.draft.interviewAssessmentRef?.assessmentId;
537
+ if (!assessmentId) {
538
+ return {
539
+ kind: "error",
540
+ status: 403,
541
+ body: operatorFailed({
542
+ command: action,
543
+ outcome: "blocked",
544
+ code: "HUMAN_CONFIRMATION_REQUIRED",
545
+ message: "fresh complete Interview assessment required before contractApply",
546
+ }),
547
+ };
548
+ }
549
+ const assessment = await ctx.assessments.get(assessmentId);
550
+ if (!assessment) {
551
+ return {
552
+ kind: "error",
553
+ status: 403,
554
+ body: operatorFailed({
555
+ command: action,
556
+ outcome: "blocked",
557
+ code: "HUMAN_CONFIRMATION_REQUIRED",
558
+ message: "assessment not found",
559
+ }),
560
+ };
561
+ }
562
+ const fresh = isAssessmentFreshForDraft(assessment, draftRec.draft);
563
+ if (!fresh.fresh) {
564
+ return {
565
+ kind: "error",
566
+ status: 403,
567
+ body: operatorFailed({
568
+ command: action,
569
+ outcome: "blocked",
570
+ code: "HUMAN_CONFIRMATION_REQUIRED",
571
+ message: fresh.reason,
572
+ }),
573
+ };
574
+ }
575
+ const staged = await stageUtf8Text(ctx.appData, JSON.stringify(draftRec.draft, null, 2), { extension: ".json" });
576
+ const expectedRevision = String(p.expectedRevision ?? "0");
577
+ const expectedHash = str(p.expectedObservedHash) ?? "unknown";
578
+ // CLI validates --request-payload-sha256 against input file bytes, not draft projection hash.
579
+ const payloadSha = staged.sha256;
580
+ return acceptOperation(ctx, {
581
+ action,
582
+ actionParams: {
583
+ taskId,
584
+ assessmentId,
585
+ draftSha256: draftRec.draftSha256,
586
+ requestPayloadSha256: payloadSha,
587
+ },
588
+ clientRequestId,
589
+ taskId,
590
+ cliArgs: [
591
+ "task",
592
+ "contract",
593
+ "apply",
594
+ "--task",
595
+ taskId,
596
+ "--input",
597
+ staged.path,
598
+ "--expected-revision",
599
+ expectedRevision,
600
+ "--expected-observed-hash",
601
+ expectedHash,
602
+ "--request-id",
603
+ clientRequestId,
604
+ "--request-payload-sha256",
605
+ payloadSha,
606
+ "--json",
607
+ ],
608
+ });
609
+ }
610
+ case "dagRunTask": {
611
+ const taskId = str(p.taskId);
612
+ const clientRequestId = str(req.clientRequestId);
613
+ if (!taskId || !clientRequestId) {
614
+ return invalid(action, "taskId and clientRequestId are required");
615
+ }
616
+ const out = await stageUtf8Text(ctx.appData, "", { extension: ".json" });
617
+ return acceptOperation(ctx, {
618
+ action,
619
+ actionParams: p,
620
+ clientRequestId,
621
+ taskId,
622
+ cliArgs: [
623
+ "dag",
624
+ "run-task",
625
+ taskId,
626
+ "--profile",
627
+ str(p.profile) ?? "auto",
628
+ "-o",
629
+ out.path,
630
+ ],
631
+ });
632
+ }
633
+ case "runDag": {
634
+ // Only confirmationId — never raw dag path from browser
635
+ const confirmationId = str(p.confirmationId);
636
+ const clientRequestId = str(req.clientRequestId);
637
+ if (!confirmationId || !clientRequestId) {
638
+ return invalid(action, "runDag requires confirmationId and clientRequestId only");
639
+ }
640
+ const conf = await ctx.confirmations.get(confirmationId);
641
+ if (!conf) {
642
+ return {
643
+ kind: "error",
644
+ status: 404,
645
+ body: operatorFailed({
646
+ command: action,
647
+ outcome: "not-found",
648
+ code: "NOT_FOUND",
649
+ message: `confirmation not found: ${confirmationId}`,
650
+ }),
651
+ };
652
+ }
653
+ // Re-read staged DAG bytes and hash
654
+ let dagBytes;
655
+ try {
656
+ dagBytes = await readFile(conf.dagBytesPath, "utf8");
657
+ }
658
+ catch (error) {
659
+ return {
660
+ kind: "error",
661
+ status: 409,
662
+ body: operatorFailed({
663
+ command: action,
664
+ outcome: "conflict",
665
+ code: "CONFIRMATION_STALE",
666
+ message: `cannot re-read staged DAG bytes: ${error instanceof Error ? error.message : String(error)}`,
667
+ }),
668
+ };
669
+ }
670
+ const dagSha256 = hashDagBytes(dagBytes);
671
+ const controller = ctx.client.getIdentity?.()?.packageFingerprint?.value ??
672
+ conf.controllerFingerprint;
673
+ const consumed = await ctx.confirmations.consume({
674
+ confirmationId,
675
+ operatorSessionId: ctx.operatorSessionId,
676
+ current: {
677
+ dagSha256,
678
+ sourceBindingSha256: conf.sourceBindingSha256,
679
+ taskContractBinding: conf.taskContractBinding,
680
+ controllerFingerprint: controller,
681
+ validationResultSha256: conf.validationResultSha256,
682
+ },
683
+ });
684
+ if (!consumed.ok) {
685
+ return {
686
+ kind: "error",
687
+ status: confirmationErrorStatus(consumed.code),
688
+ body: operatorFailed({
689
+ command: action,
690
+ outcome: "rejected",
691
+ code: consumed.code,
692
+ message: consumed.message,
693
+ }),
694
+ };
695
+ }
696
+ return acceptOperation(ctx, {
697
+ action,
698
+ actionParams: { confirmationId },
699
+ clientRequestId,
700
+ taskId: conf.taskContractBinding.taskId,
701
+ cliArgs: [
702
+ "run-dag",
703
+ "--dag",
704
+ conf.dagBytesPath,
705
+ "--cwd",
706
+ ctx.repoRoot,
707
+ "--json",
708
+ ],
709
+ });
710
+ }
711
+ default:
712
+ return {
713
+ kind: "error",
714
+ status: 400,
715
+ body: operatorFailed({
716
+ command: action,
717
+ outcome: "invalid",
718
+ code: "INVALID_INPUT",
719
+ message: `unknown or unsupported action: ${action}`,
720
+ }),
721
+ };
722
+ }
723
+ }
724
+ function invalid(action, message) {
725
+ return {
726
+ kind: "error",
727
+ status: 400,
728
+ body: operatorFailed({
729
+ command: action,
730
+ outcome: "invalid",
731
+ code: "INVALID_INPUT",
732
+ message,
733
+ }),
734
+ };
735
+ }
736
+ function confirmationErrorStatus(code) {
737
+ if (code === "NOT_FOUND")
738
+ return 404;
739
+ if (code === "HUMAN_CONFIRMATION_REQUIRED")
740
+ return 403;
741
+ return 409;
742
+ }
743
+ async function dispatchContractValidateOrDiff(ctx, action, p) {
744
+ const taskId = str(p.taskId);
745
+ const draftText = str(p.draftJson) ?? str(p.inputText);
746
+ if (!taskId)
747
+ return invalid(action, "taskId is required");
748
+ let inputPath;
749
+ if (!draftText) {
750
+ const stored = await ctx.drafts.get(taskId);
751
+ if (!stored) {
752
+ return invalid(action, "draftJson or stored draft required");
753
+ }
754
+ const staged = await stageUtf8Text(ctx.appData, JSON.stringify(stored.draft, null, 2), { extension: ".json" });
755
+ inputPath = staged.path;
756
+ }
757
+ else {
758
+ const staged = await stageUtf8Text(ctx.appData, draftText, {
759
+ extension: ".json",
760
+ });
761
+ inputPath = staged.path;
762
+ }
763
+ const sub = action === "contractValidate" ? "validate" : "diff";
764
+ const body = await runReadCli(ctx, ["task", "contract", sub, "--task", taskId, "--input", inputPath, "--json"], `task contract ${sub}`);
765
+ return { kind: "sync", status: body.ok ? 200 : 400, body };
766
+ }
767
+ export function isMutationAction(action) {
768
+ return MUTATION_OR_LONG.has(action);
769
+ }