@tea-agent/loop-agent 0.25.6 → 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/AGENTS.md +2 -1
- package/CHANGELOG.md +27 -1
- package/README.md +8 -3
- 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/import-prd.js +7 -2
- package/dist/commands/init.js +7 -5
- package/dist/commands/task-source-prepare.js +468 -0
- package/dist/executors/dag-pi-executor.js +66 -25
- package/dist/executors/model-routing.js +34 -18
- package/dist/executors/shell-write-guard.js +161 -25
- package/dist/governance/manifest-types.js +33 -5
- 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/task/task-demand-routing.js +3 -1
- package/dist/worker/console/chat/model-resolver.js +15 -3
- package/dist/worker/observe/static/constants.js +3 -2
- package/dist/worker/observe/static/dag-model.js +1 -0
- package/dist/worker/observe/static/styles.css +182 -42
- package/dist/workflows/dag/lifecycle.js +40 -30
- package/dist/workflows/dag/node-execution.js +13 -0
- package/dist/workflows/dag/types.js +59 -19
- package/docs/templates/harness.schema.json +29 -7
- package/docs/templates/init-managed-agents.md +10 -5
- package/harness.json +1 -2
- package/package.json +1 -1
- package/skills/loop-agent/SKILL.md +5 -2
- package/skills/loop-agent/references/command-reference.md +17 -15
- package/skills/loop-agent/references/harness-policy.md +3 -4
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/skills/loop-agent/references/model-routing.md +2 -0
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { DEFAULT_DAG_EXECUTOR_MODELS, } from
|
|
1
|
+
import { DEFAULT_DAG_EXECUTOR_MODELS, } from "../workflows/dag/types.js";
|
|
2
|
+
import { normalizeExecutorTierValue, } from "../governance/manifest-types.js";
|
|
2
3
|
export const DEFAULT_DAG_MODELS = {
|
|
3
4
|
HIGH: "gpt-5.5",
|
|
4
5
|
MED: "gpt-5.5",
|
|
@@ -8,6 +9,16 @@ export const DEFAULT_DAG_MODELS = {
|
|
|
8
9
|
* DAG executor model tier keys that may carry a per-complexity override.
|
|
9
10
|
*/
|
|
10
11
|
const EXECUTOR_MODEL_TIERS = ["LOW", "MED", "HIGH"];
|
|
12
|
+
/** Resolve one tier to a model and optional explicit thinking override. */
|
|
13
|
+
export function resolveExecutorTierSelection(execConfig, tier) {
|
|
14
|
+
const tierSelection = normalizeExecutorTierValue(execConfig?.[tier]);
|
|
15
|
+
if (tierSelection)
|
|
16
|
+
return tierSelection;
|
|
17
|
+
const defaultModel = execConfig?.defaultModel;
|
|
18
|
+
if (defaultModel && defaultModel !== "default")
|
|
19
|
+
return { model: defaultModel };
|
|
20
|
+
return { model: DEFAULT_DAG_EXECUTOR_MODELS.pi[tier] };
|
|
21
|
+
}
|
|
11
22
|
/**
|
|
12
23
|
* Resolve the DAG executor model matrix for Pi from harness `executors.pi`.
|
|
13
24
|
*
|
|
@@ -19,22 +30,23 @@ const EXECUTOR_MODEL_TIERS = ["LOW", "MED", "HIGH"];
|
|
|
19
30
|
* The "default" literal (injected by the schema `.default("default")`) and
|
|
20
31
|
* absent/undefined both mean "no override, fall through".
|
|
21
32
|
*/
|
|
22
|
-
export function resolveExecutorModelMatrix(
|
|
23
|
-
const tierValue = (tier) => {
|
|
24
|
-
const tierOverride = execConfig?.[tier];
|
|
25
|
-
if (tierOverride && tierOverride !== "default")
|
|
26
|
-
return tierOverride;
|
|
27
|
-
const defaultModel = execConfig?.defaultModel;
|
|
28
|
-
if (defaultModel && defaultModel !== "default")
|
|
29
|
-
return defaultModel;
|
|
30
|
-
return DEFAULT_DAG_EXECUTOR_MODELS[executor][tier];
|
|
31
|
-
};
|
|
33
|
+
export function resolveExecutorModelMatrix(_executor, execConfig) {
|
|
32
34
|
return {
|
|
33
|
-
LOW:
|
|
34
|
-
MED:
|
|
35
|
-
HIGH:
|
|
35
|
+
LOW: resolveExecutorTierSelection(execConfig, "LOW").model,
|
|
36
|
+
MED: resolveExecutorTierSelection(execConfig, "MED").model,
|
|
37
|
+
HIGH: resolveExecutorTierSelection(execConfig, "HIGH").model,
|
|
36
38
|
};
|
|
37
39
|
}
|
|
40
|
+
/** Resolve only explicitly configured per-tier thinking values. */
|
|
41
|
+
export function resolveExecutorThinkingMatrix(execConfig) {
|
|
42
|
+
const result = {};
|
|
43
|
+
for (const tier of EXECUTOR_MODEL_TIERS) {
|
|
44
|
+
const selection = normalizeExecutorTierValue(execConfig?.[tier]);
|
|
45
|
+
if (selection?.thinking)
|
|
46
|
+
result[tier] = selection.thinking;
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
38
50
|
/**
|
|
39
51
|
* Resolve the Pi DAG executor model matrix from a harness manifest.
|
|
40
52
|
*/
|
|
@@ -55,7 +67,9 @@ export function resolveModelSelection(manifest, taskConfig, step, options) {
|
|
|
55
67
|
};
|
|
56
68
|
}
|
|
57
69
|
const profileName = resolveProfileName(manifest, taskConfig.complexity, step, retryAttempt);
|
|
58
|
-
const profile = profileName
|
|
70
|
+
const profile = profileName
|
|
71
|
+
? manifest.modelProfiles?.[profileName]
|
|
72
|
+
: undefined;
|
|
59
73
|
if (!profile) {
|
|
60
74
|
return {
|
|
61
75
|
modelConfig: manifest.models?.[step],
|
|
@@ -74,7 +88,9 @@ export function resolveModelSelection(manifest, taskConfig, step, options) {
|
|
|
74
88
|
};
|
|
75
89
|
}
|
|
76
90
|
function resolveProfileName(manifest, complexity, step, retryAttempt) {
|
|
77
|
-
if (step ===
|
|
91
|
+
if (step === "implement" &&
|
|
92
|
+
retryAttempt > 0 &&
|
|
93
|
+
manifest.modelRouting?.implementRetry) {
|
|
78
94
|
return manifest.modelRouting.implementRetry;
|
|
79
95
|
}
|
|
80
96
|
const route = manifest.modelRouting?.[step];
|
|
@@ -85,11 +101,11 @@ function resolveProfileName(manifest, complexity, step, retryAttempt) {
|
|
|
85
101
|
}
|
|
86
102
|
export function formatModelSelectionLabel(modelConfig, profileName) {
|
|
87
103
|
if (!modelConfig) {
|
|
88
|
-
return profileName ? `${profileName}` :
|
|
104
|
+
return profileName ? `${profileName}` : "default";
|
|
89
105
|
}
|
|
90
106
|
const base = modelConfig.provider && modelConfig.model
|
|
91
107
|
? `${modelConfig.provider}/${modelConfig.model}`
|
|
92
|
-
: modelConfig.model ??
|
|
108
|
+
: (modelConfig.model ?? "default");
|
|
93
109
|
return profileName ? `${profileName}:${base}` : base;
|
|
94
110
|
}
|
|
95
111
|
export function formatFallbackLabel(profile) {
|
|
@@ -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 = "";
|
|
@@ -18,6 +18,34 @@ export const worktreeManifestConfigSchema = z.object({
|
|
|
18
18
|
});
|
|
19
19
|
export const taskExecutorSchema = z.enum(["pi"]);
|
|
20
20
|
export const CURSOR_TASK_EXECUTOR_REMOVED_ERROR = 'task executor "cursor" is no longer supported; governed runtime is Pi-only';
|
|
21
|
+
/** Per-tier model override: bare model id string, or `{ model, thinking? }`. */
|
|
22
|
+
export const executorTierModelSchema = z.union([
|
|
23
|
+
z.string(),
|
|
24
|
+
z.object({
|
|
25
|
+
model: z.string().min(1),
|
|
26
|
+
thinking: z.string().optional(),
|
|
27
|
+
}),
|
|
28
|
+
]);
|
|
29
|
+
/** Normalize an executors.pi LOW|MED|HIGH value. */
|
|
30
|
+
export function normalizeExecutorTierValue(value) {
|
|
31
|
+
if (value === null || value === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (typeof value === "string") {
|
|
34
|
+
if (value === "default" || value.length === 0)
|
|
35
|
+
return undefined;
|
|
36
|
+
return { model: value };
|
|
37
|
+
}
|
|
38
|
+
if (typeof value !== "object" || Array.isArray(value))
|
|
39
|
+
return undefined;
|
|
40
|
+
const model = value.model;
|
|
41
|
+
if (typeof model !== "string" || model.length === 0 || model === "default") {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const thinking = value.thinking;
|
|
45
|
+
return typeof thinking === "string" && thinking.length > 0
|
|
46
|
+
? { model, thinking }
|
|
47
|
+
: { model };
|
|
48
|
+
}
|
|
21
49
|
export const executorManifestSchema = z.object({
|
|
22
50
|
description: z.string().optional(),
|
|
23
51
|
enabled: z.boolean().optional(),
|
|
@@ -27,9 +55,9 @@ export const executorManifestSchema = z.object({
|
|
|
27
55
|
* these take priority over defaultModel for the matching DAG executor tier.
|
|
28
56
|
* "default" literal and absent/undefined both mean "no override, fall through".
|
|
29
57
|
*/
|
|
30
|
-
LOW:
|
|
31
|
-
MED:
|
|
32
|
-
HIGH:
|
|
58
|
+
LOW: executorTierModelSchema.optional(),
|
|
59
|
+
MED: executorTierModelSchema.optional(),
|
|
60
|
+
HIGH: executorTierModelSchema.optional(),
|
|
33
61
|
requiresApiKey: z.string().optional(),
|
|
34
62
|
});
|
|
35
63
|
export const workflowPolicyProfileNameSchema = z.enum([
|
|
@@ -98,7 +126,7 @@ export const workflowPolicySchema = z
|
|
|
98
126
|
})
|
|
99
127
|
.optional()
|
|
100
128
|
.default({});
|
|
101
|
-
export const CURSOR_HARNESS_EXECUTOR_REMOVED_ERROR =
|
|
129
|
+
export const CURSOR_HARNESS_EXECUTOR_REMOVED_ERROR = "harness executors.cursor is no longer supported; remove it and use executors.pi only (Cursor is available only via cursor-prompt sidecar)";
|
|
102
130
|
export const harnessManifestSchema = z
|
|
103
131
|
.object({
|
|
104
132
|
version: z.number(),
|
|
@@ -160,7 +188,7 @@ export const harnessManifestSchema = z
|
|
|
160
188
|
if ("cursorExecutorUsage" in entrypoints) {
|
|
161
189
|
ctx.addIssue({
|
|
162
190
|
code: z.ZodIssueCode.custom,
|
|
163
|
-
message:
|
|
191
|
+
message: "entrypoints.cursorExecutorUsage is no longer supported; remove it (Cursor is cursor-prompt sidecar only)",
|
|
164
192
|
path: ["entrypoints", "cursorExecutorUsage"],
|
|
165
193
|
});
|
|
166
194
|
}
|
|
@@ -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
|
+
}
|