ultracode-for-codex 0.3.3 → 0.3.4
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/README.md +17 -4
- package/ULTRACODE_INSTALL.md +15 -6
- package/dist/cli.js +56 -11
- package/dist/runtime/state-root.d.ts +3 -0
- package/dist/runtime/state-root.js +29 -0
- package/dist/runtime/workflow-runtime.d.ts +3 -0
- package/dist/runtime/workflow-runtime.js +622 -63
- package/docs/provenance-audit.md +3 -3
- package/docs/ultracode-p3b-resume-cache.md +27 -8
- package/package.json +1 -1
- package/settings.json +1 -1
- package/skills/ultracode-for-codex-cli/SKILL.md +10 -3
|
@@ -7,6 +7,7 @@ import { promisify } from 'node:util';
|
|
|
7
7
|
import { createContext, runInContext } from 'node:vm';
|
|
8
8
|
import { UltracodeRequestError, estimateTokens } from './types.js';
|
|
9
9
|
import { WORKFLOW_JOURNAL_GENESIS_AGENT_CALL_KEY, WORKFLOW_JOURNAL_WRITE_FAILED_REASON, WorkflowJournalError, WorkflowJournalWriter, computeWorkflowAgentCallKey, isWorkflowJournalError, normalizeJournalJsonValue, readWorkflowJournal, stableJson, workflowJournalPath, } from './workflow-journal.js';
|
|
10
|
+
import { defaultUltracodeStateRoot, defaultWorkflowStateDir } from './state-root.js';
|
|
10
11
|
const MAX_SCRIPT_BYTES = 64 * 1024;
|
|
11
12
|
const MAX_AGENT_CALLS = 1000;
|
|
12
13
|
const MAX_PARALLELISM = 16;
|
|
@@ -69,6 +70,7 @@ const WORKSPACE_CONTEXT_EXCLUDED_DIRS = new Set([
|
|
|
69
70
|
'node_modules',
|
|
70
71
|
'out',
|
|
71
72
|
]);
|
|
73
|
+
const EMPTY_WORKSPACE_PATH_EXCLUSIONS = new Set();
|
|
72
74
|
const WORKSPACE_CONTEXT_ALLOWED_EXTENSIONS = new Set([
|
|
73
75
|
'.cjs',
|
|
74
76
|
'.css',
|
|
@@ -178,21 +180,21 @@ const seedLenses = [
|
|
|
178
180
|
const scopeSchema = {
|
|
179
181
|
type: "object",
|
|
180
182
|
additionalProperties: false,
|
|
181
|
-
required: ["files", "summary", "lensDecisions", "lenses"],
|
|
183
|
+
required: ["files", "summary", "instructions", "lensDecisions", "lenses"],
|
|
182
184
|
properties: {
|
|
183
185
|
files: { type: "array", items: { type: "string", minLength: 1, maxLength: 240 } },
|
|
184
186
|
summary: { type: "string", minLength: 1 },
|
|
185
|
-
instructions: { type: "string" },
|
|
187
|
+
instructions: { type: ["string", "null"] },
|
|
186
188
|
lensDecisions: {
|
|
187
189
|
type: "array",
|
|
188
190
|
items: {
|
|
189
191
|
type: "object",
|
|
190
192
|
additionalProperties: false,
|
|
191
|
-
required: ["seedId", "action", "reasonCategory", "decisionRefs", "reason"],
|
|
193
|
+
required: ["seedId", "action", "selectedLensId", "reasonCategory", "decisionRefs", "reason"],
|
|
192
194
|
properties: {
|
|
193
195
|
seedId: { type: "string", minLength: 1 },
|
|
194
196
|
action: { type: "string", enum: ["select", "skip"] },
|
|
195
|
-
selectedLensId: { type: "string" },
|
|
197
|
+
selectedLensId: { type: ["string", "null"] },
|
|
196
198
|
reasonCategory: { type: "string", enum: ["matched_change", "prompt_risk", "no_evidence", "cap_limit", "redundant", "out_of_scope", "tiny_change"] },
|
|
197
199
|
decisionRefs: { type: "array", minItems: 1, items: { type: "string", minLength: 1 } },
|
|
198
200
|
reason: { type: "string", minLength: 1 }
|
|
@@ -225,14 +227,14 @@ const finderSchema = {
|
|
|
225
227
|
items: {
|
|
226
228
|
type: "object",
|
|
227
229
|
additionalProperties: false,
|
|
228
|
-
required: ["file", "summary", "failureScenario", "evidenceRefs"],
|
|
230
|
+
required: ["file", "line", "summary", "failureScenario", "evidenceRefs", "kind"],
|
|
229
231
|
properties: {
|
|
230
232
|
file: { type: "string", minLength: 1, maxLength: 240 },
|
|
231
|
-
line: { type: "integer", minimum: 1 },
|
|
233
|
+
line: { type: ["integer", "null"], minimum: 1 },
|
|
232
234
|
summary: { type: "string", minLength: 1 },
|
|
233
235
|
failureScenario: { type: "string", minLength: 1 },
|
|
234
236
|
evidenceRefs: { type: "array", minItems: 1, items: { type: "string", minLength: 1 } },
|
|
235
|
-
kind: { type: "string" }
|
|
237
|
+
kind: { type: ["string", "null"] }
|
|
236
238
|
}
|
|
237
239
|
}
|
|
238
240
|
}
|
|
@@ -241,12 +243,12 @@ const finderSchema = {
|
|
|
241
243
|
const verifierSchema = {
|
|
242
244
|
type: "object",
|
|
243
245
|
additionalProperties: false,
|
|
244
|
-
required: ["verdict", "evidence", "evidenceRefs"],
|
|
246
|
+
required: ["verdict", "evidence", "evidenceRefs", "severity"],
|
|
245
247
|
properties: {
|
|
246
248
|
verdict: { type: "string", enum: ["CONFIRMED", "PLAUSIBLE", "REFUTED"] },
|
|
247
249
|
evidence: { type: "string", minLength: 1 },
|
|
248
250
|
evidenceRefs: { type: "array", minItems: 1, items: { type: "string", minLength: 1 } },
|
|
249
|
-
severity: { type: "string", enum: ["P0", "P1", "P2", "P3"] }
|
|
251
|
+
severity: { type: ["string", "null"], enum: ["P0", "P1", "P2", "P3", null] }
|
|
250
252
|
}
|
|
251
253
|
};
|
|
252
254
|
const synthesisSchema = {
|
|
@@ -260,12 +262,12 @@ const synthesisSchema = {
|
|
|
260
262
|
items: {
|
|
261
263
|
type: "object",
|
|
262
264
|
additionalProperties: false,
|
|
263
|
-
required: ["index", "action", "reasonCategory", "reason"],
|
|
265
|
+
required: ["index", "action", "merge", "severity", "reasonCategory", "reason"],
|
|
264
266
|
properties: {
|
|
265
267
|
index: { type: "integer", minimum: 0 },
|
|
266
268
|
action: { type: "string", enum: ["report", "merge", "drop"] },
|
|
267
|
-
merge: { type: "array", minItems: 1, items: { type: "integer", minimum: 0 } },
|
|
268
|
-
severity: { type: "string", enum: ["P0", "P1", "P2", "P3"] },
|
|
269
|
+
merge: { type: ["array", "null"], minItems: 1, items: { type: "integer", minimum: 0 } },
|
|
270
|
+
severity: { type: ["string", "null"], enum: ["P0", "P1", "P2", "P3", null] },
|
|
269
271
|
reasonCategory: { type: "string", enum: ["material", "duplicate", "not_material", "report_cap", "unsupported_evidence", "superseded"] },
|
|
270
272
|
reason: { type: "string", minLength: 1 }
|
|
271
273
|
}
|
|
@@ -1085,14 +1087,14 @@ export class WorkflowTaskRegistry {
|
|
|
1085
1087
|
agentStallRetryLimit;
|
|
1086
1088
|
constructor(options) {
|
|
1087
1089
|
this.options = options;
|
|
1088
|
-
this.stateDir = options.stateDir ??
|
|
1090
|
+
this.stateDir = options.stateDir ?? defaultWorkflowStateDir(options.cwd ?? process.cwd());
|
|
1089
1091
|
this.agentStallRetryLimit = normalizeAgentStallRetryLimit(options.agentStallRetryLimit);
|
|
1090
1092
|
this.agentStallTimeoutMs = normalizeAgentStallTimeoutMs(options.agentStallTimeoutMs, options.requestTimeoutMs);
|
|
1091
1093
|
}
|
|
1092
1094
|
async launch(input) {
|
|
1093
1095
|
if (this.closed)
|
|
1094
1096
|
throw workflowInputError('Workflow runtime is closed.');
|
|
1095
|
-
const resumePlan = this.prepareResumePlan(input);
|
|
1097
|
+
const resumePlan = await this.prepareResumePlan(input);
|
|
1096
1098
|
let resolved = await this.resolveLaunchInput(resumePlan.launchInput);
|
|
1097
1099
|
const parsed = parseInlineWorkflowScript(resolved.script);
|
|
1098
1100
|
const scriptHash = workflowScriptHash(resolved.script);
|
|
@@ -1189,29 +1191,31 @@ export class WorkflowTaskRegistry {
|
|
|
1189
1191
|
scriptHash,
|
|
1190
1192
|
};
|
|
1191
1193
|
}
|
|
1192
|
-
|
|
1194
|
+
async validateResumeSource(resumeFromRunId) {
|
|
1195
|
+
const runId = normalizeResumeFromRunId(resumeFromRunId);
|
|
1196
|
+
const sourceTask = await this.workflowTaskByRunId(runId);
|
|
1197
|
+
if (!sourceTask)
|
|
1198
|
+
throw workflowInputError(`Unknown workflow run for resume: ${runId}`);
|
|
1199
|
+
if (sourceTask.status === 'running')
|
|
1200
|
+
throw workflowResumeRunningError(runId);
|
|
1201
|
+
await this.createResumeCache(sourceTask);
|
|
1202
|
+
}
|
|
1203
|
+
async prepareResumePlan(input) {
|
|
1193
1204
|
if (!Object.prototype.hasOwnProperty.call(input, 'resumeFromRunId')) {
|
|
1194
1205
|
return { launchInput: input };
|
|
1195
1206
|
}
|
|
1196
1207
|
const resumeFromRunId = normalizeResumeFromRunId(input.resumeFromRunId);
|
|
1197
|
-
const sourceTask = this.workflowTaskByRunId(resumeFromRunId);
|
|
1208
|
+
const sourceTask = await this.workflowTaskByRunId(resumeFromRunId);
|
|
1198
1209
|
if (!sourceTask)
|
|
1199
1210
|
throw workflowInputError(`Unknown workflow run for resume: ${resumeFromRunId}`);
|
|
1200
1211
|
if (sourceTask.status === 'running')
|
|
1201
1212
|
throw workflowResumeRunningError(resumeFromRunId);
|
|
1213
|
+
if (workflowLaunchHasSourceSelector(input)) {
|
|
1214
|
+
throw workflowInputError('resumeFromRunId cannot be combined with script, scriptPath, or name. Resume uses the original persisted workflow source.');
|
|
1215
|
+
}
|
|
1202
1216
|
const inheritedArgs = !Object.prototype.hasOwnProperty.call(input, 'args') && sourceTask.retryInput.args !== undefined
|
|
1203
1217
|
? { args: sourceTask.retryInput.args }
|
|
1204
1218
|
: {};
|
|
1205
|
-
if (workflowLaunchHasSourceSelector(input)) {
|
|
1206
|
-
return {
|
|
1207
|
-
sourceTask,
|
|
1208
|
-
launchInput: {
|
|
1209
|
-
...inheritedArgs,
|
|
1210
|
-
...input,
|
|
1211
|
-
resumeFromRunId,
|
|
1212
|
-
},
|
|
1213
|
-
};
|
|
1214
|
-
}
|
|
1215
1219
|
return {
|
|
1216
1220
|
sourceTask,
|
|
1217
1221
|
launchInput: {
|
|
@@ -1223,21 +1227,66 @@ export class WorkflowTaskRegistry {
|
|
|
1223
1227
|
},
|
|
1224
1228
|
};
|
|
1225
1229
|
}
|
|
1226
|
-
workflowTaskByRunId(runId) {
|
|
1230
|
+
async workflowTaskByRunId(runId) {
|
|
1227
1231
|
for (const task of this.tasks.values()) {
|
|
1228
1232
|
if (task.runId === runId)
|
|
1229
1233
|
return task;
|
|
1230
1234
|
}
|
|
1231
|
-
return
|
|
1235
|
+
return await this.durableWorkflowResumeSource(runId);
|
|
1232
1236
|
}
|
|
1233
|
-
async
|
|
1234
|
-
|
|
1237
|
+
async durableWorkflowResumeSource(runId) {
|
|
1238
|
+
const resultPath = join(this.stateDir, 'workflows', `${runId}.result.json`);
|
|
1239
|
+
let record;
|
|
1235
1240
|
try {
|
|
1236
|
-
|
|
1241
|
+
record = durableWorkflowResultRecordFromUnknown(JSON.parse(await readFile(resultPath, 'utf8')));
|
|
1237
1242
|
}
|
|
1238
1243
|
catch {
|
|
1239
|
-
|
|
1244
|
+
return undefined;
|
|
1240
1245
|
}
|
|
1246
|
+
if (!record || record.runId !== runId || !record.retryInput)
|
|
1247
|
+
return undefined;
|
|
1248
|
+
let scriptRecord;
|
|
1249
|
+
try {
|
|
1250
|
+
scriptRecord = await this.readRuntimeWorkflowScript(record.retryInput.scriptPath ?? '');
|
|
1251
|
+
}
|
|
1252
|
+
catch {
|
|
1253
|
+
return undefined;
|
|
1254
|
+
}
|
|
1255
|
+
const actualScriptHash = workflowScriptHash(scriptRecord.script);
|
|
1256
|
+
if (actualScriptHash !== record.scriptHash)
|
|
1257
|
+
return undefined;
|
|
1258
|
+
if (scriptRecord.metadata?.scriptHash !== record.scriptHash)
|
|
1259
|
+
return undefined;
|
|
1260
|
+
if (scriptRecord.metadata?.workflowName !== record.workflowName)
|
|
1261
|
+
return undefined;
|
|
1262
|
+
const transcriptDir = join(this.stateDir, 'subagents', 'workflows', runId);
|
|
1263
|
+
let completedJournal;
|
|
1264
|
+
try {
|
|
1265
|
+
completedJournal = await this.readCompletedResumeJournal({
|
|
1266
|
+
runId,
|
|
1267
|
+
transcriptDir,
|
|
1268
|
+
workflowName: record.workflowName,
|
|
1269
|
+
scriptHash: record.scriptHash,
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
catch {
|
|
1273
|
+
return undefined;
|
|
1274
|
+
}
|
|
1275
|
+
if (!durableScriptRecordMatchesJournal(scriptRecord, completedJournal.started))
|
|
1276
|
+
return undefined;
|
|
1277
|
+
if (!durableRetryInputArgsMatchJournal(record.retryInput, completedJournal.started.args))
|
|
1278
|
+
return undefined;
|
|
1279
|
+
return {
|
|
1280
|
+
runId,
|
|
1281
|
+
status: 'completed',
|
|
1282
|
+
transcriptDir,
|
|
1283
|
+
retryInput: durableRetryInputWithJournalArgs(record.retryInput, completedJournal.started.args),
|
|
1284
|
+
workflowName: record.workflowName,
|
|
1285
|
+
scriptHash: record.scriptHash,
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
async createResumeCache(sourceTask) {
|
|
1289
|
+
const { entries } = await this.readCompletedResumeJournal(sourceTask);
|
|
1241
1290
|
const completedByCallKey = new Map();
|
|
1242
1291
|
for (const entry of entries) {
|
|
1243
1292
|
if (entry.kind === 'workflow.agent.completed')
|
|
@@ -1263,6 +1312,30 @@ export class WorkflowTaskRegistry {
|
|
|
1263
1312
|
prefixOpen: true,
|
|
1264
1313
|
};
|
|
1265
1314
|
}
|
|
1315
|
+
async readCompletedResumeJournal(sourceTask) {
|
|
1316
|
+
let journal;
|
|
1317
|
+
try {
|
|
1318
|
+
journal = await readWorkflowJournal(workflowJournalPath(sourceTask.transcriptDir));
|
|
1319
|
+
}
|
|
1320
|
+
catch {
|
|
1321
|
+
throw workflowInputError(`Workflow run cannot be used as a resume source: ${sourceTask.runId}`);
|
|
1322
|
+
}
|
|
1323
|
+
const entries = journal.entries;
|
|
1324
|
+
const started = entries[0];
|
|
1325
|
+
const terminal = entries.at(-1);
|
|
1326
|
+
if (journal.truncatedTail
|
|
1327
|
+
|| !started
|
|
1328
|
+
|| started.kind !== 'workflow.run.started'
|
|
1329
|
+
|| started.runId !== sourceTask.runId
|
|
1330
|
+
|| (sourceTask.scriptHash && started.scriptHash !== sourceTask.scriptHash)
|
|
1331
|
+
|| (sourceTask.workflowName && started.workflowName !== sourceTask.workflowName)
|
|
1332
|
+
|| !terminal
|
|
1333
|
+
|| terminal.kind !== 'workflow.run.completed'
|
|
1334
|
+
|| terminal.runId !== sourceTask.runId) {
|
|
1335
|
+
throw workflowInputError(`Workflow run cannot be used as a resume source: ${sourceTask.runId}`);
|
|
1336
|
+
}
|
|
1337
|
+
return { entries, started };
|
|
1338
|
+
}
|
|
1266
1339
|
async resolveLaunchInput(input) {
|
|
1267
1340
|
const normalized = normalizeLaunchInput(input);
|
|
1268
1341
|
if (normalized.scriptPath) {
|
|
@@ -1732,6 +1805,7 @@ export class WorkflowTaskRegistry {
|
|
|
1732
1805
|
workflowSource: task.workflowSource,
|
|
1733
1806
|
...(task.workflowSourcePath ? { workflowSourcePath: task.workflowSourcePath } : {}),
|
|
1734
1807
|
scriptHash: task.scriptHash,
|
|
1808
|
+
retryInput: durableWorkflowRetryInput(task.retryInput),
|
|
1735
1809
|
result: journalResult,
|
|
1736
1810
|
}, null, 2)}\n`);
|
|
1737
1811
|
const completedSnapshot = await this.completeTask(ctx, journalResult, {
|
|
@@ -2494,6 +2568,9 @@ function normalizeResumeFromRunId(value) {
|
|
|
2494
2568
|
const runId = value.trim();
|
|
2495
2569
|
if (!runId)
|
|
2496
2570
|
throw workflowInputError('resumeFromRunId must be a non-empty workflow runId string.');
|
|
2571
|
+
if (!/^run_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(runId)) {
|
|
2572
|
+
throw workflowInputError('resumeFromRunId must be a workflow runId in run_<uuid> format.');
|
|
2573
|
+
}
|
|
2497
2574
|
return runId;
|
|
2498
2575
|
}
|
|
2499
2576
|
function normalizeAgentIsolation(value) {
|
|
@@ -2537,13 +2614,16 @@ function takeResumeCacheHit(cache, agentCallKey) {
|
|
|
2537
2614
|
return keyed;
|
|
2538
2615
|
}
|
|
2539
2616
|
async function gitOutput(cwd, args) {
|
|
2617
|
+
return (await gitOutputRaw(cwd, args)).trim();
|
|
2618
|
+
}
|
|
2619
|
+
async function gitOutputRaw(cwd, args) {
|
|
2540
2620
|
try {
|
|
2541
2621
|
const result = await execFileAsync('git', args, {
|
|
2542
2622
|
cwd,
|
|
2543
2623
|
encoding: 'utf8',
|
|
2544
2624
|
maxBuffer: 1024 * 1024,
|
|
2545
2625
|
});
|
|
2546
|
-
return result.stdout
|
|
2626
|
+
return result.stdout;
|
|
2547
2627
|
}
|
|
2548
2628
|
catch (err) {
|
|
2549
2629
|
const record = err;
|
|
@@ -2554,22 +2634,39 @@ async function gitOutput(cwd, args) {
|
|
|
2554
2634
|
}
|
|
2555
2635
|
async function buildWorkspaceContext(cwd, options) {
|
|
2556
2636
|
const root = await workspaceContextRoot(cwd);
|
|
2557
|
-
const
|
|
2637
|
+
const runtimeStateExcludedPaths = workspaceRuntimeStateExcludedPaths(root);
|
|
2638
|
+
const statusUnavailableEvidence = [];
|
|
2639
|
+
let gitStatusRaw = '';
|
|
2640
|
+
try {
|
|
2641
|
+
gitStatusRaw = await gitOutputRaw(root, ['status', '--short', '--untracked-files=all', '--', '.']);
|
|
2642
|
+
}
|
|
2643
|
+
catch (err) {
|
|
2644
|
+
statusUnavailableEvidence.push(`unavailable:git-status:${gitFailureToken(err)}`);
|
|
2645
|
+
}
|
|
2646
|
+
const gitStatus = statusUnavailableEvidence.length
|
|
2647
|
+
? `(unavailable: ${statusUnavailableEvidence[0]})`
|
|
2648
|
+
: formatGitStatusDisplay(gitStatusRaw, runtimeStateExcludedPaths);
|
|
2649
|
+
const gitStatusPaths = await gitOutputRaw(root, ['status', '--porcelain=v1', '-z', '--untracked-files=all', '--', '.']).catch((err) => {
|
|
2650
|
+
statusUnavailableEvidence.push(`unavailable:git-status-raw:${gitFailureToken(err)}`);
|
|
2651
|
+
return gitStatusRaw;
|
|
2652
|
+
});
|
|
2653
|
+
const gitStatusPathParse = parseGitStatusPaths(gitStatusPaths, runtimeStateExcludedPaths);
|
|
2654
|
+
const excludedWorkspacePaths = new Set(gitStatusPathParse.excludedPaths.map(workspacePathKey));
|
|
2558
2655
|
const reviewEvidence = options.includeDiff
|
|
2559
|
-
? await buildReviewEvidenceContext(root,
|
|
2656
|
+
? await buildReviewEvidenceContext(root, gitStatusPaths, options, statusUnavailableEvidence, runtimeStateExcludedPaths)
|
|
2560
2657
|
: undefined;
|
|
2561
2658
|
const explicitPaths = [
|
|
2562
2659
|
...options.files,
|
|
2563
2660
|
...extractMentionedWorkspacePaths(options.query ?? ''),
|
|
2564
2661
|
];
|
|
2565
|
-
const changedPaths =
|
|
2662
|
+
const changedPaths = gitStatusPathParse.paths.filter((path) => shouldIncludeWorkspaceContextPath(path, runtimeStateExcludedPaths));
|
|
2566
2663
|
const listedPaths = await listWorkspaceContextCandidates(root);
|
|
2567
2664
|
const candidates = uniqueStrings([
|
|
2568
2665
|
...explicitPaths,
|
|
2569
2666
|
...changedPaths,
|
|
2570
2667
|
...WORKSPACE_CONTEXT_PRIORITY_FILES,
|
|
2571
2668
|
...listedPaths,
|
|
2572
|
-
]).filter(shouldIncludeWorkspaceContextPath);
|
|
2669
|
+
]).filter((path) => shouldIncludeWorkspaceContextPath(path, runtimeStateExcludedPaths) && !excludedWorkspacePaths.has(workspacePathKey(path)));
|
|
2573
2670
|
const fileBlocks = [];
|
|
2574
2671
|
let usedBytes = 0;
|
|
2575
2672
|
for (const candidate of candidates) {
|
|
@@ -2606,30 +2703,36 @@ async function buildWorkspaceContext(cwd, options) {
|
|
|
2606
2703
|
] : []),
|
|
2607
2704
|
'',
|
|
2608
2705
|
'### Git Status',
|
|
2609
|
-
gitStatus
|
|
2706
|
+
gitStatus,
|
|
2610
2707
|
'',
|
|
2611
2708
|
'### Included Files',
|
|
2612
2709
|
fileBlocks.length ? fileBlocks.join('\n\n') : '(no readable text files selected)',
|
|
2613
2710
|
].join('\n');
|
|
2614
2711
|
}
|
|
2615
|
-
async function buildReviewEvidenceContext(root, gitStatus, options) {
|
|
2616
|
-
const unavailableEvidence = [];
|
|
2617
|
-
const
|
|
2712
|
+
async function buildReviewEvidenceContext(root, gitStatus, options, initialUnavailableEvidence = [], runtimeStateExcludedPaths = EMPTY_WORKSPACE_PATH_EXCLUSIONS) {
|
|
2713
|
+
const unavailableEvidence = [...initialUnavailableEvidence];
|
|
2714
|
+
const gitStatusPaths = parseGitStatusPaths(gitStatus, runtimeStateExcludedPaths);
|
|
2715
|
+
const changedPaths = gitStatusPaths.paths.filter((path) => shouldIncludeWorkspaceContextPath(path, runtimeStateExcludedPaths));
|
|
2716
|
+
const excludedDiffPaths = new Set([
|
|
2717
|
+
...gitStatusPaths.excludedPaths.map(workspacePathKey),
|
|
2718
|
+
...runtimeStateExcludedPaths,
|
|
2719
|
+
]);
|
|
2720
|
+
unavailableEvidence.push(...gitStatusPaths.unavailableEvidence);
|
|
2618
2721
|
const head = await gitOutput(root, ['rev-parse', '--verify', 'HEAD']).catch((err) => {
|
|
2619
|
-
unavailableEvidence.push(
|
|
2722
|
+
unavailableEvidence.push(unavailableGitEvidence('git-head', err));
|
|
2620
2723
|
return 'unavailable';
|
|
2621
2724
|
});
|
|
2622
|
-
const unstaged = await boundedGitOutput(root, [
|
|
2725
|
+
const unstaged = filterWorkspaceContextDiff(await boundedGitOutput(root, [
|
|
2623
2726
|
'diff',
|
|
2624
2727
|
'--no-ext-diff',
|
|
2625
2728
|
'--patch',
|
|
2626
2729
|
'--find-renames',
|
|
2627
2730
|
'--',
|
|
2628
2731
|
], options.maxDiffBytes).catch((err) => {
|
|
2629
|
-
unavailableEvidence.push(
|
|
2732
|
+
unavailableEvidence.push(unavailableGitEvidence('diff-unstaged', err));
|
|
2630
2733
|
return { text: '', truncated: false };
|
|
2631
|
-
});
|
|
2632
|
-
const staged = await boundedGitOutput(root, [
|
|
2734
|
+
}), excludedDiffPaths, runtimeStateExcludedPaths);
|
|
2735
|
+
const staged = filterWorkspaceContextDiff(await boundedGitOutput(root, [
|
|
2633
2736
|
'diff',
|
|
2634
2737
|
'--cached',
|
|
2635
2738
|
'--no-ext-diff',
|
|
@@ -2637,19 +2740,19 @@ async function buildReviewEvidenceContext(root, gitStatus, options) {
|
|
|
2637
2740
|
'--find-renames',
|
|
2638
2741
|
'--',
|
|
2639
2742
|
], options.maxDiffBytes).catch((err) => {
|
|
2640
|
-
unavailableEvidence.push(
|
|
2743
|
+
unavailableEvidence.push(unavailableGitEvidence('diff-staged', err));
|
|
2641
2744
|
return { text: '', truncated: false };
|
|
2642
|
-
});
|
|
2745
|
+
}), excludedDiffPaths, runtimeStateExcludedPaths);
|
|
2643
2746
|
let committed = { text: '', truncated: false };
|
|
2644
2747
|
let acceptedDiffBaseRef = '';
|
|
2645
2748
|
if (options.diffBaseRef) {
|
|
2646
2749
|
const baseCommit = await gitOutput(root, ['rev-parse', '--verify', `${options.diffBaseRef}^{commit}`]).catch((err) => {
|
|
2647
|
-
unavailableEvidence.push(
|
|
2750
|
+
unavailableEvidence.push(unavailableGitEvidence('diff-base', err, options.diffBaseRef));
|
|
2648
2751
|
return '';
|
|
2649
2752
|
});
|
|
2650
2753
|
if (baseCommit) {
|
|
2651
2754
|
acceptedDiffBaseRef = options.diffBaseRef;
|
|
2652
|
-
committed = await boundedGitOutput(root, [
|
|
2755
|
+
committed = filterWorkspaceContextDiff(await boundedGitOutput(root, [
|
|
2653
2756
|
'diff',
|
|
2654
2757
|
'--no-ext-diff',
|
|
2655
2758
|
'--patch',
|
|
@@ -2657,9 +2760,9 @@ async function buildReviewEvidenceContext(root, gitStatus, options) {
|
|
|
2657
2760
|
`${baseCommit}..HEAD`,
|
|
2658
2761
|
'--',
|
|
2659
2762
|
], options.maxDiffBytes).catch((err) => {
|
|
2660
|
-
unavailableEvidence.push(
|
|
2763
|
+
unavailableEvidence.push(unavailableGitEvidence('diff-committed', err, options.diffBaseRef));
|
|
2661
2764
|
return { text: '', truncated: false };
|
|
2662
|
-
});
|
|
2765
|
+
}), excludedDiffPaths, runtimeStateExcludedPaths);
|
|
2663
2766
|
}
|
|
2664
2767
|
}
|
|
2665
2768
|
const diffEvidence = [
|
|
@@ -2669,7 +2772,7 @@ async function buildReviewEvidenceContext(root, gitStatus, options) {
|
|
|
2669
2772
|
];
|
|
2670
2773
|
const allowedEvidenceRefs = uniqueStrings([
|
|
2671
2774
|
...changedPaths.map((path) => `file:${path}`),
|
|
2672
|
-
...diffEvidence.flatMap((entry) => diffEvidenceRefs(entry.kind, entry.value.text)),
|
|
2775
|
+
...diffEvidence.flatMap((entry) => diffEvidenceRefs(entry.kind, entry.value.text, runtimeStateExcludedPaths)),
|
|
2673
2776
|
]);
|
|
2674
2777
|
const allowedEvidenceIndexDigest = fullHash(allowedEvidenceRefs.join('\n'));
|
|
2675
2778
|
const sourceSnapshotId = `git:${head}:${fullHash([
|
|
@@ -2690,6 +2793,7 @@ async function buildReviewEvidenceContext(root, gitStatus, options) {
|
|
|
2690
2793
|
acceptedDiffBaseRef,
|
|
2691
2794
|
truncation,
|
|
2692
2795
|
allowedEvidenceRefs,
|
|
2796
|
+
unavailableEvidence,
|
|
2693
2797
|
}));
|
|
2694
2798
|
const sections = [
|
|
2695
2799
|
`sourceSnapshotId: ${sourceSnapshotId}`,
|
|
@@ -2727,26 +2831,98 @@ async function boundedGitOutput(root, args, maxBytes) {
|
|
|
2727
2831
|
truncated: true,
|
|
2728
2832
|
};
|
|
2729
2833
|
}
|
|
2730
|
-
function
|
|
2834
|
+
function filterWorkspaceContextDiff(value, excludedPaths, runtimeStateExcludedPaths) {
|
|
2835
|
+
if (!value.text)
|
|
2836
|
+
return value;
|
|
2837
|
+
return {
|
|
2838
|
+
...value,
|
|
2839
|
+
text: filterWorkspaceContextDiffText(value.text, excludedPaths, runtimeStateExcludedPaths),
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
function filterWorkspaceContextDiffText(text, excludedPaths, runtimeStateExcludedPaths) {
|
|
2843
|
+
const kept = [];
|
|
2844
|
+
let block = [];
|
|
2845
|
+
let includeBlock = true;
|
|
2846
|
+
const flush = () => {
|
|
2847
|
+
if (includeBlock && block.length > 0)
|
|
2848
|
+
kept.push(block.join('\n'));
|
|
2849
|
+
block = [];
|
|
2850
|
+
};
|
|
2851
|
+
for (const line of text.split(/\r?\n/)) {
|
|
2852
|
+
if (line.startsWith('diff --git ')) {
|
|
2853
|
+
flush();
|
|
2854
|
+
const header = parseGitDiffHeader(line);
|
|
2855
|
+
includeBlock = header
|
|
2856
|
+
? workspaceContextDiffPathAllowed(header.oldPath, excludedPaths, runtimeStateExcludedPaths)
|
|
2857
|
+
&& workspaceContextDiffPathAllowed(header.newPath, excludedPaths, runtimeStateExcludedPaths)
|
|
2858
|
+
: false;
|
|
2859
|
+
}
|
|
2860
|
+
block.push(line);
|
|
2861
|
+
}
|
|
2862
|
+
flush();
|
|
2863
|
+
return kept.join('\n');
|
|
2864
|
+
}
|
|
2865
|
+
function workspaceContextDiffPathAllowed(path, excludedPaths, runtimeStateExcludedPaths) {
|
|
2866
|
+
if (!path || path === '/dev/null')
|
|
2867
|
+
return true;
|
|
2868
|
+
const key = workspacePathKey(path);
|
|
2869
|
+
return shouldIncludeWorkspaceContextPath(key, runtimeStateExcludedPaths) && !workspacePathExcludedBySet(key, excludedPaths);
|
|
2870
|
+
}
|
|
2871
|
+
function diffEvidenceRefs(kind, diff, runtimeStateExcludedPaths) {
|
|
2731
2872
|
const refs = [];
|
|
2732
2873
|
let currentPath = '';
|
|
2733
2874
|
let hunkIndex = 0;
|
|
2734
2875
|
for (const line of diff.split(/\r?\n/)) {
|
|
2735
|
-
const header =
|
|
2876
|
+
const header = parseGitDiffHeader(line);
|
|
2736
2877
|
if (header) {
|
|
2737
|
-
currentPath = header
|
|
2878
|
+
currentPath = header.newPath || header.oldPath;
|
|
2738
2879
|
hunkIndex = 0;
|
|
2739
|
-
if (currentPath && currentPath !== '/dev/null')
|
|
2880
|
+
if (currentPath && currentPath !== '/dev/null' && shouldIncludeWorkspaceContextPath(currentPath, runtimeStateExcludedPaths))
|
|
2740
2881
|
refs.push(`diff:${kind}:${currentPath}`);
|
|
2741
2882
|
continue;
|
|
2742
2883
|
}
|
|
2743
|
-
if (currentPath && line.startsWith('@@')) {
|
|
2884
|
+
if (currentPath && shouldIncludeWorkspaceContextPath(currentPath, runtimeStateExcludedPaths) && line.startsWith('@@')) {
|
|
2744
2885
|
hunkIndex += 1;
|
|
2745
2886
|
refs.push(`hunk:${kind}:${currentPath}:${hunkIndex}`);
|
|
2746
2887
|
}
|
|
2747
2888
|
}
|
|
2748
2889
|
return refs;
|
|
2749
2890
|
}
|
|
2891
|
+
function parseGitDiffHeader(line) {
|
|
2892
|
+
if (!line.startsWith('diff --git '))
|
|
2893
|
+
return undefined;
|
|
2894
|
+
const first = readGitDiffHeaderToken(line.slice('diff --git '.length));
|
|
2895
|
+
if (!first)
|
|
2896
|
+
return undefined;
|
|
2897
|
+
const second = readGitDiffHeaderToken(first.rest.trimStart());
|
|
2898
|
+
if (!second || second.rest.trim())
|
|
2899
|
+
return undefined;
|
|
2900
|
+
const oldPath = gitDiffHeaderTokenPath(first.token, 'a/');
|
|
2901
|
+
const newPath = gitDiffHeaderTokenPath(second.token, 'b/');
|
|
2902
|
+
if (oldPath === undefined || newPath === undefined)
|
|
2903
|
+
return undefined;
|
|
2904
|
+
return { oldPath, newPath };
|
|
2905
|
+
}
|
|
2906
|
+
function readGitDiffHeaderToken(value) {
|
|
2907
|
+
if (!value)
|
|
2908
|
+
return undefined;
|
|
2909
|
+
if (value.startsWith('"')) {
|
|
2910
|
+
const end = gitQuotedPathEnd(value);
|
|
2911
|
+
if (end === -1)
|
|
2912
|
+
return undefined;
|
|
2913
|
+
return { token: value.slice(0, end), rest: value.slice(end) };
|
|
2914
|
+
}
|
|
2915
|
+
const separator = value.indexOf(' ');
|
|
2916
|
+
if (separator === -1)
|
|
2917
|
+
return { token: value, rest: '' };
|
|
2918
|
+
return { token: value.slice(0, separator), rest: value.slice(separator + 1) };
|
|
2919
|
+
}
|
|
2920
|
+
function gitDiffHeaderTokenPath(token, prefix) {
|
|
2921
|
+
const path = normalizeGitStatusPath(token);
|
|
2922
|
+
if (!path.startsWith(prefix))
|
|
2923
|
+
return undefined;
|
|
2924
|
+
return path.slice(prefix.length);
|
|
2925
|
+
}
|
|
2750
2926
|
function normalizeWorkspaceContextOptions(value) {
|
|
2751
2927
|
const options = asRecord(value) ?? {};
|
|
2752
2928
|
const query = typeof options.query === 'string' ? options.query : undefined;
|
|
@@ -2780,6 +2956,14 @@ async function workspaceContextRoot(cwd) {
|
|
|
2780
2956
|
return await realpath(cwd).catch(() => resolve(cwd));
|
|
2781
2957
|
}
|
|
2782
2958
|
}
|
|
2959
|
+
function workspaceRuntimeStateExcludedPaths(root) {
|
|
2960
|
+
const stateRoot = resolve(defaultUltracodeStateRoot());
|
|
2961
|
+
const workspaceRoot = resolve(root);
|
|
2962
|
+
if (!pathInsideOrEqual(workspaceRoot, stateRoot))
|
|
2963
|
+
return EMPTY_WORKSPACE_PATH_EXCLUSIONS;
|
|
2964
|
+
const relativeStateRoot = workspacePathKey(relative(workspaceRoot, stateRoot));
|
|
2965
|
+
return new Set([relativeStateRoot || '.']);
|
|
2966
|
+
}
|
|
2783
2967
|
async function listWorkspaceContextCandidates(root) {
|
|
2784
2968
|
try {
|
|
2785
2969
|
return splitLines(await gitOutput(root, ['ls-files', '--cached', '--others', '--exclude-standard']));
|
|
@@ -2826,15 +3010,311 @@ function extractMentionedWorkspacePaths(query) {
|
|
|
2826
3010
|
return [...out];
|
|
2827
3011
|
}
|
|
2828
3012
|
function pathsFromGitStatus(status) {
|
|
3013
|
+
return parseGitStatusPaths(status).paths;
|
|
3014
|
+
}
|
|
3015
|
+
function parseGitStatusPaths(status, runtimeStateExcludedPaths = EMPTY_WORKSPACE_PATH_EXCLUSIONS) {
|
|
3016
|
+
if (status.includes('\0'))
|
|
3017
|
+
return parseGitStatusPathsZ(status, runtimeStateExcludedPaths);
|
|
2829
3018
|
const paths = [];
|
|
3019
|
+
const excludedPaths = [];
|
|
3020
|
+
const unavailableEvidence = [];
|
|
3021
|
+
let entryIndex = 0;
|
|
2830
3022
|
for (const line of status.split(/\r?\n/).filter(Boolean)) {
|
|
2831
|
-
|
|
3023
|
+
entryIndex += 1;
|
|
3024
|
+
const match = /^([ MADRCUT?!]{2}) ([\s\S]+)$/.exec(line);
|
|
3025
|
+
if (!match) {
|
|
3026
|
+
unavailableEvidence.push(`unavailable:git-status-path:${entryIndex}:unparseable`);
|
|
3027
|
+
continue;
|
|
3028
|
+
}
|
|
3029
|
+
const statusCode = match[1];
|
|
3030
|
+
const rawPath = match[2];
|
|
2832
3031
|
if (!rawPath)
|
|
2833
3032
|
continue;
|
|
2834
|
-
const
|
|
2835
|
-
|
|
3033
|
+
const renameOrCopy = /[RC]/.test(statusCode);
|
|
3034
|
+
const renameParts = renameOrCopy ? splitGitStatusRename(rawPath) : undefined;
|
|
3035
|
+
if (renameOrCopy && !renameParts) {
|
|
3036
|
+
unavailableEvidence.push(`unavailable:git-status-path:${entryIndex}:unsafe-path`);
|
|
3037
|
+
continue;
|
|
3038
|
+
}
|
|
3039
|
+
if (renameParts) {
|
|
3040
|
+
const sourcePath = normalizeGitStatusPath(renameParts.source);
|
|
3041
|
+
if (!isWorkspaceEvidencePathSafe(sourcePath)) {
|
|
3042
|
+
const targetPath = normalizeGitStatusPath(renameParts.target);
|
|
3043
|
+
if (targetPath)
|
|
3044
|
+
excludedPaths.push(targetPath);
|
|
3045
|
+
unavailableEvidence.push(`unavailable:git-status-path:${entryIndex}:unsafe-source`);
|
|
3046
|
+
continue;
|
|
3047
|
+
}
|
|
3048
|
+
else if (!shouldExposeWorkspaceStatusPath(sourcePath, runtimeStateExcludedPaths)) {
|
|
3049
|
+
const targetPath = normalizeGitStatusPath(renameParts.target);
|
|
3050
|
+
if (targetPath)
|
|
3051
|
+
excludedPaths.push(targetPath);
|
|
3052
|
+
unavailableEvidence.push(`unavailable:git-status-path:${entryIndex}:excluded-source`);
|
|
3053
|
+
continue;
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
const selectedPath = renameParts ? renameParts.target : rawPath;
|
|
3057
|
+
const path = normalizeGitStatusPath(selectedPath);
|
|
3058
|
+
if (isWorkspaceEvidencePathSafe(path) && shouldExposeWorkspaceStatusPath(path, runtimeStateExcludedPaths))
|
|
3059
|
+
paths.push(path);
|
|
3060
|
+
else if (isWorkspaceEvidencePathSafe(path))
|
|
3061
|
+
excludedPaths.push(path);
|
|
3062
|
+
else
|
|
3063
|
+
unavailableEvidence.push(`unavailable:git-status-path:${entryIndex}:${renameParts ? 'unsafe-target' : 'unsafe-path'}`);
|
|
3064
|
+
}
|
|
3065
|
+
return { paths, excludedPaths, unavailableEvidence };
|
|
3066
|
+
}
|
|
3067
|
+
function parseGitStatusPathsZ(status, runtimeStateExcludedPaths = EMPTY_WORKSPACE_PATH_EXCLUSIONS) {
|
|
3068
|
+
const paths = [];
|
|
3069
|
+
const excludedPaths = [];
|
|
3070
|
+
const unavailableEvidence = [];
|
|
3071
|
+
const entries = status.split('\0').filter((entry) => entry !== '');
|
|
3072
|
+
let entryIndex = 0;
|
|
3073
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
3074
|
+
entryIndex += 1;
|
|
3075
|
+
const entry = entries[index] ?? '';
|
|
3076
|
+
const match = /^([ MADRCUT?!]{2}) ([\s\S]+)$/.exec(entry);
|
|
3077
|
+
const statusCode = match?.[1] ?? '';
|
|
3078
|
+
const path = match?.[2] ?? '';
|
|
3079
|
+
const renameOrCopy = /[RC]/.test(statusCode);
|
|
3080
|
+
let excludedBySource = false;
|
|
3081
|
+
if (renameOrCopy) {
|
|
3082
|
+
const sourcePath = entries[index + 1];
|
|
3083
|
+
if (sourcePath === undefined)
|
|
3084
|
+
unavailableEvidence.push(`unavailable:git-status-path:${entryIndex}:missing-source`);
|
|
3085
|
+
else if (!isWorkspaceEvidencePathSafe(sourcePath)) {
|
|
3086
|
+
unavailableEvidence.push(`unavailable:git-status-path:${entryIndex}:unsafe-source`);
|
|
3087
|
+
excludedBySource = true;
|
|
3088
|
+
}
|
|
3089
|
+
else if (!shouldExposeWorkspaceStatusPath(sourcePath, runtimeStateExcludedPaths)) {
|
|
3090
|
+
unavailableEvidence.push(`unavailable:git-status-path:${entryIndex}:excluded-source`);
|
|
3091
|
+
excludedBySource = true;
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
3094
|
+
if (isWorkspaceEvidencePathSafe(path) && shouldExposeWorkspaceStatusPath(path, runtimeStateExcludedPaths) && !excludedBySource)
|
|
3095
|
+
paths.push(path);
|
|
3096
|
+
else if (isWorkspaceEvidencePathSafe(path))
|
|
3097
|
+
excludedPaths.push(path);
|
|
3098
|
+
else
|
|
3099
|
+
unavailableEvidence.push(`unavailable:git-status-path:${entryIndex}:${renameOrCopy ? 'unsafe-target' : 'unsafe-path'}`);
|
|
3100
|
+
if (renameOrCopy) {
|
|
3101
|
+
index += 1;
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
return { paths, excludedPaths, unavailableEvidence };
|
|
3105
|
+
}
|
|
3106
|
+
function formatGitStatusDisplay(status, runtimeStateExcludedPaths = EMPTY_WORKSPACE_PATH_EXCLUSIONS) {
|
|
3107
|
+
if (!status.trim())
|
|
3108
|
+
return '(clean or unavailable)';
|
|
3109
|
+
if (status.includes('\0'))
|
|
3110
|
+
return formatGitStatusZDisplay(status, runtimeStateExcludedPaths);
|
|
3111
|
+
const lines = [];
|
|
3112
|
+
let entryIndex = 0;
|
|
3113
|
+
for (const line of status.split(/\r?\n/).filter(Boolean)) {
|
|
3114
|
+
entryIndex += 1;
|
|
3115
|
+
const match = /^([ MADRCUT?!]{2}) ([\s\S]+)$/.exec(line);
|
|
3116
|
+
if (!match) {
|
|
3117
|
+
lines.push(`${entryIndex}: <unparseable status omitted>`);
|
|
3118
|
+
continue;
|
|
3119
|
+
}
|
|
3120
|
+
const statusCode = match[1];
|
|
3121
|
+
const rawPath = match[2];
|
|
3122
|
+
const renameOrCopy = /[RC]/.test(statusCode);
|
|
3123
|
+
if (renameOrCopy) {
|
|
3124
|
+
const renameParts = splitGitStatusRename(rawPath);
|
|
3125
|
+
if (!renameParts) {
|
|
3126
|
+
lines.push(`${statusCode} <unsafe rename omitted>`);
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
3129
|
+
const sourcePath = normalizeGitStatusPath(renameParts.source);
|
|
3130
|
+
const targetPath = normalizeGitStatusPath(renameParts.target);
|
|
3131
|
+
if ((isWorkspaceEvidencePathSafe(sourcePath) && !shouldExposeWorkspaceStatusPath(sourcePath, runtimeStateExcludedPaths))
|
|
3132
|
+
|| (isWorkspaceEvidencePathSafe(targetPath) && !shouldExposeWorkspaceStatusPath(targetPath, runtimeStateExcludedPaths))) {
|
|
3133
|
+
lines.push(`${statusCode} <excluded rename omitted>`);
|
|
3134
|
+
continue;
|
|
3135
|
+
}
|
|
3136
|
+
lines.push(`${statusCode} ${formatGitStatusPathForDisplay(sourcePath, 'source', runtimeStateExcludedPaths)} -> ${formatGitStatusPathForDisplay(targetPath, 'target', runtimeStateExcludedPaths)}`);
|
|
3137
|
+
continue;
|
|
3138
|
+
}
|
|
3139
|
+
const path = normalizeGitStatusPath(rawPath);
|
|
3140
|
+
lines.push(`${statusCode} ${formatGitStatusPathForDisplay(path, 'path', runtimeStateExcludedPaths)}`);
|
|
3141
|
+
}
|
|
3142
|
+
return lines.length ? lines.join('\n') : '(clean or unavailable)';
|
|
3143
|
+
}
|
|
3144
|
+
function formatGitStatusZDisplay(status, runtimeStateExcludedPaths = EMPTY_WORKSPACE_PATH_EXCLUSIONS) {
|
|
3145
|
+
const lines = [];
|
|
3146
|
+
const entries = status.split('\0').filter((entry) => entry !== '');
|
|
3147
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
3148
|
+
const entry = entries[index] ?? '';
|
|
3149
|
+
const match = /^([ MADRCUT?!]{2}) ([\s\S]+)$/.exec(entry);
|
|
3150
|
+
if (!match) {
|
|
3151
|
+
lines.push(`${index + 1}: <unparseable status omitted>`);
|
|
3152
|
+
continue;
|
|
3153
|
+
}
|
|
3154
|
+
const statusCode = match[1];
|
|
3155
|
+
const path = match[2];
|
|
3156
|
+
const renameOrCopy = /[RC]/.test(statusCode);
|
|
3157
|
+
if (renameOrCopy) {
|
|
3158
|
+
const sourcePath = entries[index + 1] ?? '';
|
|
3159
|
+
if ((isWorkspaceEvidencePathSafe(sourcePath) && !shouldExposeWorkspaceStatusPath(sourcePath, runtimeStateExcludedPaths))
|
|
3160
|
+
|| (isWorkspaceEvidencePathSafe(path) && !shouldExposeWorkspaceStatusPath(path, runtimeStateExcludedPaths))) {
|
|
3161
|
+
lines.push(`${statusCode} <excluded rename omitted>`);
|
|
3162
|
+
index += 1;
|
|
3163
|
+
continue;
|
|
3164
|
+
}
|
|
3165
|
+
lines.push(`${statusCode} ${formatGitStatusPathForDisplay(sourcePath, 'source', runtimeStateExcludedPaths)} -> ${formatGitStatusPathForDisplay(path, 'target', runtimeStateExcludedPaths)}`);
|
|
3166
|
+
index += 1;
|
|
3167
|
+
continue;
|
|
3168
|
+
}
|
|
3169
|
+
lines.push(`${statusCode} ${formatGitStatusPathForDisplay(path, 'path', runtimeStateExcludedPaths)}`);
|
|
3170
|
+
}
|
|
3171
|
+
return lines.length ? lines.join('\n') : '(clean or unavailable)';
|
|
3172
|
+
}
|
|
3173
|
+
function formatGitStatusPathForDisplay(path, label, runtimeStateExcludedPaths = EMPTY_WORKSPACE_PATH_EXCLUSIONS) {
|
|
3174
|
+
if (!isWorkspaceEvidencePathSafe(path))
|
|
3175
|
+
return `<unsafe ${label} omitted>`;
|
|
3176
|
+
if (!shouldExposeWorkspaceStatusPath(path, runtimeStateExcludedPaths))
|
|
3177
|
+
return `<excluded ${label} omitted>`;
|
|
3178
|
+
if (/^\s|\s$| -> /.test(path))
|
|
3179
|
+
return JSON.stringify(path);
|
|
3180
|
+
return path;
|
|
3181
|
+
}
|
|
3182
|
+
function isWorkspaceEvidencePathSafe(path) {
|
|
3183
|
+
return path !== '' && !/[\uFFFD\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(path);
|
|
3184
|
+
}
|
|
3185
|
+
function splitGitStatusRename(rawPath) {
|
|
3186
|
+
const separator = gitStatusRenameSeparator(rawPath);
|
|
3187
|
+
if (separator === -1)
|
|
3188
|
+
return undefined;
|
|
3189
|
+
return {
|
|
3190
|
+
source: rawPath.slice(0, separator),
|
|
3191
|
+
target: rawPath.slice(separator + 4),
|
|
3192
|
+
};
|
|
3193
|
+
}
|
|
3194
|
+
function gitStatusRenameSeparator(value) {
|
|
3195
|
+
let separator = -1;
|
|
3196
|
+
let inQuote = false;
|
|
3197
|
+
let escaped = false;
|
|
3198
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
3199
|
+
const char = value[index];
|
|
3200
|
+
if (inQuote) {
|
|
3201
|
+
if (escaped) {
|
|
3202
|
+
escaped = false;
|
|
3203
|
+
continue;
|
|
3204
|
+
}
|
|
3205
|
+
if (char === '\\') {
|
|
3206
|
+
escaped = true;
|
|
3207
|
+
continue;
|
|
3208
|
+
}
|
|
3209
|
+
if (char === '"')
|
|
3210
|
+
inQuote = false;
|
|
3211
|
+
continue;
|
|
3212
|
+
}
|
|
3213
|
+
if (char === '"') {
|
|
3214
|
+
inQuote = true;
|
|
3215
|
+
continue;
|
|
3216
|
+
}
|
|
3217
|
+
if (value.startsWith(' -> ', index)) {
|
|
3218
|
+
if (separator !== -1)
|
|
3219
|
+
return -1;
|
|
3220
|
+
separator = index;
|
|
3221
|
+
index += 3;
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
return inQuote ? -1 : separator;
|
|
3225
|
+
}
|
|
3226
|
+
function gitFailureToken(err) {
|
|
3227
|
+
const record = err;
|
|
3228
|
+
if (typeof record.signal === 'string' && record.signal)
|
|
3229
|
+
return 'signal';
|
|
3230
|
+
if (typeof record.code === 'number')
|
|
3231
|
+
return `exit-${record.code}`;
|
|
3232
|
+
return 'failed';
|
|
3233
|
+
}
|
|
3234
|
+
function unavailableGitEvidence(kind, err, detail) {
|
|
3235
|
+
const safeDetail = detail && /^[A-Za-z0-9._/@+-]{1,160}$/.test(detail) ? `:${detail}` : '';
|
|
3236
|
+
return `unavailable:${kind}${safeDetail}:${gitFailureToken(err)}`;
|
|
3237
|
+
}
|
|
3238
|
+
function gitQuotedPathEnd(value) {
|
|
3239
|
+
let escaped = false;
|
|
3240
|
+
for (let index = 1; index < value.length; index += 1) {
|
|
3241
|
+
const char = value[index];
|
|
3242
|
+
if (escaped) {
|
|
3243
|
+
escaped = false;
|
|
3244
|
+
continue;
|
|
3245
|
+
}
|
|
3246
|
+
if (char === '\\') {
|
|
3247
|
+
escaped = true;
|
|
3248
|
+
continue;
|
|
3249
|
+
}
|
|
3250
|
+
if (char === '"')
|
|
3251
|
+
return index + 1;
|
|
3252
|
+
}
|
|
3253
|
+
return -1;
|
|
3254
|
+
}
|
|
3255
|
+
function normalizeGitStatusPath(value) {
|
|
3256
|
+
const trimmed = value.trim();
|
|
3257
|
+
if (!trimmed.startsWith('"') || !trimmed.endsWith('"'))
|
|
3258
|
+
return value;
|
|
3259
|
+
try {
|
|
3260
|
+
const parsed = JSON.parse(trimmed);
|
|
3261
|
+
if (typeof parsed === 'string')
|
|
3262
|
+
return parsed;
|
|
2836
3263
|
}
|
|
2837
|
-
|
|
3264
|
+
catch {
|
|
3265
|
+
return decodeGitQuotedPath(trimmed);
|
|
3266
|
+
}
|
|
3267
|
+
return decodeGitQuotedPath(trimmed);
|
|
3268
|
+
}
|
|
3269
|
+
function decodeGitQuotedPath(value) {
|
|
3270
|
+
const body = value.slice(1, -1);
|
|
3271
|
+
let out = '';
|
|
3272
|
+
let bytes = [];
|
|
3273
|
+
const flushBytes = () => {
|
|
3274
|
+
if (bytes.length === 0)
|
|
3275
|
+
return;
|
|
3276
|
+
out += Buffer.from(bytes).toString('utf8');
|
|
3277
|
+
bytes = [];
|
|
3278
|
+
};
|
|
3279
|
+
for (let index = 0; index < body.length; index += 1) {
|
|
3280
|
+
const char = body[index] ?? '';
|
|
3281
|
+
if (char !== '\\') {
|
|
3282
|
+
flushBytes();
|
|
3283
|
+
out += char;
|
|
3284
|
+
continue;
|
|
3285
|
+
}
|
|
3286
|
+
const next = body[index + 1] ?? '';
|
|
3287
|
+
if (/[0-7]/.test(next)) {
|
|
3288
|
+
let octal = next;
|
|
3289
|
+
index += 1;
|
|
3290
|
+
for (let count = 0; count < 2 && /[0-7]/.test(body[index + 1] ?? ''); count += 1) {
|
|
3291
|
+
index += 1;
|
|
3292
|
+
octal += body[index] ?? '';
|
|
3293
|
+
}
|
|
3294
|
+
bytes.push(Number.parseInt(octal, 8));
|
|
3295
|
+
continue;
|
|
3296
|
+
}
|
|
3297
|
+
flushBytes();
|
|
3298
|
+
index += 1;
|
|
3299
|
+
if (next === 'n')
|
|
3300
|
+
out += '\n';
|
|
3301
|
+
else if (next === 't')
|
|
3302
|
+
out += '\t';
|
|
3303
|
+
else if (next === 'r')
|
|
3304
|
+
out += '\r';
|
|
3305
|
+
else if (next === 'b')
|
|
3306
|
+
out += '\b';
|
|
3307
|
+
else if (next === 'f')
|
|
3308
|
+
out += '\f';
|
|
3309
|
+
else if (next === 'v')
|
|
3310
|
+
out += '\v';
|
|
3311
|
+
else if (next === 'a')
|
|
3312
|
+
out += '\x07';
|
|
3313
|
+
else
|
|
3314
|
+
out += next;
|
|
3315
|
+
}
|
|
3316
|
+
flushBytes();
|
|
3317
|
+
return out;
|
|
2838
3318
|
}
|
|
2839
3319
|
async function workspaceContextFileBlock(root, requestedPath, maxFileBytes) {
|
|
2840
3320
|
const resolved = await resolveWorkspaceContextPath(root, requestedPath);
|
|
@@ -2866,10 +3346,12 @@ async function resolveWorkspaceContextPath(root, requestedPath) {
|
|
|
2866
3346
|
relativePath: relative(root, canonical) || '.',
|
|
2867
3347
|
};
|
|
2868
3348
|
}
|
|
2869
|
-
function shouldIncludeWorkspaceContextPath(path) {
|
|
3349
|
+
function shouldIncludeWorkspaceContextPath(path, runtimeStateExcludedPaths = EMPTY_WORKSPACE_PATH_EXCLUSIONS) {
|
|
2870
3350
|
const normalized = path.replaceAll('\\', '/').replace(/^\.\/+/, '');
|
|
2871
3351
|
if (!normalized || normalized.startsWith('../') || normalized.includes('/../'))
|
|
2872
3352
|
return false;
|
|
3353
|
+
if (workspacePathExcludedBySet(normalized, runtimeStateExcludedPaths))
|
|
3354
|
+
return false;
|
|
2873
3355
|
const parts = normalized.split('/');
|
|
2874
3356
|
if (parts.some((part) => WORKSPACE_CONTEXT_EXCLUDED_DIRS.has(part)))
|
|
2875
3357
|
return false;
|
|
@@ -2881,6 +3363,29 @@ function shouldIncludeWorkspaceContextPath(path) {
|
|
|
2881
3363
|
return false;
|
|
2882
3364
|
return WORKSPACE_CONTEXT_ALLOWED_EXTENSIONS.has(name.slice(dot).toLowerCase());
|
|
2883
3365
|
}
|
|
3366
|
+
function shouldExposeWorkspaceStatusPath(path, runtimeStateExcludedPaths = EMPTY_WORKSPACE_PATH_EXCLUSIONS) {
|
|
3367
|
+
const normalized = workspacePathKey(path);
|
|
3368
|
+
if (!normalized || normalized.startsWith('../') || normalized.includes('/../'))
|
|
3369
|
+
return false;
|
|
3370
|
+
if (workspacePathExcludedBySet(normalized, runtimeStateExcludedPaths))
|
|
3371
|
+
return false;
|
|
3372
|
+
return !normalized.split('/').some((part) => WORKSPACE_CONTEXT_EXCLUDED_DIRS.has(part));
|
|
3373
|
+
}
|
|
3374
|
+
function workspacePathKey(path) {
|
|
3375
|
+
return path.replaceAll('\\', '/').replace(/^\.\/+/, '');
|
|
3376
|
+
}
|
|
3377
|
+
function workspacePathExcludedBySet(path, excludedPaths) {
|
|
3378
|
+
const key = workspacePathKey(path);
|
|
3379
|
+
if (excludedPaths.has('.'))
|
|
3380
|
+
return true;
|
|
3381
|
+
for (const excludedPath of excludedPaths) {
|
|
3382
|
+
if (!excludedPath || excludedPath === '.')
|
|
3383
|
+
continue;
|
|
3384
|
+
if (key === excludedPath || key.startsWith(`${excludedPath}/`))
|
|
3385
|
+
return true;
|
|
3386
|
+
}
|
|
3387
|
+
return false;
|
|
3388
|
+
}
|
|
2884
3389
|
function numberWorkspaceContextLines(text) {
|
|
2885
3390
|
return text.split(/\r?\n/).map((line, index) => {
|
|
2886
3391
|
return `${String(index + 1).padStart(4, ' ')} | ${line}`;
|
|
@@ -3532,6 +4037,60 @@ function workflowScriptMetadataFromUnknown(value) {
|
|
|
3532
4037
|
...(typeof record.permissionKey === 'string' ? { permissionKey: record.permissionKey } : {}),
|
|
3533
4038
|
};
|
|
3534
4039
|
}
|
|
4040
|
+
function durableWorkflowRetryInput(input) {
|
|
4041
|
+
const scriptPath = typeof input.scriptPath === 'string' ? input.scriptPath : '';
|
|
4042
|
+
if (!scriptPath)
|
|
4043
|
+
throw workflowInputError('Workflow result resume input requires a persisted scriptPath.');
|
|
4044
|
+
return {
|
|
4045
|
+
scriptPath,
|
|
4046
|
+
...(input.args !== undefined ? { args: journalJsonValueOrInputError(input.args, 'workflow args') } : {}),
|
|
4047
|
+
...(typeof input.toolName === 'string' && input.toolName ? { toolName: input.toolName } : {}),
|
|
4048
|
+
};
|
|
4049
|
+
}
|
|
4050
|
+
function durableWorkflowResultRecordFromUnknown(value) {
|
|
4051
|
+
const record = asRecord(value);
|
|
4052
|
+
if (!record || typeof record.runId !== 'string' || !record.runId.trim())
|
|
4053
|
+
return null;
|
|
4054
|
+
if (typeof record.workflowName !== 'string' || !record.workflowName.trim())
|
|
4055
|
+
return null;
|
|
4056
|
+
if (typeof record.scriptHash !== 'string' || !record.scriptHash.startsWith('sha256:'))
|
|
4057
|
+
return null;
|
|
4058
|
+
const retryInput = durableWorkflowRetryInputFromUnknown(record.retryInput);
|
|
4059
|
+
return {
|
|
4060
|
+
runId: record.runId,
|
|
4061
|
+
workflowName: record.workflowName,
|
|
4062
|
+
scriptHash: record.scriptHash,
|
|
4063
|
+
...(retryInput ? { retryInput } : {}),
|
|
4064
|
+
};
|
|
4065
|
+
}
|
|
4066
|
+
function durableWorkflowRetryInputFromUnknown(value) {
|
|
4067
|
+
const record = asRecord(value);
|
|
4068
|
+
if (!record || typeof record.scriptPath !== 'string' || !record.scriptPath.trim())
|
|
4069
|
+
return null;
|
|
4070
|
+
return {
|
|
4071
|
+
scriptPath: record.scriptPath.trim(),
|
|
4072
|
+
...(record.args !== undefined ? { args: normalizeJournalJsonValue(record.args, 'workflow args') } : {}),
|
|
4073
|
+
...(typeof record.toolName === 'string' && record.toolName.trim() ? { toolName: record.toolName.trim() } : {}),
|
|
4074
|
+
};
|
|
4075
|
+
}
|
|
4076
|
+
function durableScriptRecordMatchesJournal(scriptRecord, started) {
|
|
4077
|
+
const metadata = scriptRecord.metadata;
|
|
4078
|
+
return scriptRecord.scriptPath === started.scriptPath
|
|
4079
|
+
&& metadata !== undefined
|
|
4080
|
+
&& metadata.workflowSource === started.workflowSource
|
|
4081
|
+
&& metadata.workflowSourcePath === started.workflowSourcePath;
|
|
4082
|
+
}
|
|
4083
|
+
function durableRetryInputArgsMatchJournal(input, journalArgs) {
|
|
4084
|
+
if (!Object.prototype.hasOwnProperty.call(input, 'args'))
|
|
4085
|
+
return true;
|
|
4086
|
+
return stableJson(input.args) === stableJson(journalArgs);
|
|
4087
|
+
}
|
|
4088
|
+
function durableRetryInputWithJournalArgs(input, journalArgs) {
|
|
4089
|
+
return {
|
|
4090
|
+
...input,
|
|
4091
|
+
args: journalArgs,
|
|
4092
|
+
};
|
|
4093
|
+
}
|
|
3535
4094
|
function workflowPermissionRecordFromUnknown(value) {
|
|
3536
4095
|
const record = asRecord(value);
|
|
3537
4096
|
if (!record)
|