@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
@@ -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 {};
@@ -0,0 +1,166 @@
1
+ import { CANONICALIZER_VERSION, PROJECTION_VERSION, TASK_CONFIG_SCHEMA_VERSION, TASK_CONTRACT_REF_SCHEMA_VERSION, } from "./constants.js";
2
+ import { ContractMutationError } from "./apply.js";
3
+ import { computeCanonicalHash, computeSourceHashes, computeTaskConfigSha256, } from "./hash.js";
4
+ import { acquireTaskContractLock, assertLockHeld } from "./lock.js";
5
+ import { loadObservedTaskBytes, logicalRevisionForState, observeTaskContractState, readStoredContractRef, } from "./observe.js";
6
+ import { serializeTaskConfigForDisk } from "./project.js";
7
+ import { appendRequestLedgerEntry, findRequestLedgerEntry, } from "./request-ledger.js";
8
+ import { finalizeTransaction, runJournaledContractCommit, } from "./transaction.js";
9
+ import { getTaskPaths, parseTaskConfigForRuntime } from "../runtime.js";
10
+ /**
11
+ * Adopt current on-disk 需求.md / 执行约束.md / task.json as next managed revision.
12
+ * Does not rewrite reference bytes. Requires deterministic readable facts.
13
+ */
14
+ export async function adoptTaskContract(input) {
15
+ const existing = await findRequestLedgerEntry({
16
+ repoRoot: input.repoRoot,
17
+ taskId: input.taskId,
18
+ requestId: input.requestId,
19
+ });
20
+ if (existing) {
21
+ if (existing.requestPayloadSha256 !== input.requestPayloadSha256) {
22
+ throw new ContractMutationError("REQUEST_ID_REUSE_CONFLICT", `requestId ${input.requestId} was used with a different payload hash`);
23
+ }
24
+ const ref = await readStoredContractRef(getTaskPaths(input.repoRoot, input.taskId).contractPath);
25
+ if (!ref) {
26
+ throw new ContractMutationError("OPERATION_NEEDS_RECONCILE", "request ledger hit but contract.json missing");
27
+ }
28
+ return {
29
+ ref,
30
+ changedFiles: [],
31
+ idempotentReplay: true,
32
+ txId: existing.txId,
33
+ };
34
+ }
35
+ const lock = await acquireTaskContractLock({
36
+ repoRoot: input.repoRoot,
37
+ taskId: input.taskId,
38
+ requestId: input.requestId,
39
+ operation: "contract-adopt",
40
+ });
41
+ try {
42
+ const existingUnderLock = await findRequestLedgerEntry({
43
+ repoRoot: input.repoRoot,
44
+ taskId: input.taskId,
45
+ requestId: input.requestId,
46
+ });
47
+ if (existingUnderLock) {
48
+ if (existingUnderLock.requestPayloadSha256 !== input.requestPayloadSha256) {
49
+ throw new ContractMutationError("REQUEST_ID_REUSE_CONFLICT", `requestId ${input.requestId} was used with a different payload hash`);
50
+ }
51
+ const ref = await readStoredContractRef(getTaskPaths(input.repoRoot, input.taskId).contractPath);
52
+ if (!ref) {
53
+ throw new ContractMutationError("OPERATION_NEEDS_RECONCILE", "request ledger hit but contract.json missing");
54
+ }
55
+ return {
56
+ ref,
57
+ changedFiles: [],
58
+ idempotentReplay: true,
59
+ txId: existingUnderLock.txId,
60
+ };
61
+ }
62
+ const state = await observeTaskContractState(input.repoRoot, input.taskId);
63
+ if (state.effectiveStatus === "transaction-incomplete") {
64
+ throw new ContractMutationError("TRANSACTION_INCOMPLETE", "unfinished transaction; run task contract recover first");
65
+ }
66
+ const currentRevision = logicalRevisionForState(state);
67
+ if (input.expectedRevision !== currentRevision) {
68
+ throw new ContractMutationError("REVISION_CONFLICT", `expectedRevision ${input.expectedRevision} does not match current ${currentRevision}`, { currentRevision, expectedRevision: input.expectedRevision });
69
+ }
70
+ const bytes = await loadObservedTaskBytes(input.repoRoot, input.taskId);
71
+ if (bytes.requirementBytes === null ||
72
+ bytes.constraintsBytes === null ||
73
+ bytes.taskConfigRaw === null ||
74
+ bytes.taskConfigSha256 === null) {
75
+ throw new ContractMutationError("INVALID_INPUT", "adopt requires existing 需求.md, 执行约束.md, and task.json");
76
+ }
77
+ const observedHash = state.observedCanonicalHash;
78
+ if (!observedHash) {
79
+ throw new ContractMutationError("OBSERVED_HASH_CONFLICT", "unable to observe current canonical hash for adopt");
80
+ }
81
+ if (input.expectedObservedHash !== observedHash) {
82
+ throw new ContractMutationError("OBSERVED_HASH_CONFLICT", "expectedObservedHash does not match current observed hash", {
83
+ expectedObservedHash: input.expectedObservedHash,
84
+ observedCanonicalHash: observedHash,
85
+ });
86
+ }
87
+ const fullConfig = parseTaskConfigForRuntime(bytes.taskConfigRaw);
88
+ const taskConfigContent = serializeTaskConfigForDisk(fullConfig);
89
+ const sourceHashes = computeSourceHashes({
90
+ requirementBytes: bytes.requirementBytes,
91
+ constraintsBytes: bytes.constraintsBytes,
92
+ referenceManifestBytes: bytes.referenceManifestBytes,
93
+ });
94
+ const taskConfigSha256 = computeTaskConfigSha256(fullConfig);
95
+ const canonicalHash = computeCanonicalHash({
96
+ sourceHashes,
97
+ taskConfigSha256,
98
+ });
99
+ // Sanity: observed should equal recomputed
100
+ if (canonicalHash !== observedHash) {
101
+ throw new ContractMutationError("OBSERVED_HASH_CONFLICT", "recomputed canonical hash diverged during adopt");
102
+ }
103
+ const nextRevision = currentRevision + 1;
104
+ const nextRef = {
105
+ schemaVersion: TASK_CONTRACT_REF_SCHEMA_VERSION,
106
+ taskId: input.taskId,
107
+ revision: nextRevision,
108
+ projectionVersion: PROJECTION_VERSION,
109
+ canonicalizerVersion: CANONICALIZER_VERSION,
110
+ taskConfigSchemaVersion: TASK_CONFIG_SCHEMA_VERSION,
111
+ sourceHashes,
112
+ taskConfigSha256,
113
+ canonicalHash,
114
+ updatedAt: new Date().toISOString(),
115
+ lastRequestId: input.requestId,
116
+ };
117
+ // Adopt rewrites contract.json and re-writes same source/config bytes
118
+ // (stable content) so journal protocol is uniform.
119
+ await assertLockHeld(lock);
120
+ const { txId, ref } = await runJournaledContractCommit({
121
+ repoRoot: input.repoRoot,
122
+ taskId: input.taskId,
123
+ operation: "adopt",
124
+ requestId: input.requestId,
125
+ requestPayloadSha256: input.requestPayloadSha256,
126
+ expectedRevision: input.expectedRevision,
127
+ expectedObservedCanonicalHash: input.expectedObservedHash,
128
+ nextRevision,
129
+ fencingToken: lock.record.fencingToken,
130
+ targets: {
131
+ requirementContent: bytes.requirementBytes,
132
+ constraintsContent: bytes.constraintsBytes,
133
+ taskConfigContent,
134
+ contractRef: nextRef,
135
+ },
136
+ });
137
+ await appendRequestLedgerEntry({
138
+ repoRoot: input.repoRoot,
139
+ taskId: input.taskId,
140
+ entry: {
141
+ schemaVersion: 1,
142
+ requestId: input.requestId,
143
+ requestPayloadSha256: input.requestPayloadSha256,
144
+ txId,
145
+ revision: ref.revision,
146
+ canonicalHash: ref.canonicalHash,
147
+ recordedAt: new Date().toISOString(),
148
+ resultSummary: { ok: true, operation: "adopt" },
149
+ },
150
+ });
151
+ await finalizeTransaction({
152
+ repoRoot: input.repoRoot,
153
+ taskId: input.taskId,
154
+ txId,
155
+ });
156
+ return {
157
+ ref,
158
+ changedFiles: ["contract.json"],
159
+ idempotentReplay: false,
160
+ txId,
161
+ };
162
+ }
163
+ finally {
164
+ await lock.release();
165
+ }
166
+ }