@tea-agent/loop-agent 0.26.0 → 0.26.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 (170) hide show
  1. package/CHANGELOG.md +1032 -1020
  2. package/README.md +8 -3
  3. package/bin/loop-agent.js +21 -21
  4. package/dist/cli/command-definitions.js +25 -10
  5. package/dist/cli/help.js +4 -3
  6. package/dist/cli/program.js +43 -17
  7. package/dist/commands/cursor-prompt.js +6 -6
  8. package/dist/commands/import-prd.js +7 -2
  9. package/dist/commands/init.js +7 -5
  10. package/dist/commands/loop-benchmark.js +11 -11
  11. package/dist/commands/pi-reuse-benchmark.js +16 -16
  12. package/dist/commands/task-source-prepare.js +468 -0
  13. package/dist/executors/dag-pi-executor.js +40 -5
  14. package/dist/executors/shell-write-guard.js +161 -25
  15. package/dist/sidecars/cursor-prompt/executor.js +1 -1
  16. package/dist/task/source-prepare/build-draft.js +215 -0
  17. package/dist/task/source-prepare/completeness.js +195 -0
  18. package/dist/task/source-prepare/index.js +7 -0
  19. package/dist/task/source-prepare/parse-intent.js +373 -0
  20. package/dist/task/source-prepare/path-policy.js +197 -0
  21. package/dist/task/source-prepare/prepare.js +506 -0
  22. package/dist/task/source-prepare/reference-integrity.js +274 -0
  23. package/dist/task/source-prepare/types.js +7 -0
  24. package/dist/worker/observe/static/copy.js +67 -67
  25. package/dist/worker/observe/static/dag-layout.d.ts +31 -31
  26. package/dist/worker/observe/static/dag-layout.js +83 -83
  27. package/dist/worker/observe/static/dom.js +220 -220
  28. package/dist/worker/observe/static/relations.js +133 -133
  29. package/dist/worker/observe/static/router.js +93 -93
  30. package/dist/worker/observe/static/run-processing.js +148 -148
  31. package/dist/worker/observe/static/views/batch.js +227 -227
  32. package/dist/worker/observe/static/views/dag-graph.js +172 -172
  33. package/dist/worker/observe/static/views/failures.js +143 -143
  34. package/dist/worker/observe/static/views/feature.js +492 -492
  35. package/dist/worker/observe/static/views/run.js +453 -453
  36. package/dist/worker/observe/static/views/shell.js +7 -7
  37. package/dist/worker/observe/static/views/timeline.js +163 -163
  38. package/dist/workflows/dag/canvas-observer.js +275 -275
  39. package/docs/skills/README.md +7 -7
  40. package/docs/templates/adr.md +60 -60
  41. package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
  42. package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
  43. package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
  44. package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
  45. package/docs/templates/agent-dag-report.schema.json +473 -473
  46. package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
  47. package/docs/templates/backend-test-result.schema.json +99 -99
  48. package/docs/templates/feature-spec.md +53 -53
  49. package/docs/templates/frontend-design-contract.md +42 -42
  50. package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -17
  51. package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -16
  52. package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -16
  53. package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -16
  54. package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -16
  55. package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -16
  56. package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -21
  57. package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -29
  58. package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -28
  59. package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -28
  60. package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -29
  61. package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -27
  62. package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -28
  63. package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -28
  64. package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -27
  65. package/docs/templates/frontend-eval/metrics.md +138 -138
  66. package/docs/templates/frontend-eval/smoke-targets.md +53 -53
  67. package/docs/templates/frontend-task-constraints.md +35 -35
  68. package/docs/templates/frontend-task-requirement.md +70 -70
  69. package/docs/templates/init-evolution-review.md +35 -35
  70. package/docs/templates/init-managed-agents.md +10 -5
  71. package/docs/templates/interactive-ui-round2-experiment.md +66 -66
  72. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
  73. package/docs/templates/knowledge-sync-dag.json +178 -178
  74. package/docs/templates/knowledge-sync-draft.schema.json +71 -71
  75. package/docs/templates/product-line/closeout.yaml +9 -9
  76. package/docs/templates/product-line/design.md +13 -13
  77. package/docs/templates/product-line/links.md +10 -10
  78. package/docs/templates/product-line/requirement.md +17 -17
  79. package/docs/templates/product-line/test-plan.md +7 -7
  80. package/docs/templates/production-readiness-checklist.md +57 -57
  81. package/docs/templates/project-start-checklist.md +9 -9
  82. package/docs/templates/qa-report.md +48 -48
  83. package/docs/templates/sprint-contract.md +29 -29
  84. package/docs/templates/worker-dogfood-evidence.md +80 -80
  85. package/docs/templates/worker-dogfood-setup.md +68 -68
  86. package/package.json +1 -1
  87. package/scripts/kb-bootstrap-init-skeleton.sh +0 -0
  88. package/scripts/kb-graph-incremental-prepare.mjs +386 -386
  89. package/scripts/kb-graph-materialize.mjs +105 -105
  90. package/scripts/kb-graph-promote.mjs +164 -164
  91. package/scripts/kb-query.mjs +554 -554
  92. package/skills/ai-engineering-context/SKILL.md +48 -48
  93. package/skills/analyze-product-dependencies/SKILL.md +67 -67
  94. package/skills/analyze-product-dependencies/agents/openai.yaml +4 -4
  95. package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -30
  96. package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -28
  97. package/skills/analyze-product-dependencies/references/example.md +76 -76
  98. package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -35
  99. package/skills/analyze-product-dependencies/references/input-contract.md +11 -11
  100. package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -61
  101. package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -267
  102. package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -101
  103. package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -142
  104. package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -76
  105. package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -146
  106. package/skills/analyze-product-requirements/SKILL.md +90 -90
  107. package/skills/analyze-product-requirements/agents/openai.yaml +4 -4
  108. package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -91
  109. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -56
  110. package/skills/analyze-product-requirements/references/example.md +86 -86
  111. package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -66
  112. package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -32
  113. package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -33
  114. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -35
  115. package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -193
  116. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -69
  117. package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -97
  118. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -98
  119. package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -156
  120. package/skills/browser-tools/browser-content.js +103 -103
  121. package/skills/browser-tools/browser-cookies.js +35 -35
  122. package/skills/browser-tools/browser-eval.js +53 -53
  123. package/skills/browser-tools/browser-hn-scraper.js +108 -108
  124. package/skills/browser-tools/browser-nav.js +44 -44
  125. package/skills/browser-tools/browser-pick.js +162 -162
  126. package/skills/browser-tools/browser-screenshot.js +34 -34
  127. package/skills/browser-tools/browser-start.js +86 -86
  128. package/skills/browser-tools/package-lock.json +2556 -2556
  129. package/skills/browser-tools/package.json +19 -19
  130. package/skills/code-review-core/SKILL.md +20 -20
  131. package/skills/codebase-scout/SKILL.md +19 -19
  132. package/skills/grill-me/SKILL.md +10 -10
  133. package/skills/loop-agent/SKILL.md +5 -2
  134. package/skills/loop-agent/references/README.md +67 -67
  135. package/skills/loop-agent/references/command-reference.md +17 -15
  136. package/skills/loop-agent/references/docs-converge.md +126 -126
  137. package/skills/loop-agent/references/harness-policy.md +3 -4
  138. package/skills/loop-agent/references/learned/README.md +21 -21
  139. package/skills/loop-agent/references/long-running-loop.md +57 -57
  140. package/skills/loop-agent/references/one-shot-runs.md +85 -85
  141. package/skills/loop-agent/references/pi-prompt.md +23 -23
  142. package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
  143. package/skills/loop-agent/references/post-implementation-and-patterns.md +1 -1
  144. package/skills/loop-agent/references/source-and-plan-practice.md +3 -2
  145. package/skills/loop-agent/references/task-workflow.md +7 -5
  146. package/skills/playwright-cli/SKILL.md +420 -420
  147. package/skills/playwright-cli/references/element-attributes.md +23 -23
  148. package/skills/playwright-cli/references/playwright-tests.md +39 -39
  149. package/skills/playwright-cli/references/request-mocking.md +87 -87
  150. package/skills/playwright-cli/references/running-code.md +241 -241
  151. package/skills/playwright-cli/references/session-management.md +225 -225
  152. package/skills/playwright-cli/references/storage-state.md +275 -275
  153. package/skills/playwright-cli/references/test-generation.md +433 -433
  154. package/skills/playwright-cli/references/tracing.md +139 -139
  155. package/skills/playwright-cli/references/video-recording.md +143 -143
  156. package/skills/requesting-code-review/SKILL.md +101 -101
  157. package/skills/requesting-code-review/code-reviewer.md +168 -168
  158. package/skills/systematic-debugging/CREATION-LOG.md +119 -119
  159. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  160. package/skills/systematic-debugging/condition-based-waiting.md +115 -115
  161. package/skills/systematic-debugging/defense-in-depth.md +122 -122
  162. package/skills/systematic-debugging/find-polluter.sh +63 -63
  163. package/skills/systematic-debugging/root-cause-tracing.md +169 -169
  164. package/skills/systematic-debugging/test-academic.md +14 -14
  165. package/skills/systematic-debugging/test-pressure-1.md +58 -58
  166. package/skills/systematic-debugging/test-pressure-2.md +68 -68
  167. package/skills/systematic-debugging/test-pressure-3.md +69 -69
  168. package/skills/using-git-worktrees/SKILL.md +215 -215
  169. package/skills/verification-before-completion/SKILL.md +154 -154
  170. package/skills/webapp-testing/SKILL.md +19 -19
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
- import { createReadStream } from "node:fs";
4
- import { lstat, readlink } from "node:fs/promises";
3
+ import { constants, createReadStream } from "node:fs";
4
+ import { access, lstat, readlink } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { pathMatchesPattern } from "../shared/git-progress.js";
7
7
  async function sha256File(filePath) {
@@ -164,40 +164,176 @@ export function validateShellWriteGuard(input) {
164
164
  }
165
165
  return { ok: violations.length === 0, violations };
166
166
  }
