@tea-agent/loop-agent 0.28.7 → 0.28.9
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 +26 -0
- package/dist/application/task-lifecycle/advance.js +24 -1
- package/dist/application/task-lifecycle/plan-transitions.js +7 -4
- package/dist/cli/program.js +2 -2
- package/dist/commands/init.js +232 -65
- package/dist/executors/dag-pi-executor.js +25 -0
- package/dist/executors/model-routing.js +0 -58
- package/dist/executors/pi-sdk-executor.js +14 -1
- package/dist/executors/shell-executor.js +123 -36
- package/dist/governance/manifest-types.js +18 -51
- package/dist/task/source-prepare/build-draft.js +10 -9
- package/dist/task/source-prepare/prepare.js +107 -4
- package/dist/worker/scheduler/night-git-finalizer.js +28 -5
- package/dist/worker/scheduler/workspace-adapter.js +29 -1
- package/dist/workflows/dag/init-hybrid.js +184 -46
- package/dist/workflows/dag/node-execution.js +176 -21
- package/dist/workflows/dag/types.js +58 -0
- package/docs/governance/README.md +1 -1
- package/docs/templates/harness.schema.json +4 -8
- package/package.json +1 -1
- package/skills/loop-agent/references/command-reference.md +5 -2
- package/skills/loop-agent/references/model-routing.md +14 -7
|
@@ -59,61 +59,3 @@ export function resolveExecutorModelMatrices(manifest) {
|
|
|
59
59
|
export function resolvePiModelMatrix(manifest) {
|
|
60
60
|
return resolveExecutorModelMatrices(manifest).pi;
|
|
61
61
|
}
|
|
62
|
-
export function resolveModelSelection(manifest, taskConfig, step, options) {
|
|
63
|
-
const retryAttempt = options?.retryAttempt ?? 0;
|
|
64
|
-
if (Object.keys(manifest.modelProfiles ?? {}).length === 0) {
|
|
65
|
-
return {
|
|
66
|
-
modelConfig: manifest.models?.[step],
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
const profileName = resolveProfileName(manifest, taskConfig.complexity, step, retryAttempt);
|
|
70
|
-
const profile = profileName
|
|
71
|
-
? manifest.modelProfiles?.[profileName]
|
|
72
|
-
: undefined;
|
|
73
|
-
if (!profile) {
|
|
74
|
-
return {
|
|
75
|
-
modelConfig: manifest.models?.[step],
|
|
76
|
-
profileName,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
return {
|
|
80
|
-
modelConfig: {
|
|
81
|
-
provider: profile.provider,
|
|
82
|
-
model: profile.model,
|
|
83
|
-
thinking: profile.thinking,
|
|
84
|
-
timeoutMs: profile.timeoutMs ?? manifest.models?.[step]?.timeoutMs,
|
|
85
|
-
fallback: profile.fallback,
|
|
86
|
-
},
|
|
87
|
-
profileName,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
function resolveProfileName(manifest, complexity, step, retryAttempt) {
|
|
91
|
-
if (step === "implement" &&
|
|
92
|
-
retryAttempt > 0 &&
|
|
93
|
-
manifest.modelRouting?.implementRetry) {
|
|
94
|
-
return manifest.modelRouting.implementRetry;
|
|
95
|
-
}
|
|
96
|
-
const route = manifest.modelRouting?.[step];
|
|
97
|
-
if (!route) {
|
|
98
|
-
return undefined;
|
|
99
|
-
}
|
|
100
|
-
return route[complexity];
|
|
101
|
-
}
|
|
102
|
-
export function formatModelSelectionLabel(modelConfig, profileName) {
|
|
103
|
-
if (!modelConfig) {
|
|
104
|
-
return profileName ? `${profileName}` : "default";
|
|
105
|
-
}
|
|
106
|
-
const base = modelConfig.provider && modelConfig.model
|
|
107
|
-
? `${modelConfig.provider}/${modelConfig.model}`
|
|
108
|
-
: (modelConfig.model ?? "default");
|
|
109
|
-
return profileName ? `${profileName}:${base}` : base;
|
|
110
|
-
}
|
|
111
|
-
export function formatFallbackLabel(profile) {
|
|
112
|
-
if (!profile?.fallback) {
|
|
113
|
-
return undefined;
|
|
114
|
-
}
|
|
115
|
-
const fallback = profile.fallback;
|
|
116
|
-
return fallback.provider && fallback.model
|
|
117
|
-
? `${fallback.provider}/${fallback.model}`
|
|
118
|
-
: fallback.model;
|
|
119
|
-
}
|
|
@@ -457,6 +457,11 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
457
457
|
...(requireWriterCustomTools ? { requireWriterCustomTools: true } : {}),
|
|
458
458
|
});
|
|
459
459
|
let resolveStall;
|
|
460
|
+
let resolveSettlement;
|
|
461
|
+
let settled = false;
|
|
462
|
+
const settlementPromise = new Promise((resolve) => {
|
|
463
|
+
resolveSettlement = resolve;
|
|
464
|
+
});
|
|
460
465
|
const stallPromise = stallTimeoutMs > 0
|
|
461
466
|
? new Promise((resolve) => {
|
|
462
467
|
resolveStall = resolve;
|
|
@@ -470,6 +475,10 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
470
475
|
stallHandle = setTimeout(() => resolveStall?.("stall"), stallTimeoutMs);
|
|
471
476
|
};
|
|
472
477
|
unsubscribe = session.subscribe((event) => {
|
|
478
|
+
if (event.type === "agent_settled" && !settled) {
|
|
479
|
+
settled = true;
|
|
480
|
+
resolveSettlement?.("settled");
|
|
481
|
+
}
|
|
473
482
|
// Every real SDK event proves transport activity, including noisy deltas
|
|
474
483
|
// intentionally excluded from persisted JSONL and meaningful DAG progress.
|
|
475
484
|
armStallWatchdog();
|
|
@@ -502,6 +511,9 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
502
511
|
? `${filePrefix}\n${options.userMessage}`
|
|
503
512
|
: options.userMessage;
|
|
504
513
|
const promptPromise = session.prompt(promptMessage);
|
|
514
|
+
// A settlement event is permitted to win the race while prompt() remains
|
|
515
|
+
// pending. Observe a later rejection so it never becomes unhandled.
|
|
516
|
+
void promptPromise.catch(() => { });
|
|
505
517
|
armStallWatchdog();
|
|
506
518
|
const timeoutPromise = timeoutMs > 0
|
|
507
519
|
? new Promise((resolve) => {
|
|
@@ -512,10 +524,11 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
512
524
|
: null;
|
|
513
525
|
const raced = await Promise.race([
|
|
514
526
|
promptPromise.then(() => "done"),
|
|
527
|
+
settlementPromise,
|
|
515
528
|
...(timeoutPromise ? [timeoutPromise] : []),
|
|
516
529
|
...(stallPromise ? [stallPromise] : []),
|
|
517
530
|
]);
|
|
518
|
-
if (raced !== "done") {
|
|
531
|
+
if (raced !== "done" && raced !== "settled") {
|
|
519
532
|
timedOut = true;
|
|
520
533
|
appendStderr(raced === "stall"
|
|
521
534
|
? `pi SDK step stalled after ${stallTimeoutMs}ms with no provider activity`
|
|
@@ -35,6 +35,7 @@ import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
|
|
|
35
35
|
import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
|
|
36
36
|
import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
|
|
37
37
|
import { pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
38
|
+
import { parseAndValidateFinalWriteSetApproval, } from "../workflows/dag/node-execution.js";
|
|
38
39
|
import { buildShellProcessEnv } from "./shell-verification.js";
|
|
39
40
|
import { readRunState } from "../workflows/dag/run-store.js";
|
|
40
41
|
import { resolveDagTaskSourcePath } from "../task/dag-source-paths.js";
|
|
@@ -42,26 +43,25 @@ import { readProjectGovernanceContext } from "../workflows/dag/project-governanc
|
|
|
42
43
|
import { assertMavenPlanFresh, MavenPlanStaleError, } from "../verification/maven/index.js";
|
|
43
44
|
/** In-memory same-run success cache. Never shared across runIds. */
|
|
44
45
|
const sameRunShellCommandCaches = new Map();
|
|
45
|
-
function shellCommandCacheKey(runId,
|
|
46
|
-
return `${runId}::${
|
|
46
|
+
function shellCommandCacheKey(runId, workspaceFingerprint, contractKey) {
|
|
47
|
+
return `${runId}::${workspaceFingerprint}::${contractKey}`;
|
|
47
48
|
}
|
|
48
49
|
function buildShellCommandContractKey(input) {
|
|
50
|
+
const environment = Object.fromEntries((input.envAllowlist ?? []).sort().map((name) => [name, process.env[name] ?? null]));
|
|
49
51
|
return JSON.stringify({
|
|
50
|
-
commands: input.commands,
|
|
51
52
|
cwd: input.cwd,
|
|
52
53
|
timeoutMs: input.timeoutMs,
|
|
53
|
-
|
|
54
|
+
environment,
|
|
54
55
|
failFast: input.failFast,
|
|
55
56
|
nonZeroExitPolicy: input.nonZeroExitPolicy ?? "fail",
|
|
57
|
+
controllerInput: input.controllerInput ?? null,
|
|
56
58
|
});
|
|
57
59
|
}
|
|
58
60
|
export function clearSameRunShellCommandCachesForTests() {
|
|
59
61
|
sameRunShellCommandCaches.clear();
|
|
60
62
|
}
|
|
61
63
|
function shouldReuseSuccessfulShellCommands(shell) {
|
|
62
|
-
|
|
63
|
-
// carries final verifyEvidence so intermediate gates stay fail-closed fresh.
|
|
64
|
-
return shell.verifyEvidence?.phase === "final";
|
|
64
|
+
return shell.verifyEvidence?.phase === "intermediate" || shell.verifyEvidence?.phase === "final";
|
|
65
65
|
}
|
|
66
66
|
const DEFAULT_SHELL_TIMEOUT_MS = 300_000;
|
|
67
67
|
const SUMMARY_STDOUT_MAX = 4_000;
|
|
@@ -236,6 +236,15 @@ export function resolveShellCommands(shell) {
|
|
|
236
236
|
export async function executeShellCommand(input) {
|
|
237
237
|
return new Promise((resolve) => {
|
|
238
238
|
const startedAt = Date.now();
|
|
239
|
+
const startedAtIso = new Date(startedAt).toISOString();
|
|
240
|
+
let lastOutputActivityAt = 0;
|
|
241
|
+
const reportOutputActivity = () => {
|
|
242
|
+
const now = Date.now();
|
|
243
|
+
if (now - lastOutputActivityAt < 500)
|
|
244
|
+
return;
|
|
245
|
+
lastOutputActivityAt = now;
|
|
246
|
+
input.reportActivity?.({ attempt: 1, kind: "output", at: new Date(now).toISOString() });
|
|
247
|
+
};
|
|
239
248
|
const injectedEnv = input.dagRunMeta
|
|
240
249
|
? {
|
|
241
250
|
// Git Bash treats backslashes as escape characters inside double
|
|
@@ -271,6 +280,9 @@ export async function executeShellCommand(input) {
|
|
|
271
280
|
command: input.command,
|
|
272
281
|
cwd: input.cwd,
|
|
273
282
|
durationMs: Date.now() - startedAt,
|
|
283
|
+
wallDurationMs: Date.now() - startedAt,
|
|
284
|
+
startedAt: startedAtIso,
|
|
285
|
+
finishedAt: new Date().toISOString(),
|
|
274
286
|
stdout: formatBoundedOutput(stdout),
|
|
275
287
|
stdoutBytes: stdout.bytes,
|
|
276
288
|
stdoutTruncated: stdout.truncated,
|
|
@@ -293,10 +305,12 @@ export async function executeShellCommand(input) {
|
|
|
293
305
|
child.stdout.on("data", (chunk) => {
|
|
294
306
|
stdout = appendBoundedOutput(stdout, chunk);
|
|
295
307
|
appendOutputArtifact(input.outputArtifacts?.stdoutPath, chunk);
|
|
308
|
+
reportOutputActivity();
|
|
296
309
|
});
|
|
297
310
|
child.stderr.on("data", (chunk) => {
|
|
298
311
|
stderr = appendBoundedOutput(stderr, chunk);
|
|
299
312
|
appendOutputArtifact(input.outputArtifacts?.stderrPath, chunk);
|
|
313
|
+
reportOutputActivity();
|
|
300
314
|
});
|
|
301
315
|
child.on("error", (error) => {
|
|
302
316
|
stderr = appendBoundedOutput(stderr, `${stderr.text ? "\n" : ""}${error.message}`);
|
|
@@ -387,6 +401,17 @@ function summarizeCommandResults(results) {
|
|
|
387
401
|
stderr: truncateOutput(stderr, NODE_STDERR_MAX).text,
|
|
388
402
|
};
|
|
389
403
|
}
|
|
404
|
+
function parseCheckRepoSubcheckDurations(result) {
|
|
405
|
+
if (!/scripts\/check-repo\.sh/.test(result.command))
|
|
406
|
+
return [];
|
|
407
|
+
const durations = [];
|
|
408
|
+
for (const line of `${result.stdout}\n${result.stderr}`.split(/\r?\n/)) {
|
|
409
|
+
const match = /^CHECK_REPO_SUBCHECK_END name=(\S+) status=(success|failed) durationMs=(\d+)$/.exec(line.trim());
|
|
410
|
+
if (match)
|
|
411
|
+
durations.push({ name: match[1], status: match[2], durationMs: Number(match[3]) });
|
|
412
|
+
}
|
|
413
|
+
return durations;
|
|
414
|
+
}
|
|
390
415
|
export function buildShellResultSummaryMarkdown(input) {
|
|
391
416
|
const lines = [
|
|
392
417
|
"# Shell execution summary",
|
|
@@ -402,11 +427,11 @@ export function buildShellResultSummaryMarkdown(input) {
|
|
|
402
427
|
"",
|
|
403
428
|
"## Commands",
|
|
404
429
|
"",
|
|
405
|
-
"| # | ok | exitCode | failureCategory |
|
|
406
|
-
"
|
|
430
|
+
"| # | ok | exitCode | failureCategory | startedAt | finishedAt | wallDurationMs | executionDurationMs | reused | cwd | command |",
|
|
431
|
+
"|---|----|----------|-----------------|-----------|------------|----------------|---------------------|--------|-----|---------|",
|
|
407
432
|
];
|
|
408
433
|
input.results.forEach((result, index) => {
|
|
409
|
-
lines.push(`| ${index + 1} | ${result.ok} | ${result.exitCode ?? "null"} | ${result.failureCategory} | ${result.durationMs} | ${result.cwd} | ${result.command.replace(/\|/g, "\\|")} |`);
|
|
434
|
+
lines.push(`| ${index + 1} | ${result.ok} | ${result.exitCode ?? "null"} | ${result.failureCategory} | ${result.startedAt ?? "(unknown)"} | ${result.finishedAt ?? "(unknown)"} | ${result.wallDurationMs ?? result.durationMs} | ${result.executionDurationMs ?? result.durationMs} | ${result.reused === true} | ${result.cwd} | ${result.command.replace(/\|/g, "\\|")} |`);
|
|
410
435
|
});
|
|
411
436
|
if (input.writeGuardViolations && input.writeGuardViolations.length > 0) {
|
|
412
437
|
lines.push("", "## Write guard violations", "");
|
|
@@ -1299,6 +1324,7 @@ async function executePipelineCommands(input, meta, overrideCommands) {
|
|
|
1299
1324
|
timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
|
|
1300
1325
|
envAllowlist: shell.envAllowlist,
|
|
1301
1326
|
dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
|
|
1327
|
+
reportActivity: input.reportActivity,
|
|
1302
1328
|
outputArtifacts: {
|
|
1303
1329
|
stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
|
|
1304
1330
|
stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
|
|
@@ -1332,6 +1358,7 @@ async function executeFrontendVerificationBundle(input, meta) {
|
|
|
1332
1358
|
timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
|
|
1333
1359
|
envAllowlist: shell.envAllowlist,
|
|
1334
1360
|
dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
|
|
1361
|
+
reportActivity: input.reportActivity,
|
|
1335
1362
|
outputArtifacts: {
|
|
1336
1363
|
stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
|
|
1337
1364
|
stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
|
|
@@ -1382,6 +1409,7 @@ async function executeFrontendVerificationBundle(input, meta) {
|
|
|
1382
1409
|
timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
|
|
1383
1410
|
envAllowlist: shell.envAllowlist,
|
|
1384
1411
|
dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
|
|
1412
|
+
reportActivity: input.reportActivity,
|
|
1385
1413
|
outputArtifacts: {
|
|
1386
1414
|
stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
|
|
1387
1415
|
stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
|
|
@@ -1727,6 +1755,43 @@ async function executeFrontendLintBaseline(input, meta) {
|
|
|
1727
1755
|
}
|
|
1728
1756
|
export async function executeDagShellNode(input, meta) {
|
|
1729
1757
|
const shell = input.task.shell;
|
|
1758
|
+
if (shell?.finalWriteSetApprovalGate) {
|
|
1759
|
+
const started = Date.now();
|
|
1760
|
+
try {
|
|
1761
|
+
const state = await readRunState(meta.runDir);
|
|
1762
|
+
const writer = meta.spec.tasks.find((task) => task.id === shell.finalWriteSetApprovalGate.writerNodeId);
|
|
1763
|
+
if (!writer)
|
|
1764
|
+
throw new Error("final write-set approval gate writer is missing");
|
|
1765
|
+
const authorization = parseAndValidateFinalWriteSetApproval({
|
|
1766
|
+
task: writer,
|
|
1767
|
+
spec: meta.spec,
|
|
1768
|
+
state,
|
|
1769
|
+
});
|
|
1770
|
+
if (!authorization.ok)
|
|
1771
|
+
throw new Error(authorization.reason);
|
|
1772
|
+
return {
|
|
1773
|
+
ok: true,
|
|
1774
|
+
stdout: JSON.stringify({
|
|
1775
|
+
status: "approved",
|
|
1776
|
+
writerNodeId: writer.id,
|
|
1777
|
+
approvalDigest: authorization.approvalDigest,
|
|
1778
|
+
effectiveWriteSet: authorization.effectiveWriteSet,
|
|
1779
|
+
}),
|
|
1780
|
+
stderr: "",
|
|
1781
|
+
failureCategory: "success",
|
|
1782
|
+
durationMs: Date.now() - started,
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
catch (error) {
|
|
1786
|
+
return {
|
|
1787
|
+
ok: false,
|
|
1788
|
+
stdout: "",
|
|
1789
|
+
stderr: `final-write-set-approval-invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
1790
|
+
failureCategory: "invalid-output",
|
|
1791
|
+
durationMs: Date.now() - started,
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1730
1795
|
if (shell?.frontendBrowserToolPreflight) {
|
|
1731
1796
|
const started = Date.now();
|
|
1732
1797
|
try {
|
|
@@ -2160,6 +2225,9 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2160
2225
|
command,
|
|
2161
2226
|
cwd: input.cwd,
|
|
2162
2227
|
durationMs: 0,
|
|
2228
|
+
wallDurationMs: 0,
|
|
2229
|
+
startedAt: new Date().toISOString(),
|
|
2230
|
+
finishedAt: new Date().toISOString(),
|
|
2163
2231
|
exitCode: null,
|
|
2164
2232
|
failureCategory: "verification-plan-stale",
|
|
2165
2233
|
ok: false,
|
|
@@ -2198,6 +2266,7 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2198
2266
|
const timeoutMs = shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
|
|
2199
2267
|
const failFast = shell.failFast !== false;
|
|
2200
2268
|
const results = [];
|
|
2269
|
+
const executedCommandKeys = new Set();
|
|
2201
2270
|
let beforeStatus;
|
|
2202
2271
|
let workspaceFingerprint;
|
|
2203
2272
|
try {
|
|
@@ -2223,48 +2292,52 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2223
2292
|
workspaceFingerprint = undefined;
|
|
2224
2293
|
}
|
|
2225
2294
|
commandContractKey = buildShellCommandContractKey({
|
|
2226
|
-
commands,
|
|
2227
2295
|
cwd,
|
|
2228
2296
|
timeoutMs,
|
|
2229
2297
|
envAllowlist: shell.envAllowlist,
|
|
2230
2298
|
failFast,
|
|
2231
2299
|
nonZeroExitPolicy: shell.nonZeroExitPolicy,
|
|
2300
|
+
controllerInput: {
|
|
2301
|
+
sourceBinding: meta.spec.sourceBinding,
|
|
2302
|
+
taskContractBinding: meta.spec.taskContractBinding,
|
|
2303
|
+
runtimeContract: meta.spec.runtimeContract,
|
|
2304
|
+
},
|
|
2232
2305
|
});
|
|
2233
|
-
const cacheKey =
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
existing.commandContractKey === commandContractKey &&
|
|
2239
|
-
workspaceFingerprint &&
|
|
2240
|
-
existing.workspaceFingerprint === workspaceFingerprint) {
|
|
2306
|
+
const cacheKey = workspaceFingerprint
|
|
2307
|
+
? shellCommandCacheKey(meta.runId, workspaceFingerprint, commandContractKey)
|
|
2308
|
+
: undefined;
|
|
2309
|
+
const existing = cacheKey ? sameRunShellCommandCaches.get(cacheKey) : undefined;
|
|
2310
|
+
if (existing && existing.runId === meta.runId && existing.contractKey === commandContractKey) {
|
|
2241
2311
|
cache = existing;
|
|
2242
2312
|
}
|
|
2243
|
-
else {
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
byCommand: new Map(),
|
|
2252
|
-
};
|
|
2253
|
-
sameRunShellCommandCaches.set(cacheKey, cache);
|
|
2254
|
-
}
|
|
2313
|
+
else if (cacheKey && workspaceFingerprint) {
|
|
2314
|
+
cache = {
|
|
2315
|
+
runId: meta.runId,
|
|
2316
|
+
contractKey: commandContractKey,
|
|
2317
|
+
workspaceFingerprint,
|
|
2318
|
+
byCommand: new Map(),
|
|
2319
|
+
};
|
|
2320
|
+
sameRunShellCommandCaches.set(cacheKey, cache);
|
|
2255
2321
|
}
|
|
2256
2322
|
}
|
|
2257
2323
|
for (const command of commands) {
|
|
2258
2324
|
const commandNumber = results.length + 1;
|
|
2259
2325
|
// Cache each contractual occurrence independently. A result written for an
|
|
2260
2326
|
// earlier duplicate in this round must not satisfy a later occurrence.
|
|
2261
|
-
const commandCacheKey =
|
|
2327
|
+
const commandCacheKey = command;
|
|
2262
2328
|
const cached = cache?.byCommand.get(commandCacheKey);
|
|
2263
|
-
if (cached) {
|
|
2329
|
+
if (cached && !executedCommandKeys.has(commandCacheKey)) {
|
|
2330
|
+
const reusedAt = new Date().toISOString();
|
|
2264
2331
|
results.push({
|
|
2265
2332
|
command,
|
|
2266
2333
|
cwd: cached.cwd,
|
|
2267
|
-
durationMs:
|
|
2334
|
+
durationMs: 0,
|
|
2335
|
+
wallDurationMs: 0,
|
|
2336
|
+
executionDurationMs: cached.durationMs,
|
|
2337
|
+
startedAt: reusedAt,
|
|
2338
|
+
finishedAt: reusedAt,
|
|
2339
|
+
sourceNodeId: cached.sourceNodeId,
|
|
2340
|
+
sourceReceiptNodeId: cached.sourceReceiptNodeId,
|
|
2268
2341
|
exitCode: cached.exitCode,
|
|
2269
2342
|
failureCategory: cached.failureCategory,
|
|
2270
2343
|
ok: true,
|
|
@@ -2285,12 +2358,14 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2285
2358
|
timeoutMs,
|
|
2286
2359
|
envAllowlist: shell.envAllowlist,
|
|
2287
2360
|
dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
|
|
2361
|
+
reportActivity: input.reportActivity,
|
|
2288
2362
|
outputArtifacts: {
|
|
2289
2363
|
stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
|
|
2290
2364
|
stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
|
|
2291
2365
|
},
|
|
2292
2366
|
});
|
|
2293
2367
|
results.push(result);
|
|
2368
|
+
executedCommandKeys.add(commandCacheKey);
|
|
2294
2369
|
if (result.ok && cache) {
|
|
2295
2370
|
cache.byCommand.set(commandCacheKey, {
|
|
2296
2371
|
ok: true,
|
|
@@ -2304,6 +2379,10 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2304
2379
|
stderrTruncated: result.stderrTruncated,
|
|
2305
2380
|
timedOut: result.timedOut,
|
|
2306
2381
|
durationMs: result.durationMs,
|
|
2382
|
+
startedAt: result.startedAt ?? new Date().toISOString(),
|
|
2383
|
+
finishedAt: result.finishedAt ?? new Date().toISOString(),
|
|
2384
|
+
sourceNodeId: input.task.id,
|
|
2385
|
+
sourceReceiptNodeId: input.task.id,
|
|
2307
2386
|
cwd: result.cwd,
|
|
2308
2387
|
});
|
|
2309
2388
|
}
|
|
@@ -2330,8 +2409,8 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2330
2409
|
}
|
|
2331
2410
|
}
|
|
2332
2411
|
// Write-guard or workspace mutation invalidates same-run success reuse.
|
|
2333
|
-
if (!writeGuardOk && reuseEligible) {
|
|
2334
|
-
sameRunShellCommandCaches.delete(shellCommandCacheKey(meta.runId,
|
|
2412
|
+
if (!writeGuardOk && reuseEligible && workspaceFingerprint && commandContractKey) {
|
|
2413
|
+
sameRunShellCommandCaches.delete(shellCommandCacheKey(meta.runId, workspaceFingerprint, commandContractKey));
|
|
2335
2414
|
}
|
|
2336
2415
|
const commandsOk = results.every((result) => result.ok);
|
|
2337
2416
|
const recordNonZero = shell.nonZeroExitPolicy === "record";
|
|
@@ -2348,7 +2427,15 @@ export async function executeDagShellNode(input, meta) {
|
|
|
2348
2427
|
ok: result.ok,
|
|
2349
2428
|
exitCode: result.exitCode,
|
|
2350
2429
|
failureCategory: result.failureCategory,
|
|
2430
|
+
startedAt: result.startedAt,
|
|
2431
|
+
finishedAt: result.finishedAt,
|
|
2432
|
+
wallDurationMs: result.wallDurationMs,
|
|
2433
|
+
executionDurationMs: result.executionDurationMs ?? result.durationMs,
|
|
2351
2434
|
reused: result.reused === true,
|
|
2435
|
+
reuseSource: result.reused
|
|
2436
|
+
? { nodeId: result.sourceNodeId, receiptNodeId: result.sourceReceiptNodeId }
|
|
2437
|
+
: null,
|
|
2438
|
+
subchecks: parseCheckRepoSubcheckDurations(result),
|
|
2352
2439
|
})),
|
|
2353
2440
|
};
|
|
2354
2441
|
await writeDagNodeJsonArtifact(meta.runDir, input.task.id, "shell-command-receipt.json", receipt);
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
/** Compatibility type for Pi invocation internals; not a harness.json field. */
|
|
2
3
|
export const stepModelConfigSchema = z.object({
|
|
3
4
|
provider: z.string().optional(),
|
|
4
5
|
model: z.string().optional(),
|
|
5
6
|
thinking: z.string().optional(),
|
|
6
7
|
timeoutMs: z.number().int().positive().optional(),
|
|
7
8
|
});
|
|
8
|
-
export const modelProfileSchema = stepModelConfigSchema.extend({
|
|
9
|
-
fallback: stepModelConfigSchema.optional(),
|
|
10
|
-
});
|
|
11
|
-
export const complexityRouteSchema = z.object({
|
|
12
|
-
small: z.string().optional(),
|
|
13
|
-
medium: z.string().optional(),
|
|
14
|
-
large: z.string().optional(),
|
|
15
|
-
});
|
|
16
9
|
export const worktreeManifestConfigSchema = z.object({
|
|
17
10
|
rootRelativePath: z.string().optional().default(".worktrees"),
|
|
18
11
|
});
|
|
@@ -21,10 +14,12 @@ export const CURSOR_TASK_EXECUTOR_REMOVED_ERROR = 'task executor "cursor" is no
|
|
|
21
14
|
/** Per-tier model override: bare model id string, or `{ model, thinking? }`. */
|
|
22
15
|
export const executorTierModelSchema = z.union([
|
|
23
16
|
z.string(),
|
|
24
|
-
z
|
|
17
|
+
z
|
|
18
|
+
.object({
|
|
25
19
|
model: z.string().min(1),
|
|
26
20
|
thinking: z.string().optional(),
|
|
27
|
-
})
|
|
21
|
+
})
|
|
22
|
+
.strict(),
|
|
28
23
|
]);
|
|
29
24
|
/** Normalize an executors.pi LOW|MED|HIGH value. */
|
|
30
25
|
export function normalizeExecutorTierValue(value) {
|
|
@@ -46,20 +41,18 @@ export function normalizeExecutorTierValue(value) {
|
|
|
46
41
|
? { model, thinking }
|
|
47
42
|
: { model };
|
|
48
43
|
}
|
|
49
|
-
export const executorManifestSchema = z
|
|
44
|
+
export const executorManifestSchema = z
|
|
45
|
+
.object({
|
|
50
46
|
description: z.string().optional(),
|
|
51
47
|
enabled: z.boolean().optional(),
|
|
48
|
+
/** Compatibility fallback for old projects; fresh init writes LOW/MED/HIGH instead. */
|
|
52
49
|
defaultModel: z.string().optional().default("default"),
|
|
53
|
-
/**
|
|
54
|
-
* Per-complexity DAG model overrides. When set (and not the "default" sentinel),
|
|
55
|
-
* these take priority over defaultModel for the matching DAG executor tier.
|
|
56
|
-
* "default" literal and absent/undefined both mean "no override, fall through".
|
|
57
|
-
*/
|
|
50
|
+
/** Per-complexity overrides take priority over defaultModel. */
|
|
58
51
|
LOW: executorTierModelSchema.optional(),
|
|
59
52
|
MED: executorTierModelSchema.optional(),
|
|
60
53
|
HIGH: executorTierModelSchema.optional(),
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
})
|
|
55
|
+
.strict();
|
|
63
56
|
export const workflowPolicyProfileNameSchema = z.enum([
|
|
64
57
|
"minimal",
|
|
65
58
|
"standard",
|
|
@@ -192,6 +185,7 @@ export const workerPolicySchema = z.object({
|
|
|
192
185
|
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)";
|
|
193
186
|
export const harnessManifestSchema = z
|
|
194
187
|
.object({
|
|
188
|
+
$schema: z.string().optional(),
|
|
195
189
|
version: z.number(),
|
|
196
190
|
project: z.string(),
|
|
197
191
|
adapter: z.string().optional(),
|
|
@@ -207,48 +201,21 @@ export const harnessManifestSchema = z
|
|
|
207
201
|
entrypoints: z.record(z.string(), z.string().nullable()),
|
|
208
202
|
artifacts: z.record(z.string(), z.string().nullable()),
|
|
209
203
|
scripts: z.record(z.string(), z.string().nullable()),
|
|
210
|
-
models: z
|
|
211
|
-
.object({
|
|
212
|
-
analyze: stepModelConfigSchema.optional(),
|
|
213
|
-
plan: stepModelConfigSchema.optional(),
|
|
214
|
-
implement: stepModelConfigSchema.optional(),
|
|
215
|
-
verify: stepModelConfigSchema.optional(),
|
|
216
|
-
retrospective: stepModelConfigSchema.optional(),
|
|
217
|
-
})
|
|
218
|
-
.optional()
|
|
219
|
-
.default({}),
|
|
220
|
-
modelProfiles: z
|
|
221
|
-
.record(z.string(), modelProfileSchema)
|
|
222
|
-
.optional()
|
|
223
|
-
.default({}),
|
|
224
|
-
modelRouting: z
|
|
225
|
-
.object({
|
|
226
|
-
analyze: complexityRouteSchema.optional(),
|
|
227
|
-
plan: complexityRouteSchema.optional(),
|
|
228
|
-
implement: complexityRouteSchema.optional(),
|
|
229
|
-
retrospective: complexityRouteSchema.optional(),
|
|
230
|
-
implementRetry: z.string().optional(),
|
|
231
|
-
})
|
|
232
|
-
.optional()
|
|
233
|
-
.default({}),
|
|
234
204
|
workflowPolicy: workflowPolicySchema,
|
|
235
205
|
/** Optional; absent means Night Scheduler uses package defaults when enabled by callers. */
|
|
236
206
|
workerPolicy: workerPolicySchema.optional(),
|
|
237
207
|
worktree: worktreeManifestConfigSchema.optional(),
|
|
238
208
|
executors: z
|
|
239
|
-
.
|
|
209
|
+
.object({ pi: executorManifestSchema.optional() })
|
|
210
|
+
.strict()
|
|
240
211
|
.optional()
|
|
241
212
|
.default({}),
|
|
242
213
|
})
|
|
214
|
+
.strict()
|
|
243
215
|
.superRefine((manifest, ctx) => {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
code: z.ZodIssueCode.custom,
|
|
248
|
-
message: CURSOR_HARNESS_EXECUTOR_REMOVED_ERROR,
|
|
249
|
-
path: ["executors", "cursor"],
|
|
250
|
-
});
|
|
251
|
-
}
|
|
216
|
+
// Preserve the empty read-only compatibility projection for callers that
|
|
217
|
+
// inspect parsed manifests, while rejecting this field in JSON input.
|
|
218
|
+
Object.assign(manifest, { modelProfiles: {} });
|
|
252
219
|
const entrypoints = manifest.entrypoints ?? {};
|
|
253
220
|
if ("cursorExecutorUsage" in entrypoints) {
|
|
254
221
|
ctx.addIssue({
|
|
@@ -53,17 +53,18 @@ function resolveAcceptance(input) {
|
|
|
53
53
|
return [];
|
|
54
54
|
}
|
|
55
55
|
function resolveAllowedPaths(input) {
|
|
56
|
+
const existingAllowed = input.baseDraft?.constraints.allowedPaths?.length
|
|
57
|
+
? input.baseDraft.constraints.allowedPaths
|
|
58
|
+
: (input.existing?.allowedPaths ?? []);
|
|
59
|
+
const explicitAllowed = cleanStringList(input.flags.allowedPaths) ?? [];
|
|
60
|
+
// `--allowed-path` extends a managed boundary; it never replaces it.
|
|
56
61
|
const source = input.flags.allowedPaths !== undefined
|
|
57
|
-
?
|
|
58
|
-
:
|
|
59
|
-
|
|
60
|
-
: input.existing?.allowedPaths?.length
|
|
61
|
-
? [...input.existing.allowedPaths]
|
|
62
|
-
: [];
|
|
63
|
-
return source.map((raw) => {
|
|
62
|
+
? [...existingAllowed, ...explicitAllowed]
|
|
63
|
+
: [...existingAllowed];
|
|
64
|
+
return dedupeStable(source.map((raw) => {
|
|
64
65
|
const n = normalizePreparePath(raw);
|
|
65
66
|
return n.ok ? n.value : raw.trim();
|
|
66
|
-
});
|
|
67
|
+
}));
|
|
67
68
|
}
|
|
68
69
|
function resolveAllowedRoots(input) {
|
|
69
70
|
const roots = input.flags.allowedRoots ?? input.baseDraft?.constraints.allowedRoots ?? input.existing?.allowedRoots;
|
|
@@ -213,7 +214,7 @@ export function buildPrepareDraft(input) {
|
|
|
213
214
|
if (baseDraft?.sourceResolutions?.length) {
|
|
214
215
|
draft.sourceResolutions = [...baseDraft.sourceResolutions];
|
|
215
216
|
}
|
|
216
|
-
if (baseDraft?.hardConstraints?.length
|
|
217
|
+
if (baseDraft?.hardConstraints?.length) {
|
|
217
218
|
draft.hardConstraints = [...baseDraft.hardConstraints];
|
|
218
219
|
}
|
|
219
220
|
return {
|