imprint-mcp 0.6.2 → 0.6.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 +8 -1
- package/package.json +1 -1
- package/prompts/audit-agent.md +6 -5
- package/prompts/build-planning.md +2 -2
- package/prompts/compile-agent.md +6 -4
- package/prompts/live-verifier-agent.md +24 -11
- package/src/cli.ts +41 -6
- package/src/imprint/agent.ts +7 -2
- package/src/imprint/audit.ts +255 -42
- package/src/imprint/auth-compile-tools.ts +64 -13
- package/src/imprint/claude-cli-compile.ts +43 -7
- package/src/imprint/codex-cli-compile.ts +205 -129
- package/src/imprint/compile-agent-types.ts +25 -2
- package/src/imprint/compile-agent.ts +197 -40
- package/src/imprint/compile-tools.ts +164 -22
- package/src/imprint/inject-listener.ts +31 -3
- package/src/imprint/live-verifier.ts +167 -6
- package/src/imprint/llm.ts +7 -3
- package/src/imprint/mcp-compile-server.ts +188 -83
- package/src/imprint/mcp-server.ts +16 -1
- package/src/imprint/probe-backends.ts +60 -25
- package/src/imprint/react-flight-limits.ts +31 -0
- package/src/imprint/record.ts +657 -46
- package/src/imprint/redact.ts +240 -30
- package/src/imprint/response-body-intent.ts +73 -0
- package/src/imprint/response-body-lifecycle.ts +32 -0
- package/src/imprint/response-body-stream.ts +704 -0
- package/src/imprint/teach-state.ts +52 -2
- package/src/imprint/teach.ts +94 -69
- package/src/imprint/tracing.ts +60 -238
|
@@ -172,7 +172,18 @@ export function resolveWorkflowTriagedPath(
|
|
|
172
172
|
if (!ws) return null;
|
|
173
173
|
|
|
174
174
|
const explicitPath = resolveTeachStatePath(site, ws.triagedPath);
|
|
175
|
-
if (explicitPath)
|
|
175
|
+
if (explicitPath) {
|
|
176
|
+
// Older/resumed runs could accidentally triage an already-triaged session,
|
|
177
|
+
// leaving state pointed at `*.triaged.triaged.json`. Prefer the original
|
|
178
|
+
// single-triaged sibling when it still exists: it contains the complete
|
|
179
|
+
// load-bearing request set, while the second pass may have filtered requests
|
|
180
|
+
// selected by the first pass.
|
|
181
|
+
const singleTriagedPath = explicitPath.replace(/(?:\.triaged){2,}\.json$/, '.triaged.json');
|
|
182
|
+
if (singleTriagedPath !== explicitPath && existsSync(singleTriagedPath)) {
|
|
183
|
+
return singleTriagedPath;
|
|
184
|
+
}
|
|
185
|
+
return explicitPath;
|
|
186
|
+
}
|
|
176
187
|
|
|
177
188
|
if (!ws.completedSteps.includes('triage')) return null;
|
|
178
189
|
|
|
@@ -186,7 +197,20 @@ export function resolveWorkflowTriagedPath(
|
|
|
186
197
|
export function toRelativeTeachStatePath(site: string, absPath: string): string {
|
|
187
198
|
const localRelative = relativeToLocalSite(site, absPath);
|
|
188
199
|
if (localRelative) return localRelative;
|
|
189
|
-
|
|
200
|
+
// External artifacts are not copied under the site root. Persist their real
|
|
201
|
+
// absolute path so a later bounded resume can still resolve them; the former
|
|
202
|
+
// `_external_/<basename>` label looked portable but pointed at no actual file.
|
|
203
|
+
return pathResolve(absPath);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const TOOL_SCOPED_RESUME_STEPS = new Set<TeachStep>(['generate', 'compile-playbook', 'emit']);
|
|
207
|
+
|
|
208
|
+
/** `--tool` scopes the atomic per-tool compile unit. Earlier phases operate on
|
|
209
|
+
* the recording as a whole, while register is site-level, so accepting the flag
|
|
210
|
+
* there would either ignore it or falsely claim the run was bounded. */
|
|
211
|
+
export function validateToolScopedResumeStep(fromStep: TeachStep | undefined): string | null {
|
|
212
|
+
if (fromStep && TOOL_SCOPED_RESUME_STEPS.has(fromStep)) return null;
|
|
213
|
+
return 'error: --tool is only supported for bounded compile resumes starting at generate, compile-playbook, or emit';
|
|
190
214
|
}
|
|
191
215
|
|
|
192
216
|
export function buildTeachStateFromSession(
|
|
@@ -366,6 +390,7 @@ export function resolveStepStartTarget(
|
|
|
366
390
|
site: string,
|
|
367
391
|
state: TeachState,
|
|
368
392
|
fromStep: TeachStep,
|
|
393
|
+
toolName?: string,
|
|
369
394
|
): { workflowKey: string; ws: WorkflowState } {
|
|
370
395
|
// Exclude in-progress `_pending_*` placeholders: they never carry a candidate
|
|
371
396
|
// and never reached far enough, so a stale one with a newer timestamp would
|
|
@@ -381,6 +406,19 @@ export function resolveStepStartTarget(
|
|
|
381
406
|
].join('\n'),
|
|
382
407
|
);
|
|
383
408
|
}
|
|
409
|
+
if (toolName) {
|
|
410
|
+
const target = entries.find(
|
|
411
|
+
([workflowKey, ws]) => workflowKey === toolName || ws.candidate?.toolName === toolName,
|
|
412
|
+
);
|
|
413
|
+
if (!target) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
`Cannot resume tool "${toolName}" for "${site}": it is not present in the persisted teach state.`,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
const [workflowKey, ws] = target;
|
|
419
|
+
assertResumableAt(site, workflowKey, ws, fromStep);
|
|
420
|
+
return { workflowKey, ws };
|
|
421
|
+
}
|
|
384
422
|
// Most-recently-updated resumable workflow — the run a developer just executed
|
|
385
423
|
// and wants to resume a single phase of. Newly discovered orphan sessions can
|
|
386
424
|
// have newer timestamps but only `record`/`redact`; they must not shadow the
|
|
@@ -423,6 +461,7 @@ export function selectMultiToolResumePlans(
|
|
|
423
461
|
opts?: {
|
|
424
462
|
noInteractive?: boolean;
|
|
425
463
|
allTools?: boolean;
|
|
464
|
+
toolName?: string;
|
|
426
465
|
plannedToolNames?: ReadonlySet<string>;
|
|
427
466
|
},
|
|
428
467
|
): MultiToolResumeSelection {
|
|
@@ -445,6 +484,17 @@ export function selectMultiToolResumePlans(
|
|
|
445
484
|
}
|
|
446
485
|
plans.push({ workflowKey: key, candidate: ws.candidate, sharedContext: ws.sharedContext });
|
|
447
486
|
}
|
|
487
|
+
if (opts?.toolName) {
|
|
488
|
+
const selected = plans.find(
|
|
489
|
+
(plan) => plan.workflowKey === opts.toolName || plan.candidate?.toolName === opts.toolName,
|
|
490
|
+
);
|
|
491
|
+
if (!selected) {
|
|
492
|
+
throw new Error(
|
|
493
|
+
`Cannot resume tool "${opts.toolName}": it is not resumable from "${fromStep}" in the selected recording.`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
return { plans: [selected], skipped };
|
|
497
|
+
}
|
|
448
498
|
if (opts?.noInteractive && !opts.allTools && plans.length > 1) {
|
|
449
499
|
const primary = plans.find((plan) => plan.candidate?.primary);
|
|
450
500
|
const selected = primary ?? plans.find((plan) => plan.workflowKey === targetWorkflowKey);
|
package/src/imprint/teach.ts
CHANGED
|
@@ -150,6 +150,30 @@ export function hasDurableAuthState(
|
|
|
150
150
|
return stored.cookies.length > 0 || (stored.storage?.length ?? 0) > 0;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
export function resolvedArtifactCheckpointPath(
|
|
154
|
+
site: string,
|
|
155
|
+
_storedPath: string | undefined,
|
|
156
|
+
resolvedPath: string,
|
|
157
|
+
): string {
|
|
158
|
+
// toRelative keeps managed artifacts site-relative and genuine external
|
|
159
|
+
// artifacts absolute, so both forms remain resolvable on the next resume.
|
|
160
|
+
return toRelative(site, resolvedPath);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function selectCompileSessionArtifact(
|
|
164
|
+
resumedTriagedPath: string | null,
|
|
165
|
+
redactedPath: string | null,
|
|
166
|
+
): { path: string | null; triaged: boolean } {
|
|
167
|
+
const derivedTriagedPath = redactedPath?.replace(/\.redacted\.json$/, '.triaged.json') ?? null;
|
|
168
|
+
const path =
|
|
169
|
+
(resumedTriagedPath && existsSync(resumedTriagedPath) ? resumedTriagedPath : null) ??
|
|
170
|
+
(derivedTriagedPath && existsSync(derivedTriagedPath) ? derivedTriagedPath : redactedPath);
|
|
171
|
+
return {
|
|
172
|
+
path,
|
|
173
|
+
triaged: path === resumedTriagedPath || path === derivedTriagedPath,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
153
177
|
function fileSha256(path: string): string {
|
|
154
178
|
return createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
155
179
|
}
|
|
@@ -205,6 +229,8 @@ interface TeachOptions {
|
|
|
205
229
|
keepTest?: boolean;
|
|
206
230
|
/** Non-interactive: compile every detected candidate instead of primary only. */
|
|
207
231
|
allTools?: boolean;
|
|
232
|
+
/** Bounded resume: compile exactly one persisted candidate from the retained recording. */
|
|
233
|
+
tool?: string;
|
|
208
234
|
/** Skip the replay-and-diff stage entirely. */
|
|
209
235
|
skipReplay?: boolean;
|
|
210
236
|
/** Run only specific phases of the teach chain. `fromStep` resumes a PRIOR run
|
|
@@ -490,6 +516,7 @@ export async function teach(opts: TeachOptions): Promise<TeachResult> {
|
|
|
490
516
|
let workflowKey: string | null = null;
|
|
491
517
|
let sessionPath: string | null = opts.fromSession ?? null;
|
|
492
518
|
let redactedPath: string | null = null;
|
|
519
|
+
let resumedTriagedPath: string | null = null;
|
|
493
520
|
let usingFromSession = false;
|
|
494
521
|
|
|
495
522
|
const hasExisting = completedWorkflows.length > 0 || incompleteWorkflows.length > 0;
|
|
@@ -506,7 +533,7 @@ export async function teach(opts: TeachOptions): Promise<TeachResult> {
|
|
|
506
533
|
// Non-interactive phase resume: start at a specific step, reusing a prior
|
|
507
534
|
// run's persisted outputs. resolveStepStartTarget picks the most-recent
|
|
508
535
|
// workflow and THROWS if it didn't reach far enough (the dependency guard).
|
|
509
|
-
const target = resolveStepStartTarget(site, state, opts.fromStep);
|
|
536
|
+
const target = resolveStepStartTarget(site, state, opts.fromStep, opts.tool);
|
|
510
537
|
workflowKey = target.workflowKey;
|
|
511
538
|
startFrom = opts.fromStep;
|
|
512
539
|
sessionPath = resolveTeachStatePath(site, target.ws.sessionPath);
|
|
@@ -1215,9 +1242,14 @@ export async function teach(opts: TeachOptions): Promise<TeachResult> {
|
|
|
1215
1242
|
}
|
|
1216
1243
|
const ws = state.workflows[workflowKey];
|
|
1217
1244
|
const resolvedTriagedPath = resolveWorkflowTriagedPath(site, ws);
|
|
1218
|
-
|
|
1245
|
+
resumedTriagedPath = resolvedTriagedPath;
|
|
1246
|
+
if (
|
|
1247
|
+
ws &&
|
|
1248
|
+
resolvedTriagedPath &&
|
|
1249
|
+
resolveTeachStatePath(site, ws.triagedPath) !== resolvedTriagedPath
|
|
1250
|
+
) {
|
|
1219
1251
|
updateCheckpoint(site, state, workflowKey, 'triage', {
|
|
1220
|
-
triagedPath:
|
|
1252
|
+
triagedPath: resolvedArtifactCheckpointPath(site, ws.triagedPath, resolvedTriagedPath),
|
|
1221
1253
|
});
|
|
1222
1254
|
}
|
|
1223
1255
|
// A `--from-step` resume into plan-prereqs/generate reconstructs the prior
|
|
@@ -1242,6 +1274,7 @@ export async function teach(opts: TeachOptions): Promise<TeachResult> {
|
|
|
1242
1274
|
const selection = selectMultiToolResumePlans(state, workflowKey, startFrom, {
|
|
1243
1275
|
noInteractive: opts.noInteractive,
|
|
1244
1276
|
allTools: opts.allTools,
|
|
1277
|
+
toolName: opts.tool,
|
|
1245
1278
|
plannedToolNames,
|
|
1246
1279
|
});
|
|
1247
1280
|
for (const { workflowKey: skippedKey, reason } of selection.skipped) {
|
|
@@ -1325,15 +1358,12 @@ export async function teach(opts: TeachOptions): Promise<TeachResult> {
|
|
|
1325
1358
|
// thousands of irrelevant requests (large auth recordings otherwise may never converge on a
|
|
1326
1359
|
// playbook because of this). The detect-candidates summary already triages;
|
|
1327
1360
|
// this aligns the compile session with it.
|
|
1328
|
-
const
|
|
1329
|
-
const
|
|
1330
|
-
triagedCandidate && existsSync(triagedCandidate) ? triagedCandidate : redactedPath;
|
|
1331
|
-
const useTriaged = sessionForCompile === triagedCandidate;
|
|
1332
|
-
const compileSessionPath = requireSessionFile(sessionForCompile, {
|
|
1361
|
+
const compileSessionArtifact = selectCompileSessionArtifact(resumedTriagedPath, redactedPath);
|
|
1362
|
+
const compileSessionPath = requireSessionFile(compileSessionArtifact.path, {
|
|
1333
1363
|
site,
|
|
1334
1364
|
workflowKey: plans[0]?.workflowKey ?? workflowKey,
|
|
1335
1365
|
startFrom,
|
|
1336
|
-
kind:
|
|
1366
|
+
kind: compileSessionArtifact.triaged ? 'triaged' : 'redacted',
|
|
1337
1367
|
});
|
|
1338
1368
|
|
|
1339
1369
|
// ── Clean up stale tools from previous teach runs ──
|
|
@@ -1380,66 +1410,61 @@ export async function teach(opts: TeachOptions): Promise<TeachResult> {
|
|
|
1380
1410
|
const authDetected = plans.some((pl) => sharedContextHasAuth(pl.sharedContext));
|
|
1381
1411
|
let buildPlanPath = '';
|
|
1382
1412
|
let sharedModulesManifest: SharedModuleManifestEntry[] = [];
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
buildPlanPath
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
updateCheckpoint(site, state, pl.workflowKey, 'plan-prereqs', {
|
|
1439
|
-
buildPlanPath: buildPlanPath ? toRelative(site, buildPlanPath) : undefined,
|
|
1440
|
-
sharedModules: sharedModulesManifest,
|
|
1441
|
-
});
|
|
1442
|
-
}
|
|
1413
|
+
const sidecar = buildPlanSidecarPath(site);
|
|
1414
|
+
const firstWs = state.workflows[plans[0]?.workflowKey ?? ''];
|
|
1415
|
+
// A bounded primary-only resume still belongs to its original multi-tool
|
|
1416
|
+
// build plan. Reuse that durable contract even though this invocation selects
|
|
1417
|
+
// one tool; otherwise its assigned shared modules and producer-token contract
|
|
1418
|
+
// silently disappear from the revision agent's context.
|
|
1419
|
+
const alreadyPlanned =
|
|
1420
|
+
opts.fromStep !== 'plan-prereqs' &&
|
|
1421
|
+
plans.every((pl) => state.workflows[pl.workflowKey]?.completedSteps.includes('plan-prereqs')) &&
|
|
1422
|
+
existsSync(sidecar);
|
|
1423
|
+
if (willGenerate && compileModel && alreadyPlanned && firstWs) {
|
|
1424
|
+
buildPlanPath = sidecar;
|
|
1425
|
+
sharedModulesManifest = firstWs.sharedModules ?? [];
|
|
1426
|
+
} else if ((selectedCandidates.length >= 2 || authDetected) && willGenerate && compileModel) {
|
|
1427
|
+
// Mute raw `[imprint …]` logs from the planning subtree (build-plan,
|
|
1428
|
+
// teach-plan, prereq-builder) while the spinner is live — progress flows
|
|
1429
|
+
// through onProgress → spinner.message instead, matching the replay and
|
|
1430
|
+
// compile phases. The skip/timeout reason is surfaced cleanly below.
|
|
1431
|
+
muteLog();
|
|
1432
|
+
spinner.start('Planning shared modules');
|
|
1433
|
+
try {
|
|
1434
|
+
const prereq = await planAndBuildPrereqs({
|
|
1435
|
+
site,
|
|
1436
|
+
redactedSessionPath: compileSessionPath,
|
|
1437
|
+
candidates: selectedCandidates,
|
|
1438
|
+
sharedContext: plans[0]?.sharedContext,
|
|
1439
|
+
siteClassifications,
|
|
1440
|
+
providerName: compileProviderName,
|
|
1441
|
+
model: compileModel,
|
|
1442
|
+
onProgress: (msg) => spinner.message(msg),
|
|
1443
|
+
});
|
|
1444
|
+
buildPlanPath = prereq.buildPlanPath;
|
|
1445
|
+
sharedModulesManifest = prereq.sharedModules;
|
|
1446
|
+
const verified = sharedModulesManifest.filter((m) => m.verified).length;
|
|
1447
|
+
spinner.stop(
|
|
1448
|
+
buildPlanPath
|
|
1449
|
+
? `Build plan ready (${verified}/${sharedModulesManifest.length} shared module${sharedModulesManifest.length === 1 ? '' : 's'} verified).`
|
|
1450
|
+
: 'Build plan skipped.',
|
|
1451
|
+
);
|
|
1452
|
+
if (prereq.skippedReason) p.log.warn(prereq.skippedReason);
|
|
1453
|
+
} catch (err) {
|
|
1454
|
+
spinner.stop('Build planning failed — compiling tools independently.');
|
|
1455
|
+
p.log.warn(
|
|
1456
|
+
`Build planning failed: ${err instanceof Error ? err.message : String(err)}\nTools will compile without shared modules.`,
|
|
1457
|
+
);
|
|
1458
|
+
buildPlanPath = '';
|
|
1459
|
+
sharedModulesManifest = [];
|
|
1460
|
+
} finally {
|
|
1461
|
+
unmuteLog();
|
|
1462
|
+
}
|
|
1463
|
+
for (const pl of plans) {
|
|
1464
|
+
updateCheckpoint(site, state, pl.workflowKey, 'plan-prereqs', {
|
|
1465
|
+
buildPlanPath: buildPlanPath ? toRelative(site, buildPlanPath) : undefined,
|
|
1466
|
+
sharedModules: sharedModulesManifest,
|
|
1467
|
+
});
|
|
1443
1468
|
}
|
|
1444
1469
|
}
|
|
1445
1470
|
|