@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.
- package/CHANGELOG.md +1032 -1020
- package/README.md +8 -3
- package/bin/loop-agent.js +21 -21
- package/dist/cli/command-definitions.js +25 -10
- package/dist/cli/help.js +4 -3
- package/dist/cli/program.js +43 -17
- package/dist/commands/cursor-prompt.js +6 -6
- package/dist/commands/import-prd.js +7 -2
- package/dist/commands/init.js +7 -5
- package/dist/commands/loop-benchmark.js +11 -11
- package/dist/commands/pi-reuse-benchmark.js +16 -16
- package/dist/commands/task-source-prepare.js +468 -0
- package/dist/executors/dag-pi-executor.js +40 -5
- package/dist/executors/shell-write-guard.js +161 -25
- package/dist/sidecars/cursor-prompt/executor.js +1 -1
- package/dist/task/source-prepare/build-draft.js +215 -0
- package/dist/task/source-prepare/completeness.js +195 -0
- package/dist/task/source-prepare/index.js +7 -0
- package/dist/task/source-prepare/parse-intent.js +373 -0
- package/dist/task/source-prepare/path-policy.js +197 -0
- package/dist/task/source-prepare/prepare.js +506 -0
- package/dist/task/source-prepare/reference-integrity.js +274 -0
- package/dist/task/source-prepare/types.js +7 -0
- package/dist/worker/observe/static/copy.js +67 -67
- package/dist/worker/observe/static/dag-layout.d.ts +31 -31
- package/dist/worker/observe/static/dag-layout.js +83 -83
- package/dist/worker/observe/static/dom.js +220 -220
- package/dist/worker/observe/static/relations.js +133 -133
- package/dist/worker/observe/static/router.js +93 -93
- package/dist/worker/observe/static/run-processing.js +148 -148
- package/dist/worker/observe/static/views/batch.js +227 -227
- package/dist/worker/observe/static/views/dag-graph.js +172 -172
- package/dist/worker/observe/static/views/failures.js +143 -143
- package/dist/worker/observe/static/views/feature.js +492 -492
- package/dist/worker/observe/static/views/run.js +453 -453
- package/dist/worker/observe/static/views/shell.js +7 -7
- package/dist/worker/observe/static/views/timeline.js +163 -163
- package/dist/workflows/dag/canvas-observer.js +275 -275
- package/docs/skills/README.md +7 -7
- package/docs/templates/adr.md +60 -60
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
- package/docs/templates/agent-dag-report.schema.json +473 -473
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
- package/docs/templates/backend-test-result.schema.json +99 -99
- package/docs/templates/feature-spec.md +53 -53
- package/docs/templates/frontend-design-contract.md +42 -42
- package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -17
- package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -21
- package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -29
- package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -29
- package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -27
- package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -27
- package/docs/templates/frontend-eval/metrics.md +138 -138
- package/docs/templates/frontend-eval/smoke-targets.md +53 -53
- package/docs/templates/frontend-task-constraints.md +35 -35
- package/docs/templates/frontend-task-requirement.md +70 -70
- package/docs/templates/init-evolution-review.md +35 -35
- package/docs/templates/init-managed-agents.md +10 -5
- package/docs/templates/interactive-ui-round2-experiment.md +66 -66
- package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
- package/docs/templates/knowledge-sync-dag.json +178 -178
- package/docs/templates/knowledge-sync-draft.schema.json +71 -71
- package/docs/templates/product-line/closeout.yaml +9 -9
- package/docs/templates/product-line/design.md +13 -13
- package/docs/templates/product-line/links.md +10 -10
- package/docs/templates/product-line/requirement.md +17 -17
- package/docs/templates/product-line/test-plan.md +7 -7
- package/docs/templates/production-readiness-checklist.md +57 -57
- package/docs/templates/project-start-checklist.md +9 -9
- package/docs/templates/qa-report.md +48 -48
- package/docs/templates/sprint-contract.md +29 -29
- package/docs/templates/worker-dogfood-evidence.md +80 -80
- package/docs/templates/worker-dogfood-setup.md +68 -68
- package/package.json +1 -1
- package/scripts/kb-bootstrap-init-skeleton.sh +0 -0
- package/scripts/kb-graph-incremental-prepare.mjs +386 -386
- package/scripts/kb-graph-materialize.mjs +105 -105
- package/scripts/kb-graph-promote.mjs +164 -164
- package/scripts/kb-query.mjs +554 -554
- package/skills/ai-engineering-context/SKILL.md +48 -48
- package/skills/analyze-product-dependencies/SKILL.md +67 -67
- package/skills/analyze-product-dependencies/agents/openai.yaml +4 -4
- package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -30
- package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -28
- package/skills/analyze-product-dependencies/references/example.md +76 -76
- package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -35
- package/skills/analyze-product-dependencies/references/input-contract.md +11 -11
- package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -61
- package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -267
- package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -101
- package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -142
- package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -76
- package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -146
- package/skills/analyze-product-requirements/SKILL.md +90 -90
- package/skills/analyze-product-requirements/agents/openai.yaml +4 -4
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -91
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -56
- package/skills/analyze-product-requirements/references/example.md +86 -86
- package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -66
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -32
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -33
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -35
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -193
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -69
- package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -97
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -98
- package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -156
- package/skills/browser-tools/browser-content.js +103 -103
- package/skills/browser-tools/browser-cookies.js +35 -35
- package/skills/browser-tools/browser-eval.js +53 -53
- package/skills/browser-tools/browser-hn-scraper.js +108 -108
- package/skills/browser-tools/browser-nav.js +44 -44
- package/skills/browser-tools/browser-pick.js +162 -162
- package/skills/browser-tools/browser-screenshot.js +34 -34
- package/skills/browser-tools/browser-start.js +86 -86
- package/skills/browser-tools/package-lock.json +2556 -2556
- package/skills/browser-tools/package.json +19 -19
- package/skills/code-review-core/SKILL.md +20 -20
- package/skills/codebase-scout/SKILL.md +19 -19
- package/skills/grill-me/SKILL.md +10 -10
- package/skills/loop-agent/SKILL.md +5 -2
- package/skills/loop-agent/references/README.md +67 -67
- package/skills/loop-agent/references/command-reference.md +17 -15
- package/skills/loop-agent/references/docs-converge.md +126 -126
- package/skills/loop-agent/references/harness-policy.md +3 -4
- package/skills/loop-agent/references/learned/README.md +21 -21
- package/skills/loop-agent/references/long-running-loop.md +57 -57
- package/skills/loop-agent/references/one-shot-runs.md +85 -85
- package/skills/loop-agent/references/pi-prompt.md +23 -23
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
- package/skills/loop-agent/references/post-implementation-and-patterns.md +1 -1
- package/skills/loop-agent/references/source-and-plan-practice.md +3 -2
- package/skills/loop-agent/references/task-workflow.md +7 -5
- package/skills/playwright-cli/SKILL.md +420 -420
- package/skills/playwright-cli/references/element-attributes.md +23 -23
- package/skills/playwright-cli/references/playwright-tests.md +39 -39
- package/skills/playwright-cli/references/request-mocking.md +87 -87
- package/skills/playwright-cli/references/running-code.md +241 -241
- package/skills/playwright-cli/references/session-management.md +225 -225
- package/skills/playwright-cli/references/storage-state.md +275 -275
- package/skills/playwright-cli/references/test-generation.md +433 -433
- package/skills/playwright-cli/references/tracing.md +139 -139
- package/skills/playwright-cli/references/video-recording.md +143 -143
- package/skills/requesting-code-review/SKILL.md +101 -101
- package/skills/requesting-code-review/code-reviewer.md +168 -168
- package/skills/systematic-debugging/CREATION-LOG.md +119 -119
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
- package/skills/systematic-debugging/condition-based-waiting.md +115 -115
- package/skills/systematic-debugging/defense-in-depth.md +122 -122
- package/skills/systematic-debugging/find-polluter.sh +63 -63
- package/skills/systematic-debugging/root-cause-tracing.md +169 -169
- package/skills/systematic-debugging/test-academic.md +14 -14
- package/skills/systematic-debugging/test-pressure-1.md +58 -58
- package/skills/systematic-debugging/test-pressure-2.md +68 -68
- package/skills/systematic-debugging/test-pressure-3.md +69 -69
- package/skills/using-git-worktrees/SKILL.md +215 -215
- package/skills/verification-before-completion/SKILL.md +154 -154
- 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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
292
|
+
return await dependencies.runAttempt(cwd, executable);
|
|
174
293
|
}
|
|
175
294
|
catch (error) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
:
|
|
188
|
-
|
|
189
|
-
|
|
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(
|
|
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
|
+
}
|