wicked-crew 0.7.26 → 0.7.28
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/dist/api/routes.d.ts +8 -0
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +9 -2
- package/dist/api/routes.js.map +1 -1
- package/dist/api/server.d.ts +13 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +14 -0
- package/dist/api/server.js.map +1 -1
- package/dist/cli/index.js +46 -18
- package/dist/cli/index.js.map +1 -1
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +22 -18
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/deliver.d.ts +26 -10
- package/dist/core/deliver.d.ts.map +1 -1
- package/dist/core/deliver.js +27 -11
- package/dist/core/deliver.js.map +1 -1
- package/dist/core/deliverable-floor.d.ts.map +1 -1
- package/dist/core/deliverable-floor.js +5 -4
- package/dist/core/deliverable-floor.js.map +1 -1
- package/dist/interactive/bridge-pool.d.ts +130 -2
- package/dist/interactive/bridge-pool.d.ts.map +1 -1
- package/dist/interactive/bridge-pool.js +281 -8
- package/dist/interactive/bridge-pool.js.map +1 -1
- package/dist/interactive/bus-location.d.ts +65 -0
- package/dist/interactive/bus-location.d.ts.map +1 -0
- package/dist/interactive/bus-location.js +75 -0
- package/dist/interactive/bus-location.js.map +1 -0
- package/dist/interactive/chat-events.d.ts.map +1 -1
- package/dist/interactive/chat-events.js +29 -23
- package/dist/interactive/chat-events.js.map +1 -1
- package/dist/interactive/demo-events.d.ts +16 -1
- package/dist/interactive/demo-events.d.ts.map +1 -1
- package/dist/interactive/demo-events.js +262 -43
- package/dist/interactive/demo-events.js.map +1 -1
- package/dist/interactive/doc-grounding.d.ts +202 -0
- package/dist/interactive/doc-grounding.d.ts.map +1 -0
- package/dist/interactive/doc-grounding.js +647 -0
- package/dist/interactive/doc-grounding.js.map +1 -0
- package/dist/interactive/draft-events.d.ts +54 -1
- package/dist/interactive/draft-events.d.ts.map +1 -1
- package/dist/interactive/draft-events.js +234 -117
- package/dist/interactive/draft-events.js.map +1 -1
- package/dist/interactive/edit-events.d.ts.map +1 -1
- package/dist/interactive/edit-events.js +27 -21
- package/dist/interactive/edit-events.js.map +1 -1
- package/dist/interactive/proxy-routes.d.ts +78 -0
- package/dist/interactive/proxy-routes.d.ts.map +1 -1
- package/dist/interactive/proxy-routes.js +355 -14
- package/dist/interactive/proxy-routes.js.map +1 -1
- package/dist/interactive/repo-snapshot.d.ts +31 -0
- package/dist/interactive/repo-snapshot.d.ts.map +1 -1
- package/dist/interactive/repo-snapshot.js +56 -1
- package/dist/interactive/repo-snapshot.js.map +1 -1
- package/dist/qe/conformance.d.ts +10 -1
- package/dist/qe/conformance.d.ts.map +1 -1
- package/dist/qe/conformance.js +57 -4
- package/dist/qe/conformance.js.map +1 -1
- package/dist/skills/plugin-source.d.ts +149 -32
- package/dist/skills/plugin-source.d.ts.map +1 -1
- package/dist/skills/plugin-source.js +303 -108
- package/dist/skills/plugin-source.js.map +1 -1
- package/dist/skills/root-fence.d.ts +2 -2
- package/dist/skills/root-fence.js +4 -4
- package/dist/skills/root-fence.js.map +1 -1
- package/dist/skills/runtime.d.ts +19 -2
- package/dist/skills/runtime.d.ts.map +1 -1
- package/dist/skills/runtime.js +82 -8
- package/dist/skills/runtime.js.map +1 -1
- package/dist/skills/store.d.ts +2 -2
- package/dist/skills/store.d.ts.map +1 -1
- package/dist/skills/store.js +30 -9
- package/dist/skills/store.js.map +1 -1
- package/dist/studio/assets/index-CdmCMh2f.js +539 -0
- package/dist/studio/index.html +1 -1
- package/dist/studio/testid-inventory.json +125 -5
- package/endpoint-manifest.json +16 -1
- package/package.json +3 -3
- package/dist/studio/assets/index-FcZg3WDR.js +0 -539
|
@@ -36,11 +36,13 @@
|
|
|
36
36
|
*/
|
|
37
37
|
import { resolveProjectGraphBinding } from '../projects/graph.js';
|
|
38
38
|
import { mkdirSync, existsSync, rmSync, statSync } from 'node:fs';
|
|
39
|
-
import { join } from 'node:path';
|
|
39
|
+
import { basename, dirname, join } from 'node:path';
|
|
40
40
|
import { randomUUID } from 'node:crypto';
|
|
41
41
|
import { InteractiveHandoffLedger } from './ledger.js';
|
|
42
42
|
import { crewStateHome } from '../projects/state-home.js';
|
|
43
|
-
import { snapshotRepo } from './repo-snapshot.js';
|
|
43
|
+
import { runDirInsideRepo, snapshotRepo } from './repo-snapshot.js';
|
|
44
|
+
import { resolveInteractiveRoot } from './bridge-root.js';
|
|
45
|
+
import { groundingNarration, resolveGroundingRepos, snapshotDirName, styleContract, } from './doc-grounding.js';
|
|
44
46
|
import { DELIVERABLE_FLOOR_PHASE_ID } from '../core/deliverable-floor.js';
|
|
45
47
|
// ── Vocabulary constants (interactive's, verbatim — src/service/events.js is the truth) ──────
|
|
46
48
|
export const INTERACTIVE_DOMAIN = 'wicked-interactive';
|
|
@@ -61,6 +63,22 @@ export const INTERACTIVE_PRODUCER = 'wi-crew';
|
|
|
61
63
|
* malformed document_id can't name a ledger key or a draft file path. Shared with the
|
|
62
64
|
* structural-edit seam (edit-events.ts), which guards the same identity. */
|
|
63
65
|
export const DOC_NAME = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
66
|
+
/**
|
|
67
|
+
* The doc-identity half of EVERY payload crew's interactive seams emit (acceptance finding F-045):
|
|
68
|
+
* `document_id`, plus `project_id` when the document is project-bound. The bridge stamps
|
|
69
|
+
* `project_id` on its own emits (DES-PROJECT-001 enrichment) and the studio files frames by it —
|
|
70
|
+
* so a crew heartbeat carrying `document_id` alone was filed under the Unfiled mount while the
|
|
71
|
+
* project-bound thread heard nothing and, 90 s later, told the user "the generation service may
|
|
72
|
+
* be down" over a run that was executing. Shared by all four seams (draft/edit/chat/demo); an
|
|
73
|
+
* unfiled doc keeps the field OFF — never a fabricated 'default'.
|
|
74
|
+
*/
|
|
75
|
+
export function docScope(documentId, projectId) {
|
|
76
|
+
return projectId !== undefined ? { document_id: documentId, project_id: projectId } : { document_id: documentId };
|
|
77
|
+
}
|
|
78
|
+
/** How long a seam waits for the proxy to record a document's create-time grounding binding when
|
|
79
|
+
* a create for the same project is still in flight (doc-grounding.ts `waitFor`): the bridge emits
|
|
80
|
+
* `doc.created` before it answers the create, so the bus can beat the record by a few ms. */
|
|
81
|
+
export const GROUNDING_BINDING_WAIT_MS = 3000;
|
|
64
82
|
// ── The workflow (workflows-as-data) ─────────────────────────────────────────────────────────
|
|
65
83
|
export const INTERACTIVE_DRAFT_WORKFLOW = 'interactive-draft';
|
|
66
84
|
/**
|
|
@@ -159,6 +177,12 @@ export function oneLine(text, cap) {
|
|
|
159
177
|
* has no repo member, the registry no longer knows the ref, or the adapter cannot answer (old
|
|
160
178
|
* addon, engine hiccup) — every one of those degrades to today's behavior: an ungrounded launch.
|
|
161
179
|
*
|
|
180
|
+
* NO LONGER THE SEAM'S GROUNDING RULE (acceptance finding F-046): "the first member" grounded a
|
|
181
|
+
* brochure about wicked-studio on wicked-core. The draft and demo seams now resolve through
|
|
182
|
+
* `doc-grounding.ts` `resolveGroundingRepos` — the repos NAMED on the create request, else the
|
|
183
|
+
* ones the brief names, else the project's sole repo, else none (narrated). Kept exported for
|
|
184
|
+
* diagnostics and its callers.
|
|
185
|
+
*
|
|
162
186
|
* WHY: a doc created under a repo-backed project used to launch its governed draft/revision
|
|
163
187
|
* run with NO repo context at all, so the worker could not read the project's actual code and
|
|
164
188
|
* generated placeholder content (operator report, wicked-studio project). Shared by the draft
|
|
@@ -281,24 +305,49 @@ export function proposeClause() {
|
|
|
281
305
|
* SINGLE-LINE by contract: the PTY seat runner refuses any embedded newline (FINDING-011), so
|
|
282
306
|
* every fragment here stays on one line and the brief is flattened+capped by {@link oneLine}.
|
|
283
307
|
*/
|
|
284
|
-
export function draftProblem(doc, outPath, snapshotDir, intent) {
|
|
308
|
+
export function draftProblem(doc, outPath, snapshotDir, intent, grounding) {
|
|
285
309
|
const sources = doc.sourcePaths.length > 0
|
|
286
310
|
? `Source materials to read: ${doc.sourcePaths.join(', ')}.`
|
|
287
311
|
: 'There are no source files — the brief alone is the spec.';
|
|
288
312
|
const brief = doc.brief.length > 0 ? oneLine(doc.brief, 2000) : '(no brief provided)';
|
|
289
|
-
|
|
313
|
+
// F-046: the SUBJECT — which repositories the document is about, stated before any tool clause,
|
|
314
|
+
// so the worker never substitutes a sibling repository (the brochure-about-studio-drafted-from-
|
|
315
|
+
// core outcome). Absent for an unfiled doc or a repo-less project.
|
|
316
|
+
const subjects = grounding?.subjects ?? [];
|
|
317
|
+
const subjectClause = subjects.length > 0
|
|
318
|
+
? `This document is ABOUT the ${subjects.length === 1 ? 'repository' : 'repositories'} ` +
|
|
319
|
+
`${subjects.map((s) => s.name).join(', ')} — ground every product claim in ` +
|
|
320
|
+
`${subjects.length === 1 ? 'that repository' : 'those repositories'} (never a sibling repository ` +
|
|
321
|
+
`that happens to share the project), and say in the draft's notes when something you need is not in reach. `
|
|
322
|
+
: grounding?.unnamedAmong !== undefined && grounding.unnamedAmong > 1
|
|
323
|
+
? `The project has ${grounding.unnamedAmong} repositories and none was named for this document — do not ` +
|
|
324
|
+
`present details from an arbitrary one as the subject's; where the brief refers to a specific ` +
|
|
325
|
+
`repository, say in the draft's notes that its source was not in reach. `
|
|
326
|
+
: '';
|
|
327
|
+
// The offline fallback: the subjects' snapshots when grounding resolved them (F-046), else the
|
|
328
|
+
// legacy single snapshot dir a caller passed. Paths ride VERBATIM (Copilot, crew#313).
|
|
329
|
+
const snapshots = subjects.flatMap((s) => s.snapshotDir !== undefined ? [{ name: s.name, dir: s.snapshotDir }] : []);
|
|
330
|
+
if (snapshots.length === 0 && snapshotDir !== undefined)
|
|
331
|
+
snapshots.push({ name: undefined, dir: snapshotDir });
|
|
332
|
+
const fallback = snapshots.length === 0
|
|
333
|
+
? ''
|
|
334
|
+
: snapshots.length === 1
|
|
335
|
+
? `If the estate tools are unavailable, fall back to the offline repository snapshot at ${snapshots[0].dir}` +
|
|
336
|
+
`${snapshots[0].name !== undefined ? ` (${snapshots[0].name})` : ''} instead. `
|
|
337
|
+
: `If the estate tools are unavailable, fall back to the offline repository snapshots at ` +
|
|
338
|
+
`${snapshots.map((s) => `${s.dir} (${s.name})`).join(' and ')} instead. `;
|
|
339
|
+
const groundingClause = `Ground every claim in the indexed repositories via the wicked-estate MCP tools: ` +
|
|
290
340
|
`SearchEntity to find relevant code and docs, FetchContent to read them, ContextBundle to gather ` +
|
|
291
341
|
`related material, and RetrieveEntity/TraverseGraph to follow references — research across all ` +
|
|
292
342
|
`bound repos and use what those tools return, never placeholders. ` +
|
|
293
|
-
|
|
294
|
-
? `If the estate tools are unavailable, fall back to the offline repository snapshot at ${snapshotDir} instead. `
|
|
295
|
-
: '');
|
|
343
|
+
fallback;
|
|
296
344
|
// The recall clause (DES-MEM-FACETED-001 Phase 3) sits right beside grounding; `''` when the
|
|
297
345
|
// intent carries no axis, so an unfiled draft reads exactly as it did before this phase.
|
|
298
346
|
const recall = recallClause(intent);
|
|
299
347
|
const propose = proposeClause();
|
|
300
348
|
return (`Produce the first draft of the wicked-interactive document "${doc.documentId}" ` +
|
|
301
|
-
`(requested style: ${doc.style}). The user's brief: ${brief} ${sources}
|
|
349
|
+
`(requested style: ${doc.style} — ${styleContract(doc.style)}). The user's brief: ${brief} ${sources} ` +
|
|
350
|
+
`${subjectClause}${groundingClause}${recall}${propose}` +
|
|
302
351
|
`The finished draft MUST be written to exactly this absolute file path: ${outPath}`);
|
|
303
352
|
}
|
|
304
353
|
/** Deterministic bus idempotency key for the one draft this seam may land per document. */
|
|
@@ -365,6 +414,8 @@ export async function startInteractiveDraftSubscriber(adapter, opts = {}) {
|
|
|
365
414
|
const ledger = new InteractiveHandoffLedger(opts.ledgerPath ?? join(defaultStateDir(), 'interactive-draft-ledger.json'));
|
|
366
415
|
const draftDir = opts.draftDir ?? join(defaultStateDir(), 'interactive-drafts');
|
|
367
416
|
const heartbeatMs = opts.heartbeatMs ?? 15_000;
|
|
417
|
+
const groundingStore = opts.groundingStore;
|
|
418
|
+
const resolveDocsRoot = opts.resolveDocsRoot ?? (() => resolveInteractiveRoot(null));
|
|
368
419
|
// The run executes ONE MORE unit than the def declares: the crew#311 deliverable floor,
|
|
369
420
|
// appended per-run by `launchRun` from `requireDeliverables`. `agentPhaseCount` is what the
|
|
370
421
|
// council-and-worker narration branches key on (unchanged); `phaseCount` is the run's real
|
|
@@ -398,10 +449,15 @@ export async function startInteractiveDraftSubscriber(adapter, opts = {}) {
|
|
|
398
449
|
return false;
|
|
399
450
|
}
|
|
400
451
|
}
|
|
452
|
+
/** Every `status.posted` this seam emits is typed as the published frame's payload (codex on
|
|
453
|
+
* crew#506: the wire type at the real boundary, not a detached alias) — `emitInteractive` adds `ts`. */
|
|
454
|
+
function emitStatus(payload) {
|
|
455
|
+
return emitInteractive(STATUS_POSTED, { ...payload });
|
|
456
|
+
}
|
|
401
457
|
function narrate(flight, message) {
|
|
402
458
|
flight.narration = message;
|
|
403
|
-
|
|
404
|
-
|
|
459
|
+
emitStatus({
|
|
460
|
+
...docScope(flight.documentId, flight.projectId),
|
|
405
461
|
state: 'working',
|
|
406
462
|
message,
|
|
407
463
|
});
|
|
@@ -415,20 +471,34 @@ export async function startInteractiveDraftSubscriber(adapter, opts = {}) {
|
|
|
415
471
|
}
|
|
416
472
|
return flight;
|
|
417
473
|
}
|
|
418
|
-
/** CREW-UX-8 v4: the repo
|
|
474
|
+
/** CREW-UX-8 v4: the repo snapshots are launch-scoped — remove them on EVERY terminal path
|
|
419
475
|
* (success, no-file, emit-failure, run failure/cancel) so the inbox never accretes dead
|
|
420
476
|
* clones. Best-effort: a leftover snapshot is a disk-space wart, never a correctness one. */
|
|
421
|
-
function
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
477
|
+
function removeSnapshots(flight) {
|
|
478
|
+
const dirs = flight.snapshotDirs;
|
|
479
|
+
flight.snapshotDirs = [];
|
|
480
|
+
const parents = new Set();
|
|
481
|
+
for (const dir of dirs) {
|
|
482
|
+
try {
|
|
483
|
+
rmSync(dir, { recursive: true, force: true });
|
|
484
|
+
log(`[interactive-draft] removed repo snapshot ${dir}`);
|
|
485
|
+
parents.add(dirname(dir));
|
|
486
|
+
}
|
|
487
|
+
catch (err) {
|
|
488
|
+
log(`[interactive-draft] could not remove repo snapshot ${dir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
489
|
+
}
|
|
429
490
|
}
|
|
430
|
-
|
|
431
|
-
|
|
491
|
+
// The per-run `repos/` parent held nothing but this run's snapshots — it goes too, so the
|
|
492
|
+
// inbox never accretes empty shells.
|
|
493
|
+
for (const parent of parents) {
|
|
494
|
+
if (basename(parent) !== 'repos')
|
|
495
|
+
continue;
|
|
496
|
+
try {
|
|
497
|
+
rmSync(parent, { recursive: true, force: true });
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
/* best-effort */
|
|
501
|
+
}
|
|
432
502
|
}
|
|
433
503
|
}
|
|
434
504
|
/** Terminal-event fold: turn the governed run's own events into interactive narration, and
|
|
@@ -522,11 +592,11 @@ export async function startInteractiveDraftSubscriber(adapter, opts = {}) {
|
|
|
522
592
|
}
|
|
523
593
|
if (event.type === 'sessionFailed' || event.type === 'runCancelled') {
|
|
524
594
|
endFlight(runId);
|
|
525
|
-
|
|
595
|
+
removeSnapshots(flight); // the failure path cleans its snapshots too (CREW-UX-8 v4)
|
|
526
596
|
ledger.recordFailure(flight.documentId);
|
|
527
597
|
const why = flight.failureDetail !== undefined ? ` Reason: ${oneLine(flight.failureDetail, 600)}` : '';
|
|
528
|
-
|
|
529
|
-
|
|
598
|
+
emitStatus({
|
|
599
|
+
...docScope(flight.documentId, flight.projectId),
|
|
530
600
|
state: 'error',
|
|
531
601
|
message: `The crew run answering this document ${event.type === 'runCancelled' ? 'was cancelled' : 'failed'} ` +
|
|
532
602
|
`(run ${runId}).${why} Inspect it via the crew API (GET /api/v1/runs/${runId}); the assist loop can still take over.`,
|
|
@@ -535,9 +605,9 @@ export async function startInteractiveDraftSubscriber(adapter, opts = {}) {
|
|
|
535
605
|
}
|
|
536
606
|
});
|
|
537
607
|
function finalize(flight, runId) {
|
|
538
|
-
// The run is terminal — its grounding
|
|
539
|
-
|
|
540
|
-
const { documentId, outPath } = flight;
|
|
608
|
+
// The run is terminal — its grounding snapshots are done serving reads, on every branch below.
|
|
609
|
+
removeSnapshots(flight);
|
|
610
|
+
const { documentId, projectId, outPath } = flight;
|
|
541
611
|
let ok = false;
|
|
542
612
|
try {
|
|
543
613
|
ok = existsSync(outPath) && statSync(outPath).size > 0;
|
|
@@ -547,8 +617,8 @@ export async function startInteractiveDraftSubscriber(adapter, opts = {}) {
|
|
|
547
617
|
}
|
|
548
618
|
if (!ok) {
|
|
549
619
|
ledger.recordFailure(documentId);
|
|
550
|
-
|
|
551
|
-
|
|
620
|
+
emitStatus({
|
|
621
|
+
...docScope(documentId, projectId),
|
|
552
622
|
state: 'error',
|
|
553
623
|
message: `The crew run completed but produced no draft file at ${outPath} (run ${runId}).`,
|
|
554
624
|
});
|
|
@@ -557,14 +627,14 @@ export async function startInteractiveDraftSubscriber(adapter, opts = {}) {
|
|
|
557
627
|
}
|
|
558
628
|
// ADR-0019 D5: announce by path — the service reads the file itself, so a large draft
|
|
559
629
|
// never rides the bus payload. The deterministic key makes a re-announce a WB-002 no-op.
|
|
560
|
-
const emitted = emitInteractive(DRAFT_COMPLETED, {
|
|
630
|
+
const emitted = emitInteractive(DRAFT_COMPLETED, { ...docScope(documentId, projectId), html_path: outPath }, draftIdempotencyKey(documentId));
|
|
561
631
|
if (!emitted) {
|
|
562
632
|
// The bus refused the announce (non-WB-002): the draft exists on disk but never reached
|
|
563
633
|
// the service. Fail HONEST — leaving the ledger row launched-but-never-closed would
|
|
564
634
|
// silently eat every replay of this doc (the launch gate is `ledger.has`).
|
|
565
635
|
ledger.recordFailure(documentId);
|
|
566
|
-
|
|
567
|
-
|
|
636
|
+
emitStatus({
|
|
637
|
+
...docScope(documentId, projectId),
|
|
568
638
|
state: 'error',
|
|
569
639
|
message: `Crew finished the draft but could not announce it on the bus (run ${runId}); ` +
|
|
570
640
|
`the draft file is at ${outPath}. Inspect the crew daemon log.`,
|
|
@@ -573,8 +643,8 @@ export async function startInteractiveDraftSubscriber(adapter, opts = {}) {
|
|
|
573
643
|
return;
|
|
574
644
|
}
|
|
575
645
|
ledger.recordEmitted(documentId);
|
|
576
|
-
|
|
577
|
-
|
|
646
|
+
emitStatus({
|
|
647
|
+
...docScope(documentId, projectId),
|
|
578
648
|
state: 'complete',
|
|
579
649
|
message: 'First draft is in — landing it on the canvas now. Click any block to refine it.',
|
|
580
650
|
});
|
|
@@ -611,88 +681,138 @@ export async function startInteractiveDraftSubscriber(adapter, opts = {}) {
|
|
|
611
681
|
// must endFlight() it.
|
|
612
682
|
const flight = {
|
|
613
683
|
documentId: doc.documentId,
|
|
684
|
+
projectId: doc.projectId,
|
|
614
685
|
outPath,
|
|
686
|
+
snapshotDirs: [],
|
|
615
687
|
narration: 'Crew run launched — working on your draft…',
|
|
616
688
|
};
|
|
617
689
|
inFlight.set(runId, flight);
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
690
|
+
// F-046: WHICH repositories is this document about? The create request's `repo_ref(s)` (the
|
|
691
|
+
// proxy recorded them, keyed by this doc — waited for, bounded, when the create is still
|
|
692
|
+
// answering), else the ones the brief names, else the project's sole repo, else NONE. Never
|
|
693
|
+
// the project's first member: that grounded a brochure about wicked-studio on wicked-core.
|
|
694
|
+
// Unbound docs skip this entirely and launch exactly as before.
|
|
695
|
+
let decision;
|
|
696
|
+
if (doc.projectId !== undefined) {
|
|
697
|
+
// The sidecar sits beside the doc; a refused partition (bridge-root.ts) throws here and the
|
|
698
|
+
// frame goes unanswered — fail closed, like the sibling seams' docs-root reads.
|
|
699
|
+
const binding = groundingStore !== undefined
|
|
700
|
+
? await groundingStore.waitFor(resolveDocsRoot(doc.projectId), doc.documentId, doc.projectId, GROUNDING_BINDING_WAIT_MS)
|
|
701
|
+
: undefined;
|
|
702
|
+
decision = await resolveGroundingRepos(adapter, doc.projectId, doc.brief, binding?.repo_refs, log);
|
|
703
|
+
}
|
|
704
|
+
emitStatus({
|
|
705
|
+
...docScope(doc.documentId, doc.projectId),
|
|
624
706
|
state: 'processing',
|
|
625
707
|
message: 'A governed crew picked up your brief — planning the draft…',
|
|
626
708
|
});
|
|
627
|
-
// The
|
|
628
|
-
// the UI's silence budget) and BEFORE launchRun: <runDir>/
|
|
629
|
-
// declared extra write root, so the unbound worker can read it (write roots are readable,
|
|
709
|
+
// The snapshots happen crew-side, AFTER the pickup narration (a big clone must not starve
|
|
710
|
+
// the UI's silence budget) and BEFORE launchRun: <runDir>/repos/<name> sits inside the run's
|
|
711
|
+
// OWN declared extra write root, so the unbound worker can read it (write roots are readable,
|
|
630
712
|
// wicked-core#259) even though the live repo root is boundary-denied (wicked-core#294) —
|
|
631
713
|
// and NO OTHER run can (per-run isolation, Copilot crew#313). An unsnapshotable repo (over
|
|
632
|
-
// budget, unreadable, clone+copy failed) degrades HONESTLY:
|
|
633
|
-
// per-cause note on the thread, and the full reason in the log.
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
714
|
+
// budget, unreadable, clone+copy failed) degrades HONESTLY: the subject is still NAMED in the
|
|
715
|
+
// task, a visible per-cause note lands on the thread, and the full reason in the log.
|
|
716
|
+
// The inbox must never sit inside ANY registered repository (codex on crew#506): it is the
|
|
717
|
+
// worker's extra write root, so a draft dir configured inside a checkout hands the unbound
|
|
718
|
+
// worker write access to live source whatever the run is about — the per-repo dest-overlap
|
|
719
|
+
// check below covers only the repo being snapshotted. Refuse BEFORE anything is created.
|
|
720
|
+
let inside;
|
|
721
|
+
try {
|
|
722
|
+
inside = await runDirInsideRepo(adapter, runDir);
|
|
723
|
+
}
|
|
724
|
+
catch (err) {
|
|
725
|
+
// Unresolvable (EACCES/ELOOP — plain ENOENT is walked past) = unprovable = refused, WITH a
|
|
726
|
+
// user-facing status naming the cause before the frame dead-letters (Copilot on crew#506).
|
|
727
|
+
endFlight(runId);
|
|
728
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
729
|
+
const message = `Crew refused to draft this document: the configured draft directory (${draftDir}) could not be verified ` +
|
|
730
|
+
`against the registered repositories (${why}). Fix the path or its permissions, then replay the request.`;
|
|
731
|
+
emitStatus({ ...docScope(doc.documentId, doc.projectId), state: 'error', message });
|
|
732
|
+
log(`[interactive-draft] doc ${doc.documentId}: REFUSING launch — run dir ${runDir} unverifiable: ${why}`);
|
|
733
|
+
throw new Error(message);
|
|
734
|
+
}
|
|
735
|
+
if (inside !== null) {
|
|
736
|
+
endFlight(runId);
|
|
737
|
+
const message = `Crew refused to draft this document: the configured draft directory (${draftDir}) overlaps the ` +
|
|
738
|
+
`registered repository ${inside}, so launching would give the worker write access inside live source. ` +
|
|
739
|
+
`Point the crew draft directory outside every registered repository, then replay the request.`;
|
|
740
|
+
emitStatus({ ...docScope(doc.documentId, doc.projectId), state: 'error', message });
|
|
741
|
+
log(`[interactive-draft] doc ${doc.documentId}: REFUSING launch — run dir ${runDir} is inside repo ${inside}`);
|
|
742
|
+
throw new Error(message);
|
|
743
|
+
}
|
|
744
|
+
const snapshotted = [];
|
|
745
|
+
const subjects = [];
|
|
746
|
+
const taken = new Set();
|
|
747
|
+
for (const repo of decision?.repos ?? []) {
|
|
748
|
+
const dest = join(runDir, 'repos', snapshotDirName(repo, taken));
|
|
638
749
|
if (!groundablePath(dest)) {
|
|
639
750
|
// The PATH itself cannot ride the grounding clause (too long for the PTY prompt
|
|
640
751
|
// budget, or multi-line) — and a truncated spelling would name a nonexistent dir, so
|
|
641
|
-
//
|
|
642
|
-
|
|
643
|
-
|
|
752
|
+
// the snapshot is SKIPPED before any clone happens (Copilot, crew#313).
|
|
753
|
+
emitStatus({
|
|
754
|
+
...docScope(doc.documentId, doc.projectId),
|
|
644
755
|
state: 'working',
|
|
645
|
-
message:
|
|
756
|
+
message: `snapshot path for ${repo.name} too long to hand to the worker — drafting without its snapshot`,
|
|
646
757
|
});
|
|
647
|
-
log(`[interactive-draft] doc ${doc.documentId}: snapshot dest ${dest} cannot ride the grounding clause —
|
|
758
|
+
log(`[interactive-draft] doc ${doc.documentId}: snapshot dest ${dest} cannot ride the grounding clause — no snapshot for ${repo.repoRef}`);
|
|
759
|
+
subjects.push({ name: repo.name });
|
|
760
|
+
continue;
|
|
648
761
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
762
|
+
// Track the dest BEFORE the await (Copilot round 2): stop() during the clone must be able
|
|
763
|
+
// to sweep the half-made snapshot through the placeholder flight.
|
|
764
|
+
flight.snapshotDirs.push(dest);
|
|
765
|
+
const snap = await snapshotRepo(repo.rootPath, dest, { maxBytes: opts.repoSnapshotMaxBytes, log });
|
|
766
|
+
if (snap.ok) {
|
|
767
|
+
snapshotted.push(repo);
|
|
768
|
+
subjects.push({ name: repo.name, snapshotDir: dest });
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
flight.snapshotDirs = flight.snapshotDirs.filter((d) => d !== dest); // nothing landed — snapshotRepo cleans its partials
|
|
772
|
+
if (snap.reason === 'dest-overlap') {
|
|
773
|
+
// FAIL CLOSED (Copilot round 2): the configured draft dir places this run's write
|
|
774
|
+
// root inside the live repository (or the repo is registered at the inbox). An
|
|
775
|
+
// "ungrounded" launch would still hand the unbound worker read/write access to
|
|
776
|
+
// live repo content through `extraWriteRoots: [runDir]` — so the launch is REFUSED
|
|
777
|
+
// outright: no mkdir, no run, no ledger row. The status names the CONFIG problem;
|
|
778
|
+
// the thrown error dead-letters the frame, replayable after the config is fixed.
|
|
779
|
+
endFlight(runId);
|
|
780
|
+
removeSnapshots(flight); // whatever sibling subjects already landed
|
|
781
|
+
const message = `Crew refused to draft this document: the configured draft directory (${draftDir}) ` +
|
|
782
|
+
`overlaps the project's repository (${repo.rootPath}), so launching would give the ` +
|
|
783
|
+
`worker write access inside the live repo. Point the crew draft directory outside ` +
|
|
784
|
+
`every registered repository, then replay the request.`;
|
|
785
|
+
emitStatus({
|
|
786
|
+
...docScope(doc.documentId, doc.projectId),
|
|
787
|
+
state: 'error',
|
|
788
|
+
message,
|
|
789
|
+
});
|
|
790
|
+
log(`[interactive-draft] doc ${doc.documentId}: REFUSING launch — draft dir ${draftDir} overlaps repo ${repo.rootPath} (dest-overlap)`);
|
|
791
|
+
throw new Error(message);
|
|
792
|
+
}
|
|
793
|
+
// Per-cause operator message (Copilot, crew#313): "too large" was previously
|
|
794
|
+
// claimed for EVERY failure — a deleted repo is not a large one. (`dest-overlap`
|
|
795
|
+
// is handled above: it refuses the launch instead of degrading.)
|
|
796
|
+
const because = {
|
|
797
|
+
'too-large': 'repository too large to snapshot',
|
|
798
|
+
'root-unreadable': 'repository path is missing or unreadable',
|
|
799
|
+
'dest-unclearable': 'a stale snapshot could not be cleared',
|
|
800
|
+
'copy-failed': 'repository snapshot failed (clone and copy both errored)',
|
|
801
|
+
};
|
|
802
|
+
emitStatus({
|
|
803
|
+
...docScope(doc.documentId, doc.projectId),
|
|
804
|
+
state: 'working',
|
|
805
|
+
message: `${because[snap.reason]} (${repo.name}) — drafting without its snapshot`,
|
|
806
|
+
});
|
|
807
|
+
log(`[interactive-draft] doc ${doc.documentId}: repo ${repo.rootPath} could not be snapshotted (${snap.reason}) — no snapshot for ${repo.repoRef}`);
|
|
808
|
+
subjects.push({ name: repo.name });
|
|
809
|
+
}
|
|
810
|
+
// The thread hears WHERE this draft is grounded and WHY (F-046 follow-up) — or, for a
|
|
811
|
+
// multi-repo project whose document named nothing, how to name one next time.
|
|
812
|
+
if (decision !== undefined) {
|
|
813
|
+
const line = groundingNarration(decision, snapshotted, 'draft');
|
|
814
|
+
if (line !== null) {
|
|
815
|
+
emitStatus({ ...docScope(doc.documentId, doc.projectId), state: 'working', message: line });
|
|
696
816
|
}
|
|
697
817
|
}
|
|
698
818
|
// Shutdown gate (Copilot round 2): stop() may have run during the awaits above — its sweep
|
|
@@ -700,15 +820,7 @@ export async function startInteractiveDraftSubscriber(adapter, opts = {}) {
|
|
|
700
820
|
// a launch must never start once the subscriber detached from the engine's events.
|
|
701
821
|
if (closed) {
|
|
702
822
|
endFlight(runId);
|
|
703
|
-
flight
|
|
704
|
-
if (snapshotDest !== undefined) {
|
|
705
|
-
try {
|
|
706
|
-
rmSync(snapshotDest, { recursive: true, force: true });
|
|
707
|
-
}
|
|
708
|
-
catch {
|
|
709
|
-
// best-effort — a leftover snapshot is a disk-space wart, never a correctness one
|
|
710
|
-
}
|
|
711
|
-
}
|
|
823
|
+
removeSnapshots(flight);
|
|
712
824
|
log(`[interactive-draft] doc ${doc.documentId}: subscriber stopped before launch — abandoned (a replay retries)`);
|
|
713
825
|
return;
|
|
714
826
|
}
|
|
@@ -742,7 +854,12 @@ export async function startInteractiveDraftSubscriber(adapter, opts = {}) {
|
|
|
742
854
|
// axis (the reliably-available axis on this seam). An unfiled doc leaves it undefined, so
|
|
743
855
|
// the clause is omitted. Phase 6: thread cli/repo (no single cli is in scope here — the
|
|
744
856
|
// launch carries the whole council roster via `clisJson`, not one assigned seat).
|
|
745
|
-
problem: draftProblem(doc, outPath,
|
|
857
|
+
problem: draftProblem(doc, outPath, undefined, doc.projectId !== undefined ? { project: doc.projectId } : undefined, decision !== undefined
|
|
858
|
+
? {
|
|
859
|
+
subjects,
|
|
860
|
+
...(decision.source === 'none' ? { unnamedAmong: decision.memberCount } : {}),
|
|
861
|
+
}
|
|
862
|
+
: undefined),
|
|
746
863
|
sessionId: runId,
|
|
747
864
|
clisJson: opts.clisJson ?? JSON.stringify(rosterOf(adapter)),
|
|
748
865
|
workflow: INTERACTIVE_DRAFT_WORKFLOW,
|
|
@@ -783,12 +900,12 @@ export async function startInteractiveDraftSubscriber(adapter, opts = {}) {
|
|
|
783
900
|
// A launch that never happened keeps no flight and no snapshot (a replayed frame
|
|
784
901
|
// re-registers and re-snapshots fresh).
|
|
785
902
|
endFlight(runId);
|
|
786
|
-
|
|
903
|
+
removeSnapshots(flight);
|
|
787
904
|
// The 'processing' status is already on the thread — close it out honestly so the
|
|
788
905
|
// canvas never sits in an in-between state on a launch that went nowhere.
|
|
789
906
|
const reason = err instanceof Error ? err.message : String(err);
|
|
790
|
-
|
|
791
|
-
|
|
907
|
+
emitStatus({
|
|
908
|
+
...docScope(doc.documentId, doc.projectId),
|
|
792
909
|
state: 'error',
|
|
793
910
|
message: `Crew could not start a run for this document: ${reason}. The assist loop can still take over.`,
|
|
794
911
|
});
|
|
@@ -814,8 +931,8 @@ export async function startInteractiveDraftSubscriber(adapter, opts = {}) {
|
|
|
814
931
|
flight.heartbeat = setInterval(() => {
|
|
815
932
|
// Repeat the last real narration so the ~20s status.requested window is always fed,
|
|
816
933
|
// even mid-phase when the engine is quiet.
|
|
817
|
-
|
|
818
|
-
|
|
934
|
+
emitStatus({
|
|
935
|
+
...docScope(flight.documentId, flight.projectId),
|
|
819
936
|
state: 'working',
|
|
820
937
|
message: flight.narration,
|
|
821
938
|
});
|
|
@@ -856,7 +973,7 @@ export async function startInteractiveDraftSubscriber(adapter, opts = {}) {
|
|
|
856
973
|
// whatever the in-flight clone re-materializes after this rm. The engine's workers
|
|
857
974
|
// die with the daemon, so nothing is still reading the snapshot.
|
|
858
975
|
if (flight !== undefined)
|
|
859
|
-
|
|
976
|
+
removeSnapshots(flight);
|
|
860
977
|
}
|
|
861
978
|
await sub.stop();
|
|
862
979
|
},
|