@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,296 @@
1
+ import { access, readdir, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { CONSTRAINTS_FILE_NAME, CONTRACT_FILE_NAME, REQUIREMENT_FILE_NAME, TRANSACTIONS_DIR_NAME, } from "./constants.js";
4
+ import { computeCanonicalHash, computeSourceHashes, computeTaskConfigSha256, } from "./hash.js";
5
+ import { parseTaskContractRef } from "./schema.js";
6
+ import { getTaskPaths } from "../runtime.js";
7
+ import { parseTaskConfigForRuntime } from "../runtime.js";
8
+ import { getSourceManifestPath, readSourceManifest, } from "../source-references.js";
9
+ async function readTextIfExists(filePath) {
10
+ try {
11
+ return await readFile(filePath, "utf-8");
12
+ }
13
+ catch (error) {
14
+ if (error &&
15
+ typeof error === "object" &&
16
+ "code" in error &&
17
+ error.code === "ENOENT") {
18
+ return null;
19
+ }
20
+ throw error;
21
+ }
22
+ }
23
+ async function pathExists(filePath) {
24
+ try {
25
+ await access(filePath);
26
+ return true;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ export async function loadObservedTaskBytes(repoRoot, taskId) {
33
+ const paths = getTaskPaths(repoRoot, taskId);
34
+ const requirementPath = path.join(paths.sourceDir, REQUIREMENT_FILE_NAME);
35
+ const constraintsPath = path.join(paths.sourceDir, CONSTRAINTS_FILE_NAME);
36
+ const [requirementBytes, constraintsBytes, taskConfigText, manifestText] = await Promise.all([
37
+ readTextIfExists(requirementPath),
38
+ readTextIfExists(constraintsPath),
39
+ readTextIfExists(paths.taskConfigPath),
40
+ readTextIfExists(getSourceManifestPath(paths.sourceDir)),
41
+ ]);
42
+ let taskConfigRaw = null;
43
+ let taskConfigSha256 = null;
44
+ if (taskConfigText !== null) {
45
+ taskConfigRaw = JSON.parse(taskConfigText);
46
+ const parsed = parseTaskConfigForRuntime(taskConfigRaw);
47
+ taskConfigSha256 = computeTaskConfigSha256(parsed);
48
+ }
49
+ return {
50
+ requirementBytes,
51
+ constraintsBytes,
52
+ taskConfigRaw,
53
+ taskConfigSha256,
54
+ referenceManifestBytes: manifestText,
55
+ requirementPath,
56
+ constraintsPath,
57
+ taskConfigPath: paths.taskConfigPath,
58
+ contractPath: paths.contractPath,
59
+ transactionsDir: paths.transactionsDir,
60
+ };
61
+ }
62
+ export async function readStoredContractRef(contractPath) {
63
+ const text = await readTextIfExists(contractPath);
64
+ if (text === null)
65
+ return null;
66
+ try {
67
+ return parseTaskContractRef(JSON.parse(text));
68
+ }
69
+ catch (error) {
70
+ throw new Error(`invalid contract.json at ${contractPath}: ${error instanceof Error ? error.message : String(error)}`);
71
+ }
72
+ }
73
+ export async function listUnfinishedTransactionDirs(transactionsDir) {
74
+ if (!(await pathExists(transactionsDir)))
75
+ return [];
76
+ const entries = await readdir(transactionsDir, { withFileTypes: true });
77
+ const unfinished = [];
78
+ for (const entry of entries) {
79
+ if (!entry.isDirectory())
80
+ continue;
81
+ const statePath = path.join(transactionsDir, entry.name, "state.json");
82
+ const commitPath = path.join(transactionsDir, entry.name, "commit.json");
83
+ const stateText = await readTextIfExists(statePath);
84
+ if (!stateText) {
85
+ unfinished.push(entry.name);
86
+ continue;
87
+ }
88
+ try {
89
+ const state = JSON.parse(stateText);
90
+ if (state.phase === "finalized" ||
91
+ (state.phase === "committed" && (await pathExists(commitPath)))) {
92
+ // committed without finalized still counts as unfinished for doctor,
93
+ // but recover only needs finalize — still list it.
94
+ if (state.phase === "finalized")
95
+ continue;
96
+ }
97
+ if (state.phase !== "finalized") {
98
+ unfinished.push(entry.name);
99
+ }
100
+ }
101
+ catch {
102
+ unfinished.push(entry.name);
103
+ }
104
+ }
105
+ return unfinished;
106
+ }
107
+ /**
108
+ * Derive TaskContractStateV1 by re-reading actual files (design §5.3.2/5.3.8).
109
+ * effectiveStatus is always derived at read time — never trusted from disk alone.
110
+ */
111
+ export async function observeTaskContractState(repoRoot, taskId) {
112
+ const paths = getTaskPaths(repoRoot, taskId);
113
+ const errors = [];
114
+ const taskExists = await pathExists(paths.taskConfigPath);
115
+ if (!taskExists) {
116
+ return {
117
+ effectiveStatus: "legacy-unversioned",
118
+ observationErrors: [
119
+ {
120
+ code: "NOT_FOUND",
121
+ path: paths.taskConfigPath,
122
+ message: `task not found: ${taskId}`,
123
+ },
124
+ ],
125
+ };
126
+ }
127
+ const unfinished = await listUnfinishedTransactionDirs(paths.transactionsDir);
128
+ if (unfinished.length > 0) {
129
+ const observed = await computeObservedCanonicalHashSafe(repoRoot, taskId);
130
+ let ref;
131
+ try {
132
+ ref = (await readStoredContractRef(paths.contractPath)) ?? undefined;
133
+ }
134
+ catch (error) {
135
+ errors.push({
136
+ code: "INVALID_CONTRACT_JSON",
137
+ path: paths.contractPath,
138
+ message: error instanceof Error ? error.message : String(error),
139
+ });
140
+ }
141
+ return {
142
+ ref,
143
+ effectiveStatus: "transaction-incomplete",
144
+ observedCanonicalHash: observed.hash ?? undefined,
145
+ observationErrors: [
146
+ ...errors,
147
+ ...unfinished.map((txId) => ({
148
+ code: "TRANSACTION_INCOMPLETE",
149
+ path: path.join(TRANSACTIONS_DIR_NAME, txId),
150
+ message: `unfinished transaction: ${txId}`,
151
+ })),
152
+ ...(observed.errors ?? []),
153
+ ],
154
+ };
155
+ }
156
+ let ref = null;
157
+ try {
158
+ ref = await readStoredContractRef(paths.contractPath);
159
+ }
160
+ catch (error) {
161
+ errors.push({
162
+ code: "INVALID_CONTRACT_JSON",
163
+ path: paths.contractPath,
164
+ message: error instanceof Error ? error.message : String(error),
165
+ });
166
+ }
167
+ const observed = await computeObservedCanonicalHashSafe(repoRoot, taskId);
168
+ errors.push(...(observed.errors ?? []));
169
+ if (!ref) {
170
+ return {
171
+ effectiveStatus: "legacy-unversioned",
172
+ observedCanonicalHash: observed.hash ?? undefined,
173
+ observationErrors: errors,
174
+ };
175
+ }
176
+ if (!observed.hash) {
177
+ return {
178
+ ref,
179
+ effectiveStatus: "externally-modified",
180
+ observationErrors: [
181
+ ...errors,
182
+ {
183
+ code: "OBSERVE_FAILED",
184
+ message: "unable to compute observed canonical hash",
185
+ },
186
+ ],
187
+ };
188
+ }
189
+ if (ref.canonicalHash !== observed.hash) {
190
+ return {
191
+ ref,
192
+ effectiveStatus: "externally-modified",
193
+ observedCanonicalHash: observed.hash,
194
+ observationErrors: [
195
+ ...errors,
196
+ {
197
+ code: "EXTERNALLY_MODIFIED",
198
+ message: "on-disk source/task config hash does not match contract.json canonicalHash",
199
+ },
200
+ ],
201
+ };
202
+ }
203
+ // Optional: verify source-manifest still matches recorded hash when present.
204
+ if (ref.sourceHashes.referenceManifestSha256) {
205
+ const manifest = await readSourceManifest(paths.sourceDir);
206
+ if (!manifest) {
207
+ return {
208
+ ref,
209
+ effectiveStatus: "externally-modified",
210
+ observedCanonicalHash: observed.hash,
211
+ observationErrors: [
212
+ ...errors,
213
+ {
214
+ code: "MANIFEST_MISSING",
215
+ path: getSourceManifestPath(paths.sourceDir),
216
+ message: "contract records referenceManifestSha256 but source-manifest.json is missing",
217
+ },
218
+ ],
219
+ };
220
+ }
221
+ }
222
+ return {
223
+ ref,
224
+ effectiveStatus: "managed",
225
+ observedCanonicalHash: observed.hash,
226
+ observationErrors: errors,
227
+ };
228
+ }
229
+ async function computeObservedCanonicalHashSafe(repoRoot, taskId) {
230
+ const errors = [];
231
+ try {
232
+ const bytes = await loadObservedTaskBytes(repoRoot, taskId);
233
+ if (bytes.requirementBytes === null ||
234
+ bytes.constraintsBytes === null ||
235
+ bytes.taskConfigSha256 === null) {
236
+ if (bytes.requirementBytes === null) {
237
+ errors.push({
238
+ code: "MISSING_REQUIREMENT",
239
+ path: bytes.requirementPath,
240
+ message: `missing ${REQUIREMENT_FILE_NAME}`,
241
+ });
242
+ }
243
+ if (bytes.constraintsBytes === null) {
244
+ errors.push({
245
+ code: "MISSING_CONSTRAINTS",
246
+ path: bytes.constraintsPath,
247
+ message: `missing ${CONSTRAINTS_FILE_NAME}`,
248
+ });
249
+ }
250
+ if (bytes.taskConfigSha256 === null) {
251
+ errors.push({
252
+ code: "MISSING_TASK_CONFIG",
253
+ path: bytes.taskConfigPath,
254
+ message: "missing or unreadable task.json",
255
+ });
256
+ }
257
+ return { hash: null, errors };
258
+ }
259
+ const sourceHashes = computeSourceHashes({
260
+ requirementBytes: bytes.requirementBytes,
261
+ constraintsBytes: bytes.constraintsBytes,
262
+ referenceManifestBytes: bytes.referenceManifestBytes,
263
+ });
264
+ const hash = computeCanonicalHash({
265
+ sourceHashes,
266
+ taskConfigSha256: bytes.taskConfigSha256,
267
+ });
268
+ return { hash, errors };
269
+ }
270
+ catch (error) {
271
+ errors.push({
272
+ code: "OBSERVE_FAILED",
273
+ message: error instanceof Error ? error.message : String(error),
274
+ });
275
+ return { hash: null, errors };
276
+ }
277
+ }
278
+ export async function computeObservedCanonicalHash(repoRoot, taskId) {
279
+ const result = await computeObservedCanonicalHashSafe(repoRoot, taskId);
280
+ if (!result.hash) {
281
+ throw new Error(`unable to compute observed canonical hash for ${taskId}: ${result.errors
282
+ .map((e) => e.message)
283
+ .join("; ")}`);
284
+ }
285
+ return result.hash;
286
+ }
287
+ export function logicalRevisionForState(state) {
288
+ if (!state.ref)
289
+ return 0;
290
+ return state.ref.revision;
291
+ }
292
+ /** Object-form entry point used by DAG generate/revalidate. */
293
+ export async function observeTaskContract(input) {
294
+ return observeTaskContractState(input.repoRoot, input.taskId);
295
+ }
296
+ export { CONTRACT_FILE_NAME };
@@ -0,0 +1,19 @@
1
+ import path from "node:path";
2
+ import { getTaskPaths } from "../runtime.js";
3
+ import { CONTRACT_JSON_BASENAME, REQUEST_LEDGER_BASENAME, TRANSACTIONS_DIRNAME, } from "./constants.js";
4
+ export function getTaskContractPaths(repoRoot, taskId) {
5
+ const base = getTaskPaths(repoRoot, taskId);
6
+ return {
7
+ taskDir: base.taskDir,
8
+ taskConfigPath: base.taskConfigPath,
9
+ sourceDir: base.sourceDir,
10
+ requirementPath: path.join(base.sourceDir, "需求.md"),
11
+ constraintsPath: path.join(base.sourceDir, "执行约束.md"),
12
+ sourceManifestPath: path.join(base.sourceDir, "source-manifest.json"),
13
+ referencesDir: path.join(base.sourceDir, "references"),
14
+ contractPath: path.join(base.taskDir, CONTRACT_JSON_BASENAME),
15
+ transactionsDir: path.join(base.taskDir, TRANSACTIONS_DIRNAME),
16
+ requestLedgerPath: path.join(base.taskDir, REQUEST_LEDGER_BASENAME),
17
+ lockPath: path.join(base.taskDir, ".contract.lock"),
18
+ };
19
+ }
@@ -0,0 +1,170 @@
1
+ import { canonicalizePathArray, canonicalizeTextProjection, } from "./canonicalize.js";
2
+ /**
3
+ * Project Draft → source/需求.md (deterministic template, projectionVersion=1).
4
+ */
5
+ export function projectRequirementMarkdown(draft) {
6
+ const lines = [
7
+ `# 需求:${draft.title}`,
8
+ "",
9
+ "## Objective",
10
+ "",
11
+ draft.requirement.objective,
12
+ "",
13
+ "## Scope",
14
+ "",
15
+ ];
16
+ if (draft.requirement.scope.length === 0) {
17
+ lines.push("- (none)");
18
+ }
19
+ else {
20
+ for (const item of draft.requirement.scope) {
21
+ lines.push(`- ${item}`);
22
+ }
23
+ }
24
+ lines.push("", "## Non-goals", "");
25
+ if (draft.requirement.nonGoals.length === 0) {
26
+ lines.push("- (none)");
27
+ }
28
+ else {
29
+ for (const item of draft.requirement.nonGoals) {
30
+ lines.push(`- ${item}`);
31
+ }
32
+ }
33
+ lines.push("", "## Acceptance Criteria", "");
34
+ for (const ac of draft.requirement.acceptanceCriteria) {
35
+ lines.push(`- ${ac.id}: ${ac.text}`);
36
+ }
37
+ if (draft.assumptions && draft.assumptions.length > 0) {
38
+ lines.push("", "## Assumptions", "");
39
+ for (const item of draft.assumptions) {
40
+ lines.push(`- ${item}`);
41
+ }
42
+ }
43
+ if (draft.openQuestions && draft.openQuestions.length > 0) {
44
+ lines.push("", "## Open Questions", "");
45
+ for (const item of draft.openQuestions) {
46
+ lines.push(`- ${item}`);
47
+ }
48
+ }
49
+ if (draft.sourceResolutions && draft.sourceResolutions.length > 0) {
50
+ lines.push("", "## Source Resolutions", "");
51
+ for (const res of draft.sourceResolutions) {
52
+ lines.push(`- ${res.sourceRef}: ${res.conflict} → ${res.decision} (${res.reason}; decidedBy=${res.decidedBy})`);
53
+ }
54
+ }
55
+ if (draft.references && draft.references.length > 0) {
56
+ lines.push("", "## References", "");
57
+ for (const ref of draft.references) {
58
+ lines.push(`- [${ref.role}] ${ref.ref}`);
59
+ }
60
+ }
61
+ lines.push("");
62
+ return canonicalizeTextProjection(lines.join("\n"));
63
+ }
64
+ /**
65
+ * Project Draft → source/执行约束.md.
66
+ */
67
+ export function projectConstraintsMarkdown(draft) {
68
+ const lines = [
69
+ `# 执行约束:${draft.title}`,
70
+ "",
71
+ "## Invariants",
72
+ "",
73
+ ];
74
+ const invariants = draft.constraints.invariants.length > 0
75
+ ? draft.constraints.invariants
76
+ : draft.hardConstraints ?? [];
77
+ if (invariants.length === 0) {
78
+ lines.push("- (none)");
79
+ }
80
+ else {
81
+ for (const item of invariants) {
82
+ lines.push(`- ${item}`);
83
+ }
84
+ }
85
+ if (draft.constraints.protectedUserChanges &&
86
+ draft.constraints.protectedUserChanges.length > 0) {
87
+ lines.push("", "## Protected user changes", "");
88
+ for (const item of draft.constraints.protectedUserChanges) {
89
+ lines.push(`- ${item}`);
90
+ }
91
+ }
92
+ lines.push("", "## Allowed paths", "");
93
+ if (draft.constraints.allowedPaths.length === 0) {
94
+ lines.push("- (none)");
95
+ }
96
+ else {
97
+ for (const item of draft.constraints.allowedPaths) {
98
+ lines.push(`- ${item}`);
99
+ }
100
+ }
101
+ lines.push("", "## Forbidden paths", "");
102
+ if (draft.constraints.forbiddenPaths.length === 0) {
103
+ lines.push("- (none)");
104
+ }
105
+ else {
106
+ for (const item of draft.constraints.forbiddenPaths) {
107
+ lines.push(`- ${item}`);
108
+ }
109
+ }
110
+ lines.push("", "## Expected verification", "");
111
+ if (draft.verification.commands.length === 0) {
112
+ lines.push("- (none)");
113
+ }
114
+ else {
115
+ for (const cmd of draft.verification.commands) {
116
+ const timeout = cmd.timeoutMs !== undefined ? ` (timeoutMs=${cmd.timeoutMs})` : "";
117
+ lines.push(`- ${cmd.label}: \`${cmd.command}\`${timeout}`);
118
+ }
119
+ }
120
+ if (draft.constraints.failureConditions &&
121
+ draft.constraints.failureConditions.length > 0) {
122
+ lines.push("", "## Failure conditions", "");
123
+ for (const item of draft.constraints.failureConditions) {
124
+ lines.push(`- ${item}`);
125
+ }
126
+ }
127
+ if (draft.constraints.knownRisks && draft.constraints.knownRisks.length > 0) {
128
+ lines.push("", "## Known risks", "");
129
+ for (const item of draft.constraints.knownRisks) {
130
+ lines.push(`- ${item}`);
131
+ }
132
+ }
133
+ lines.push("");
134
+ return canonicalizeTextProjection(lines.join("\n"));
135
+ }
136
+ /**
137
+ * Merge managed fields from Draft into existing TaskConfig.
138
+ * Non-managed fields are preserved byte-for-byte at the object level
139
+ * (caller still re-serializes via full config schema parse).
140
+ */
141
+ export function mergeManagedTaskConfigFields(existing, draft) {
142
+ const hardConstraints = draft.hardConstraints ??
143
+ (draft.constraints.invariants.length > 0
144
+ ? draft.constraints.invariants
145
+ : existing.hardConstraints);
146
+ const next = {
147
+ ...existing,
148
+ title: draft.title,
149
+ taskKind: draft.taskKind,
150
+ allowedPaths: canonicalizePathArray(draft.constraints.allowedPaths),
151
+ forbiddenPaths: canonicalizePathArray(draft.constraints.forbiddenPaths),
152
+ hardConstraints,
153
+ verifyCommands: draft.verification.commands.map((cmd) => ({
154
+ label: cmd.label,
155
+ command: cmd.command,
156
+ ...(cmd.timeoutMs !== undefined ? { timeoutMs: cmd.timeoutMs } : {}),
157
+ })),
158
+ };
159
+ if (draft.featureId !== undefined) {
160
+ next.featureId = draft.featureId;
161
+ }
162
+ return next;
163
+ }
164
+ /**
165
+ * Serialize TaskConfig for on-disk managed write (pretty JSON + trailing newline).
166
+ * Hashing uses full canonical JSON separately (canonicalizeJsonValue).
167
+ */
168
+ export function serializeTaskConfigForDisk(config) {
169
+ return `${JSON.stringify(config, null, 2)}\n`;
170
+ }