imprint-mcp 0.6.2 → 0.6.3
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/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 +38 -3
- package/src/imprint/agent.ts +7 -2
- package/src/imprint/audit.ts +212 -20
- package/src/imprint/auth-compile-tools.ts +64 -13
- 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/live-verifier.ts +167 -6
- package/src/imprint/llm.ts +4 -0
- 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/teach-state.ts +52 -2
- package/src/imprint/teach.ts +94 -69
|
@@ -49,6 +49,9 @@ interface RunMcpServerOptions {
|
|
|
49
49
|
name?: string;
|
|
50
50
|
/** Server version. */
|
|
51
51
|
version?: string;
|
|
52
|
+
/** Optional exact tool-name allowlist. Used by audit sessions to keep
|
|
53
|
+
* human-interactive authentication workflows out of headless MCP exposure. */
|
|
54
|
+
includeToolNames?: readonly string[];
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
interface ResolvedTool extends DiscoveredTool {
|
|
@@ -58,6 +61,17 @@ interface ResolvedTool extends DiscoveredTool {
|
|
|
58
61
|
preferredOrder?: ConcreteBackend[];
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
/** Exact allowlist selection used by bounded MCP sessions such as audit. An
|
|
65
|
+
* omitted allowlist preserves normal production behavior and exposes all tools. */
|
|
66
|
+
export function selectMcpTools<T extends { workflow: { toolName: string } }>(
|
|
67
|
+
tools: readonly T[],
|
|
68
|
+
includeToolNames?: readonly string[],
|
|
69
|
+
): T[] {
|
|
70
|
+
if (!includeToolNames) return [...tools];
|
|
71
|
+
const includedNames = new Set(includeToolNames);
|
|
72
|
+
return tools.filter((tool) => includedNames.has(tool.workflow.toolName));
|
|
73
|
+
}
|
|
74
|
+
|
|
61
75
|
/** Tool description shown to MCP clients. Includes the operator's
|
|
62
76
|
* recorded narration when present — surprisingly load-bearing for the
|
|
63
77
|
* LLM picking the right tool. */
|
|
@@ -437,7 +451,8 @@ function formatToolError(
|
|
|
437
451
|
|
|
438
452
|
export async function runMcpServer(opts: RunMcpServerOptions): Promise<void> {
|
|
439
453
|
const assetRoot = opts.assetRoot ?? imprintHomeDir();
|
|
440
|
-
const
|
|
454
|
+
const discoveredAll = await discoverTools(assetRoot, opts.site, '[imprint mcp]');
|
|
455
|
+
const discovered = selectMcpTools(discoveredAll, opts.includeToolNames);
|
|
441
456
|
const tools: ResolvedTool[] = discovered.map((t) => {
|
|
442
457
|
const playbookPath = pathResolve(t.dir, 'playbook.yaml');
|
|
443
458
|
const cacheStatus = loadBackendsCacheStatus(t.site, assetRoot, t.dir, {
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
BackendsCacheSchema,
|
|
27
27
|
type ConcreteBackend,
|
|
28
28
|
CronConfigSchema,
|
|
29
|
+
type ToolResult,
|
|
29
30
|
WorkflowSchema,
|
|
30
31
|
} from './types.ts';
|
|
31
32
|
import { VERSION } from './version.ts';
|
|
@@ -120,20 +121,32 @@ export async function probeResolvedTool(
|
|
|
120
121
|
log(`probing backends for ${tool.workflow.toolName}…`);
|
|
121
122
|
log(` params: ${JSON.stringify(params)}`);
|
|
122
123
|
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
// falls through fetch-bootstrap (~30-60s) before reaching the spliced-in
|
|
127
|
-
// cdp-replay rung, wasting time on every call.
|
|
124
|
+
// Probe the workflow's normal candidates first. Plain workflows keep the
|
|
125
|
+
// cheap fetch/stealth/playbook path; browser-backed API transports are only
|
|
126
|
+
// added if none of those candidates works.
|
|
128
127
|
const stealthCache = new Map<string, StealthFetch>();
|
|
129
128
|
const cdpPool = new Map<string, CdpBrowserFetch>();
|
|
130
129
|
const allBackends = probeCandidateBackendsForWorkflow(tool.workflow);
|
|
131
130
|
const results: BackendsCache['results'] = {};
|
|
132
131
|
const working: BackendProbeCandidate[] = [];
|
|
133
132
|
const preferredMaxMs = preferredBackendMaxMs();
|
|
133
|
+
const persistCurrentCache = (): BackendsCache => {
|
|
134
|
+
const cache: BackendsCache = {
|
|
135
|
+
probedAt: new Date().toISOString(),
|
|
136
|
+
imprintVersion: VERSION,
|
|
137
|
+
schemaVersion: 2,
|
|
138
|
+
workflowHash: workflowHash(tool.workflow),
|
|
139
|
+
capabilityHash: workflowCapabilityHash(tool.workflow),
|
|
140
|
+
preferredOrder: rankSuccessfulBackends(working),
|
|
141
|
+
results,
|
|
142
|
+
};
|
|
143
|
+
BackendsCacheSchema.parse(cache);
|
|
144
|
+
writeFileSync(outPath, `${JSON.stringify(cache, null, 2)}\n`);
|
|
145
|
+
return cache;
|
|
146
|
+
};
|
|
134
147
|
|
|
135
|
-
|
|
136
|
-
for (const backend of
|
|
148
|
+
const probeCandidates = async (backends: ConcreteBackend[]): Promise<void> => {
|
|
149
|
+
for (const backend of backends) {
|
|
137
150
|
log(`probing ${backend}…`);
|
|
138
151
|
const t0 = Date.now();
|
|
139
152
|
const { result, attempts } = await runWithLadder(
|
|
@@ -147,6 +160,14 @@ export async function probeResolvedTool(
|
|
|
147
160
|
const durationMs = Date.now() - t0;
|
|
148
161
|
const attempt = attempts[0];
|
|
149
162
|
|
|
163
|
+
const invariantFailure = backendInvariantProbeFailure(result);
|
|
164
|
+
if (invariantFailure) {
|
|
165
|
+
log(` ${backend}: workflow rejected the request before transport — stopping probe`);
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Backend-independent workflow failure for ${tool.workflow.toolName}: ${invariantFailure}`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
150
171
|
if (!attempt) {
|
|
151
172
|
results[backend] = { outcome: 'skipped', detail: 'no attempt recorded' };
|
|
152
173
|
continue;
|
|
@@ -189,6 +210,11 @@ export async function probeResolvedTool(
|
|
|
189
210
|
...(warm?.ok ? { warmDurationMs: warm.durationMs, rankingDurationMs } : {}),
|
|
190
211
|
tooSlow,
|
|
191
212
|
});
|
|
213
|
+
// Keep a valid preferred backend durable as soon as one succeeds. A
|
|
214
|
+
// later optional candidate may tarpit until the aggregate preparation
|
|
215
|
+
// deadline; losing this earlier success would force an unnecessary
|
|
216
|
+
// reprobe and leave the suite with no prepared backend.
|
|
217
|
+
persistCurrentCache();
|
|
192
218
|
log(
|
|
193
219
|
` ${backend}: OK in ${durationMs}ms${warm?.ok ? ` (warm ${warm.durationMs}ms)` : ''}${tooSlow ? ' (cold slow)' : ''}`,
|
|
194
220
|
);
|
|
@@ -212,6 +238,14 @@ export async function probeResolvedTool(
|
|
|
212
238
|
log(` ${backend}: ${result.error} — ${result.message.slice(0, 100)}`);
|
|
213
239
|
}
|
|
214
240
|
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
await probeCandidates(allBackends);
|
|
245
|
+
if (working.length === 0 && !allBackends.includes('cdp-replay')) {
|
|
246
|
+
log('default backends failed; probing browser-backed API fallbacks…');
|
|
247
|
+
await probeCandidates(['cdp-replay', 'fetch-bootstrap']);
|
|
248
|
+
}
|
|
215
249
|
} finally {
|
|
216
250
|
await closeProbeCdpPool(cdpPool);
|
|
217
251
|
}
|
|
@@ -224,24 +258,24 @@ export async function probeResolvedTool(
|
|
|
224
258
|
);
|
|
225
259
|
}
|
|
226
260
|
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
probedAt: new Date().toISOString(),
|
|
230
|
-
imprintVersion: VERSION,
|
|
231
|
-
schemaVersion: 2,
|
|
232
|
-
workflowHash: workflowHash(tool.workflow),
|
|
233
|
-
capabilityHash: workflowCapabilityHash(tool.workflow),
|
|
234
|
-
preferredOrder,
|
|
235
|
-
results,
|
|
236
|
-
};
|
|
237
|
-
BackendsCacheSchema.parse(cache); // catch schema drift early
|
|
238
|
-
|
|
239
|
-
writeFileSync(outPath, `${JSON.stringify(cache, null, 2)}\n`);
|
|
240
|
-
log(`wrote ${outPath} — preferred: ${preferredOrder.join(' → ')}`);
|
|
261
|
+
const cache = persistCurrentCache();
|
|
262
|
+
log(`wrote ${outPath} — preferred: ${cache.preferredOrder.join(' → ')}`);
|
|
241
263
|
|
|
242
264
|
return { cache, outPath };
|
|
243
265
|
}
|
|
244
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Some generated-workflow failures happen before any HTTP request is sent.
|
|
269
|
+
* Retrying those through browser-backed transports is both expensive and
|
|
270
|
+
* misleading: a different transport cannot repair a deterministic transform.
|
|
271
|
+
* Keep remote HTTP BAD_RESPONSE failures probeable because bot defenses can
|
|
272
|
+
* return backend-specific 4xx responses.
|
|
273
|
+
*/
|
|
274
|
+
export function backendInvariantProbeFailure(result: ToolResult): string | null {
|
|
275
|
+
if (result.ok || result.error !== 'BAD_RESPONSE') return null;
|
|
276
|
+
return /^request transform failed for request \d+:/i.test(result.message) ? result.message : null;
|
|
277
|
+
}
|
|
278
|
+
|
|
245
279
|
export function rankSuccessfulBackends(candidates: BackendProbeCandidate[]): ConcreteBackend[] {
|
|
246
280
|
return [...candidates]
|
|
247
281
|
.sort((a, b) => {
|
|
@@ -302,9 +336,10 @@ function preferredBackendMaxMs(): number {
|
|
|
302
336
|
|
|
303
337
|
function workflowNeedsBootstrap(workflow: ResolvedTool['workflow']): boolean {
|
|
304
338
|
if (workflow.bootstrap) return true;
|
|
305
|
-
return workflow.requests.some((
|
|
306
|
-
(
|
|
307
|
-
(
|
|
339
|
+
return workflow.requests.some((request) =>
|
|
340
|
+
(request.captures ?? []).some(
|
|
341
|
+
(capture) =>
|
|
342
|
+
capture.capability === 'browser_bootstrap' || capture.capability === 'stealth_bootstrap',
|
|
308
343
|
),
|
|
309
344
|
);
|
|
310
345
|
}
|
|
@@ -313,7 +348,7 @@ export function probeCandidateBackendsForWorkflow(
|
|
|
313
348
|
workflow: ResolvedTool['workflow'],
|
|
314
349
|
): ConcreteBackend[] {
|
|
315
350
|
return workflowNeedsBootstrap(workflow) || prefersCdpReplayFirst(workflow)
|
|
316
|
-
? ['fetch', '
|
|
351
|
+
? ['fetch', 'cdp-replay', 'fetch-bootstrap', 'stealth-fetch', 'playbook']
|
|
317
352
|
: ['fetch', 'stealth-fetch', 'playbook'];
|
|
318
353
|
}
|
|
319
354
|
|
|
@@ -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
|
|