167
+ export class GitStatusUnavailableError extends Error {
168
+ diagnostics;
169
+ constructor(input) {
170
+ const last = input.attempts.at(-1);
171
+ const exitCode = last?.exitCode === undefined ? "unavailable" : String(last.exitCode);
172
+ const signal = last?.signal ?? "unavailable";
173
+ const phase = input.phase ?? "unspecified";
174
+ super(`git status failed after ${input.attempts.length} attempts (phase=${phase}, exit code=${exitCode}, exit code hex=${last?.exitCodeHex ?? "unavailable"}, signal=${signal}, cwd=${path.resolve(input.cwd)}): ${last?.detail ?? "unknown error"}`);
175
+ this.name = "GitStatusUnavailableError";
176
+ this.diagnostics = {
177
+ schemaVersion: 1,
178
+ phase,
179
+ cwd: path.resolve(input.cwd),
180
+ platform: input.platform ?? process.platform,
181
+ executableCandidates: input.executableCandidates ?? [],
182
+ requiredWindowsEnvironment: input.requiredWindowsEnvironment ?? requiredWindowsEnvironment(process.env),
183
+ attempts: input.attempts,
184
+ };
185
+ }
186
+ }
187
+ function requiredWindowsEnvironment(env) {
188
+ return {
189
+ SystemRootPresent: Boolean(env.SystemRoot ?? env.SYSTEMROOT),
190
+ windirPresent: Boolean(env.windir ?? env.WINDIR),
191
+ ComSpecPresent: Boolean(env.ComSpec ?? env.COMSPEC),
192
+ PATHEXTPresent: Boolean(env.PATHEXT),
193
+ };
194
+ }
195
+ function errorExitCode(error) {
196
+ if (error &&
197
+ typeof error === "object" &&
198
+ "exitCode" in error &&
199
+ typeof error.exitCode === "number") {
200
+ return error.exitCode;
201
+ }
202
+ return undefined;
203
+ }
204
+ function errorSignal(error) {
205
+ if (error &&
206
+ typeof error === "object" &&
207
+ "signal" in error &&
208
+ typeof error.signal === "string") {
209
+ return error.signal;
210
+ }
211
+ return null;
212
+ }
213
+ function formatWindowsExitCode(exitCode) {
214
+ if (exitCode === undefined)
215
+ return undefined;
216
+ return `0x${(exitCode >>> 0).toString(16).padStart(8, "0").toUpperCase()}`;
217
+ }
218
+ function isWindowsDllInitializationFailure(platform, exitCode) {
219
+ return platform === "win32" && exitCode !== undefined && (exitCode >>> 0) === 0xc0000142;
220
+ }
221
+ function boundedErrorDetail(error) {
222
+ const detail = error instanceof Error ? error.message : String(error);
223
+ return detail.length <= 1000 ? detail : `${detail.slice(0, 1000)}...[truncated]`;
224
+ }
225
+ export function deriveSameInstallationGitCandidates(primary) {
226
+ const normalized = path.win32.normalize(primary);
227
+ const lower = normalized.toLowerCase();
228
+ let fallback;
229
+ if (lower.endsWith("\\mingw64\\bin\\git.exe")) {
230
+ const root = path.win32.resolve(path.win32.dirname(normalized), "..", "..");
231
+ fallback = path.win32.join(root, "cmd", "git.exe");
232
+ }
233
+ else if (lower.endsWith("\\cmd\\git.exe")) {
234
+ const root = path.win32.resolve(path.win32.dirname(normalized), "..");
235
+ fallback = path.win32.join(root, "mingw64", "bin", "git.exe");
236
+ }
237
+ return Array.from(new Set([normalized, ...(fallback ? [fallback] : [])]));
238
+ }
239
+ async function resolveGitExecutableCandidates(platform, env) {
240
+ if (platform !== "win32")
241
+ return ["git"];
242
+ const pathValue = env.PATH ?? env.Path ?? "";
243
+ for (const rawEntry of pathValue.split(path.delimiter)) {
244
+ const entry = rawEntry.trim().replace(/^"|"$/g, "");
245
+ if (!entry)
246
+ continue;
247
+ const candidate = path.win32.join(entry, "git.exe");
248
+ try {
249
+ await access(candidate, constants.X_OK);
250
+ const sameInstallation = deriveSameInstallationGitCandidates(candidate);
251
+ const existing = [];
252
+ for (const executable of sameInstallation) {
253
+ try {
254
+ await access(executable, constants.X_OK);
255
+ existing.push(executable);
256
+ }
257
+ catch {
258
+ // Optional same-installation fallback is absent.
259
+ }
260
+ }
261
+ if (existing.length > 0)
262
+ return existing;
263
+ }
264
+ catch {
265
+ // Keep searching PATH entries.
266
+ }
267
+ }
268
+ return ["git"];
269
+ }
167
270
  export async function readGitStatusPorcelain(cwd, options = {}) {
168
- const attempts = Math.max(1, options.attempts ?? 5);
169
- const retryDelayMs = Math.max(0, options.retryDelayMs ?? 150);
170
- let lastError;
171
- for (let attempt = 1; attempt <= attempts; attempt += 1) {
271
+ return readGitStatusPorcelainWithDependencies(cwd, options, {
272
+ platform: process.platform,
273
+ resolveExecutableCandidates: () => resolveGitExecutableCandidates(process.platform, process.env),
274
+ runAttempt: readGitStatusPorcelainOnce,
275
+ sleep: async (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)),
276
+ now: Date.now,
277
+ env: process.env,
278
+ });
279
+ }
280
+ export async function readGitStatusPorcelainWithDependencies(cwd, options, dependencies) {
281
+ const candidates = await dependencies.resolveExecutableCandidates();
282
+ const executableCandidates = candidates.length > 0 ? candidates : ["git"];
283
+ const normalMaxAttempts = Math.max(1, options.attempts ?? 5);
284
+ const transientMaxAttempts = Math.max(1, options.attempts ?? 10);
285
+ const normalRetryBaseMs = Math.max(0, options.retryDelayMs ?? 150);
286
+ const transientRetryBaseMs = Math.max(0, options.retryDelayMs ?? 250);
287
+ const attemptDiagnostics = [];
288
+ for (let attempt = 1; attempt <= transientMaxAttempts; attempt += 1) {
289
+ const executable = executableCandidates[(attempt - 1) % executableCandidates.length];
290
+ const startedAt = dependencies.now();
172
291
  try {
173
- return await readGitStatusPorcelainOnce(cwd);
292
+ return await dependencies.runAttempt(cwd, executable);
174
293
  }
175
294
  catch (error) {
176
- lastError = error;
177
- if (attempt < attempts && retryDelayMs > 0) {
178
- await new Promise((resolve) => setTimeout(resolve, retryDelayMs * attempt));
179
- }
295
+ const exitCode = errorExitCode(error);
296
+ const transient = isWindowsDllInitializationFailure(dependencies.platform, exitCode);
297
+ attemptDiagnostics.push({
298
+ attempt,
299
+ executable,
300
+ ...(exitCode === undefined ? {} : { exitCode }),
301
+ ...(formatWindowsExitCode(exitCode)
302
+ ? { exitCodeHex: formatWindowsExitCode(exitCode) }
303
+ : {}),
304
+ signal: errorSignal(error),
305
+ durationMs: Math.max(0, dependencies.now() - startedAt),
306
+ ...(transient
307
+ ? { transientKind: "windows-dll-init-failed" }
308
+ : {}),
309
+ detail: boundedErrorDetail(error),
310
+ });
311
+ const maxAttempts = transient ? transientMaxAttempts : normalMaxAttempts;
312
+ if (attempt >= maxAttempts)
313
+ break;
314
+ const delayMs = transient
315
+ ? Math.min(4000, transientRetryBaseMs * 2 ** (attempt - 1))
316
+ : normalRetryBaseMs * attempt;
317
+ if (delayMs > 0)
318
+ await dependencies.sleep(delayMs);
180
319
  }
181
320
  }
182
- const detail = lastError instanceof Error ? lastError.message : String(lastError);
183
- const exitCode = lastError instanceof Error &&
184
- "exitCode" in lastError &&
185
- typeof lastError.exitCode === "number"
186
- ? String(lastError.exitCode)
187
- : "unavailable";
188
- const signal = lastError instanceof Error &&
189
- "signal" in lastError &&
190
- typeof lastError.signal === "string"
191
- ? lastError.signal
192
- : "unavailable";
193
- throw new Error(`git status failed after ${attempts} attempts (exit code=${exitCode}, signal=${signal}, cwd=${path.resolve(cwd)}): ${detail}`, { cause: lastError });
321
+ throw new GitStatusUnavailableError({
322
+ cwd,
323
+ phase: options.phase,
324
+ platform: dependencies.platform,
325
+ executableCandidates,
326
+ requiredWindowsEnvironment: requiredWindowsEnvironment(dependencies.env ?? process.env),
327
+ attempts: attemptDiagnostics,
328
+ });
194
329
  }
195
- function readGitStatusPorcelainOnce(cwd) {
330
+ function readGitStatusPorcelainOnce(cwd, executable) {
196
331
  return new Promise((resolve, reject) => {
197
- const child = spawn("git", ["status", "--porcelain=v1", "--untracked-files=all"], {
332
+ const child = spawn(executable, ["status", "--porcelain=v1", "--untracked-files=all"], {
198
333
  cwd,
199
334
  env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
200
335
  stdio: ["ignore", "pipe", "pipe"],
336
+ windowsHide: true,
201
337
  });
202
338
  let stdout = "";
203
339
  let stderr = "";
@@ -29,7 +29,7 @@ export function resolveArtifactWriteDir(options) {
29
29
  }
30
30
  export function buildArtifactPathPrompt(writeDir) {
31
31
  if (!writeDir)
32
- return `After changes, write artifacts/修改记录.md and artifacts/验证结果.md with verification evidence.
32
+ return `After changes, write artifacts/修改记录.md and artifacts/验证结果.md with verification evidence.
33
33
  ${ARTIFACT_INSTRUCTIONS}`;
34
34
  return [
35
35
  `After changes, write the following files:`,
@@ -0,0 +1,215 @@
1
+ import { TASK_CONTRACT_DRAFT_SCHEMA_VERSION } from "../contract/constants.js";
2
+ import { taskKindSchema } from "../config-types.js";
3
+ import { assessPreparePaths, mergeForbiddenPaths, normalizePreparePath, dedupeStable, } from "./path-policy.js";
4
+ function trimNonEmpty(value) {
5
+ if (value === undefined)
6
+ return undefined;
7
+ const trimmed = value.trim();
8
+ return trimmed ? trimmed : undefined;
9
+ }
10
+ function cleanStringList(values) {
11
+ if (values === undefined)
12
+ return undefined;
13
+ return values.map((v) => v.trim()).filter(Boolean);
14
+ }
15
+ function resolveTaskKind(flags, baseDraft, existing) {
16
+ const candidate = flags.taskKind ?? baseDraft?.taskKind ?? existing?.taskKind ?? "standard";
17
+ const parsed = taskKindSchema.safeParse(candidate);
18
+ return parsed.success ? parsed.data : String(candidate);
19
+ }
20
+ function resolveTitle(taskId, flags, facts, baseDraft, existing) {
21
+ return (trimNonEmpty(flags.title) ??
22
+ trimNonEmpty(baseDraft?.title) ??
23
+ trimNonEmpty(facts?.title) ??
24
+ trimNonEmpty(existing?.title) ??
25
+ taskId);
26
+ }
27
+ function resolveObjective(flags, facts, baseDraft, title) {
28
+ return (trimNonEmpty(flags.objective) ??
29
+ trimNonEmpty(baseDraft?.requirement.objective) ??
30
+ trimNonEmpty(facts?.objective) ??
31
+ title);
32
+ }
33
+ function resolveStringArrayField(input) {
34
+ if (input.explicit !== undefined) {
35
+ return cleanStringList(input.explicit) ?? [];
36
+ }
37
+ if (input.base && input.base.length > 0)
38
+ return [...input.base];
39
+ if (input.facts && input.facts.length > 0)
40
+ return [...input.facts];
41
+ return [];
42
+ }
43
+ function resolveAcceptance(input) {
44
+ if (input.explicit !== undefined) {
45
+ return input.explicit
46
+ .map((item) => ({ id: item.id.trim(), text: item.text.trim() }))
47
+ .filter((item) => item.id && item.text);
48
+ }
49
+ if (input.base && input.base.length > 0)
50
+ return [...input.base];
51
+ if (input.facts && input.facts.length > 0)
52
+ return [...input.facts];
53
+ return [];
54
+ }
55
+ function resolveAllowedPaths(input) {
56
+ const source = input.flags.allowedPaths !== undefined
57
+ ? (cleanStringList(input.flags.allowedPaths) ?? [])
58
+ : input.baseDraft?.constraints.allowedPaths?.length
59
+ ? [...input.baseDraft.constraints.allowedPaths]
60
+ : input.existing?.allowedPaths?.length
61
+ ? [...input.existing.allowedPaths]
62
+ : [];
63
+ return source.map((raw) => {
64
+ const n = normalizePreparePath(raw);
65
+ return n.ok ? n.value : raw.trim();
66
+ });
67
+ }
68
+ function resolveVerifyCommands(input) {
69
+ if (input.flags.verifyCommands !== undefined) {
70
+ return input.flags.verifyCommands.map((cmd) => ({
71
+ label: cmd.label.trim(),
72
+ command: cmd.command.trim(),
73
+ ...(cmd.timeoutMs !== undefined ? { timeoutMs: cmd.timeoutMs } : {}),
74
+ }));
75
+ }
76
+ if (input.baseDraft?.verification.commands?.length) {
77
+ return input.baseDraft.verification.commands.map((cmd) => ({
78
+ label: cmd.label,
79
+ command: cmd.command,
80
+ ...(cmd.timeoutMs !== undefined ? { timeoutMs: cmd.timeoutMs } : {}),
81
+ }));
82
+ }
83
+ if (input.existing?.verifyCommands?.length) {
84
+ return input.existing.verifyCommands.map((cmd) => ({
85
+ label: cmd.label,
86
+ command: cmd.command,
87
+ ...(cmd.timeoutMs !== undefined ? { timeoutMs: cmd.timeoutMs } : {}),
88
+ }));
89
+ }
90
+ return [];
91
+ }
92
+ /**
93
+ * Build a thin TaskContractDraftV1 from draft base / PRD facts / flags / task config.
94
+ * Engineering fields never come from PRD facts.
95
+ */
96
+ export function buildPrepareDraft(input) {
97
+ const { taskId, existingTaskConfig, baseDraft, facts, flags, references } = input;
98
+ const title = resolveTitle(taskId, flags, facts, baseDraft, existingTaskConfig);
99
+ const taskKind = resolveTaskKind(flags, baseDraft, existingTaskConfig);
100
+ const featureId = trimNonEmpty(flags.featureId) ??
101
+ trimNonEmpty(baseDraft?.featureId) ??
102
+ trimNonEmpty(existingTaskConfig?.featureId) ??
103
+ undefined;
104
+ const objective = resolveObjective(flags, facts, baseDraft, title);
105
+ const scope = resolveStringArrayField({
106
+ explicit: flags.scope,
107
+ base: baseDraft?.requirement.scope,
108
+ facts: facts?.scope,
109
+ });
110
+ const nonGoals = resolveStringArrayField({
111
+ explicit: flags.nonGoals,
112
+ base: baseDraft?.requirement.nonGoals,
113
+ facts: facts?.nonGoals,
114
+ });
115
+ const acceptanceCriteria = resolveAcceptance({
116
+ explicit: flags.acceptanceCriteria,
117
+ base: baseDraft?.requirement.acceptanceCriteria,
118
+ facts: facts?.acceptanceCriteria,
119
+ });
120
+ const allowedPaths = resolveAllowedPaths({
121
+ flags,
122
+ baseDraft,
123
+ existing: existingTaskConfig,
124
+ });
125
+ // D12: protected defaults always merge unless explicitly disabled.
126
+ // Explicit --forbidden-path appends; does not replace defaults.
127
+ const finalForbidden = mergeForbiddenPaths({
128
+ existing: baseDraft?.constraints.forbiddenPaths ??
129
+ existingTaskConfig?.forbiddenPaths ??
130
+ [],
131
+ explicit: flags.forbiddenPaths,
132
+ noDefaultForbiddenPaths: flags.noDefaultForbiddenPaths,
133
+ });
134
+ const invariants = resolveStringArrayField({
135
+ explicit: flags.invariants,
136
+ base: baseDraft?.constraints.invariants,
137
+ facts: facts?.invariants,
138
+ });
139
+ const openQuestions = resolveStringArrayField({
140
+ explicit: flags.openQuestions,
141
+ base: baseDraft?.openQuestions,
142
+ facts: facts?.openQuestions,
143
+ });
144
+ const assumptions = resolveStringArrayField({
145
+ explicit: flags.assumptions,
146
+ base: baseDraft?.assumptions,
147
+ facts: facts?.assumptions,
148
+ });
149
+ const verifyCommands = resolveVerifyCommands({
150
+ flags,
151
+ baseDraft,
152
+ existing: existingTaskConfig,
153
+ });
154
+ const pathAssessment = assessPreparePaths({
155
+ allowedPaths,
156
+ forbiddenPaths: finalForbidden,
157
+ noDefaultForbiddenPaths: flags.noDefaultForbiddenPaths,
158
+ });
159
+ const draft = {
160
+ schemaVersion: TASK_CONTRACT_DRAFT_SCHEMA_VERSION,
161
+ taskId,
162
+ title,
163
+ taskKind: taskKind,
164
+ requirement: {
165
+ objective,
166
+ scope,
167
+ nonGoals,
168
+ acceptanceCriteria,
169
+ },
170
+ constraints: {
171
+ invariants,
172
+ allowedPaths: pathAssessment.allowed.length > 0
173
+ ? pathAssessment.allowed
174
+ : dedupeStable(allowedPaths),
175
+ forbiddenPaths: pathAssessment.forbidden.length > 0
176
+ ? pathAssessment.forbidden
177
+ : dedupeStable(finalForbidden),
178
+ },
179
+ verification: {
180
+ commands: verifyCommands
181
+ .filter((cmd) => cmd.label && cmd.command)
182
+ .map((cmd) => ({
183
+ label: cmd.label,
184
+ command: cmd.command,
185
+ ...(cmd.timeoutMs !== undefined ? { timeoutMs: cmd.timeoutMs } : {}),
186
+ })),
187
+ },
188
+ };
189
+ if (featureId)
190
+ draft.featureId = featureId;
191
+ if (references && references.length > 0) {
192
+ draft.references = references.map((ref) => ({
193
+ role: ref.role,
194
+ ref: ref.ref,
195
+ }));
196
+ }
197
+ else if (baseDraft?.references?.length) {
198
+ draft.references = [...baseDraft.references];
199
+ }
200
+ if (openQuestions.length > 0)
201
+ draft.openQuestions = openQuestions;
202
+ if (assumptions.length > 0)
203
+ draft.assumptions = assumptions;
204
+ if (baseDraft?.sourceResolutions?.length) {
205
+ draft.sourceResolutions = [...baseDraft.sourceResolutions];
206
+ }
207
+ if (baseDraft?.hardConstraints?.length && invariants.length === 0) {
208
+ draft.hardConstraints = [...baseDraft.hardConstraints];
209
+ }
210
+ return {
211
+ draft,
212
+ pathAssessment,
213
+ acceptanceConflicts: facts?.acceptanceConflicts ?? [],
214
+ };
215
+ }
@@ -0,0 +1,195 @@
1
+ const FRESH_SOURCE_MISSING_CODES = new Set([
2
+ "MISSING_REQUIREMENT",
3
+ "MISSING_CONSTRAINTS",
4
+ ]);
5
+ export function isFreshLegacyUnversioned(state, sourceFilesPresent) {
6
+ if (state.effectiveStatus !== "legacy-unversioned")
7
+ return false;
8
+ if (state.observationErrors.some((e) => e.code === "NOT_FOUND"))
9
+ return false;
10
+ // Fresh new-task: both source files missing/empty. Any non-empty source is dirty.
11
+ if (sourceFilesPresent &&
12
+ (sourceFilesPresent.requirement || sourceFilesPresent.constraints)) {
13
+ return false;
14
+ }
15
+ const codes = state.observationErrors.map((e) => e.code);
16
+ // Allow empty errors only when caller already proved both files are empty.
17
+ if (codes.length === 0) {
18
+ return Boolean(sourceFilesPresent);
19
+ }
20
+ return codes.every((code) => FRESH_SOURCE_MISSING_CODES.has(code));
21
+ }
22
+ export function observationBlocksPrepare(state) {
23
+ if (state.observationErrors.some((e) => e.code === "NOT_FOUND")) {
24
+ return {
25
+ code: "TASK_NOT_FOUND",
26
+ level: "blocking",
27
+ message: "task not found",
28
+ };
29
+ }
30
+ if (state.effectiveStatus === "transaction-incomplete") {
31
+ return {
32
+ code: "TRANSACTION_INCOMPLETE",
33
+ level: "blocking",
34
+ message: "unfinished contract transaction; run task contract recover before prepare",
35
+ };
36
+ }
37
+ const disallowed = state.observationErrors.filter((e) => !FRESH_SOURCE_MISSING_CODES.has(e.code));
38
+ // EXTERNALLY_MODIFIED alone is the dirty-source signal handled by C6.
39
+ // Any other observation code (e.g. MANIFEST_MISSING, OBSERVE_FAILED,
40
+ // INVALID_CONTRACT_JSON) is independently blocking; force cannot bypass it.
41
+ const hard = disallowed.filter((e) => e.code !== "EXTERNALLY_MODIFIED");
42
+ if (hard.length > 0) {
43
+ return {
44
+ code: "OBSERVATION_ERROR",
45
+ level: "blocking",
46
+ message: hard.map((e) => e.message).join("; "),
47
+ };
48
+ }
49
+ return null;
50
+ }
51
+ export function listPrepareGaps(input) {
52
+ const gaps = [];
53
+ const risks = [];
54
+ const { draft } = input;
55
+ const obs = observationBlocksPrepare(input.state);
56
+ if (obs)
57
+ gaps.push(obs);
58
+ if (!draft.requirement.objective?.trim()) {
59
+ gaps.push({
60
+ code: "EMPTY_OBJECTIVE",
61
+ level: "blocking",
62
+ field: "objective",
63
+ message: "requirement.objective is required",
64
+ });
65
+ }
66
+ if (draft.requirement.acceptanceCriteria.length === 0) {
67
+ gaps.push({
68
+ code: "EMPTY_ACCEPTANCE",
69
+ level: "blocking",
70
+ field: "acceptanceCriteria",
71
+ message: "at least one acceptance criterion is required",
72
+ });
73
+ }
74
+ if (input.acceptanceConflicts && input.acceptanceConflicts.length > 0) {
75
+ for (const conflict of input.acceptanceConflicts) {
76
+ gaps.push({
77
+ code: "ACCEPTANCE_ID_CONFLICT",
78
+ level: "blocking",
79
+ field: "acceptanceCriteria",
80
+ message: `acceptance criterion id ${conflict.id} has conflicting texts`,
81
+ });
82
+ }
83
+ }
84
+ if (draft.constraints.allowedPaths.length === 0) {
85
+ gaps.push({
86
+ code: "EMPTY_ALLOWED_PATHS",
87
+ level: "blocking",
88
+ field: "allowedPaths",
89
+ message: "allowedPaths must be non-empty before apply",
90
+ });
91
+ }
92
+ if (!draft.taskKind?.trim()) {
93
+ gaps.push({
94
+ code: "EMPTY_TASK_KIND",
95
+ level: "blocking",
96
+ field: "taskKind",
97
+ message: "taskKind is required",
98
+ });
99
+ }
100
+ if (draft.taskKind === "knowledge-sync" && !draft.featureId) {
101
+ gaps.push({
102
+ code: "MISSING_FEATURE_ID",
103
+ level: "blocking",
104
+ field: "featureId",
105
+ message: "featureId is required when taskKind is knowledge-sync",
106
+ });
107
+ }
108
+ if (draft.requirement.scope.length === 0) {
109
+ gaps.push({
110
+ code: "EMPTY_SCOPE",
111
+ level: "warning",
112
+ field: "scope",
113
+ message: "scope is empty; projection will show (none)",
114
+ });
115
+ }
116
+ if (draft.requirement.nonGoals.length === 0) {
117
+ gaps.push({
118
+ code: "EMPTY_NON_GOALS",
119
+ level: "warning",
120
+ field: "nonGoals",
121
+ message: "nonGoals is empty; projection will show (none)",
122
+ });
123
+ }
124
+ if (draft.verification.commands.length === 0) {
125
+ gaps.push({
126
+ code: "EMPTY_VERIFY",
127
+ level: "warning",
128
+ field: "verifyCommands",
129
+ message: "verifyCommands is empty",
130
+ });
131
+ }
132
+ if (input.sourceIntegrity) {
133
+ for (const issue of input.sourceIntegrity.issues) {
134
+ if (issue.level === "blocking") {
135
+ gaps.push({
136
+ code: issue.code,
137
+ level: "blocking",
138
+ message: issue.message,
139
+ field: issue.path,
140
+ });
141
+ }
142
+ else {
143
+ gaps.push({
144
+ code: issue.code,
145
+ level: "warning",
146
+ message: issue.message,
147
+ field: issue.path,
148
+ });
149
+ }
150
+ }
151
+ }
152
+ if (input.pathAssessment) {
153
+ risks.push(...input.pathAssessment.blocking, ...input.pathAssessment.elevated, ...input.pathAssessment.warnings);
154
+ for (const risk of input.pathAssessment.blocking) {
155
+ gaps.push({
156
+ code: risk.code,
157
+ level: "blocking",
158
+ message: risk.message,
159
+ field: risk.path,
160
+ });
161
+ }
162
+ }
163
+ // Dirty source gate (C6 / D14)
164
+ const status = input.state.effectiveStatus;
165
+ const hasExistingSource = Boolean(input.sourceFilesPresent?.requirement ||
166
+ input.sourceFilesPresent?.constraints);
167
+ const hasDirtySource = status === "externally-modified" ||
168
+ (status === "legacy-unversioned" && hasExistingSource);
169
+ if (hasDirtySource && !input.forceOverwriteSource) {
170
+ gaps.push({
171
+ code: "DIRTY_SOURCE",
172
+ level: "blocking",
173
+ message: "existing non-empty or externally-modified source refuses --apply; use show/diff/adopt/recover or --force-overwrite-source",
174
+ });
175
+ risks.push({
176
+ code: "DIRTY_SOURCE",
177
+ level: "blocking",
178
+ message: "source is dirty/externally-modified; prepare will not overwrite without --force-overwrite-source",
179
+ });
180
+ }
181
+ if (!input.hasImportedPrd) {
182
+ gaps.push({
183
+ code: "NO_IMPORTED_PRD",
184
+ level: "warning",
185
+ message: "no imported PRD in use; prefer detailed PRD → import-prd → prepare (escape hatch only)",
186
+ });
187
+ }
188
+ return { gaps, risks };
189
+ }
190
+ export function hasBlockingGaps(gaps) {
191
+ return gaps.some((gap) => gap.level === "blocking");
192
+ }
193
+ export function hasBlockingRisks(risks) {
194
+ return risks.some((risk) => risk.level === "blocking");
195
+ }
@@ -0,0 +1,7 @@
1
+ export * from "./types.js";
2
+ export * from "./path-policy.js";
3
+ export * from "./parse-intent.js";
4
+ export * from "./reference-integrity.js";
5
+ export * from "./completeness.js";
6
+ export * from "./build-draft.js";
7
+ export * from "./prepare.js";