crewhaus 0.2.3 → 0.3.0

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/index.js CHANGED
@@ -9,19 +9,24 @@ import { SpecParseError, compile, lower } from "@crewhaus/compiler";
9
9
  import { buildContextBundle, discoverRoots } from "@crewhaus/context-bundle";
10
10
  import { DEFAULT_PRICING, classifyPricingStaleness, computeCacheSavingsMicros, parsePricingFeed, pickNewestPricing, resolvePricing, } from "@crewhaus/cost-tracker";
11
11
  import { compareVersions, createFileBackedRegistry, latestVersion, } from "@crewhaus/dataset-registry";
12
- import { CrewhausError } from "@crewhaus/errors";
12
+ import { CrewhausError, RunFailedError } from "@crewhaus/errors";
13
13
  import { loadDataset } from "@crewhaus/eval-dataset";
14
14
  import { parseGradersConfig } from "@crewhaus/eval-grader";
15
15
  import { extractCurrentPrompt as extractInstructions, optimizeSpec, } from "@crewhaus/eval-optimizer-orchestrator";
16
16
  import { buildMatrix, diffReports, formatUsd, hashDatasetFile, loadRun, readBaselines, readRunIndex, renderMatrix, renderReport, setBaseline, } from "@crewhaus/eval-report";
17
- import { runEval as runEvalLib } from "@crewhaus/eval-runner";
17
+ import { createExamRunner, runEval as runEvalLib, } from "@crewhaus/eval-runner";
18
18
  import { openEventLog } from "@crewhaus/event-log";
19
19
  import { loadHooks, runHooks } from "@crewhaus/hooks-engine";
20
20
  import { ArgParseError, parseArgs, } from "@crewhaus/infra-utils";
21
21
  import { GENERATED_README_MARKER } from "@crewhaus/ir";
22
22
  import { createLogger } from "@crewhaus/logging";
23
- import { McpHost } from "@crewhaus/mcp-host";
24
- import { captureFacts, createMemoryStore, deriveMemoryDecision, summarizeDurableFacts, } from "@crewhaus/memory-store";
23
+ import { McpHost, resolveMcpServerConfig } from "@crewhaus/mcp-host";
24
+ // PR 10 the memory fabric's composition root: `crewhaus run` makes the
25
+ // same one stable wireMemory call compiled bundles emit. PR 16 adds
26
+ // connectThredz (the §4.3 backend flip's boot helper — bare-name MCP tool
27
+ // aliases + degrade-on-failure).
28
+ import { connectThredz, memoryFragmentFromIr, runDreamBootCatchUp, wireMemory, } from "@crewhaus/memory-service";
29
+ import { createMemoryStore } from "@crewhaus/memory-store";
25
30
  import { BUILTIN_DEFAULT_RULES, PermissionConfigError, parsePermissionsConfig, tagRules, } from "@crewhaus/permission-engine";
26
31
  // Adaptive model routing — `crewhaus route status|reset` inspects/clears the
27
32
  // per-(routeKey, model) reward scoreboard behind `agent.model_pool`; advise
@@ -36,9 +41,11 @@ import { specHasPath } from "@crewhaus/spec-patch";
36
41
  import { spawnSubAgent } from "@crewhaus/sub-agent-spawner";
37
42
  import { ToolCatalog } from "@crewhaus/tool-catalog";
38
43
  import { registerMcpServer } from "@crewhaus/tool-mcp";
39
- import { createMemoryTools } from "@crewhaus/tool-memory";
40
44
  import { createTaskTool } from "@crewhaus/tool-task";
41
45
  import { TraceEventBus } from "@crewhaus/trace-event-bus";
46
+ // 0.3.0 memory release (design §3.1, PR 9) — the local wiki substrate behind
47
+ // `crewhaus wiki list|show|search|stats`.
48
+ import { createWikiStore } from "@crewhaus/wiki-store";
42
49
  import { parseDocument } from "yaml";
43
50
  // Item 15 — `optimize --from-advice` (eval-gated apply of the advisor's
44
51
  // SpecPatches: suggestions-file validation, the accept/reject/compose loop
@@ -113,6 +120,12 @@ import { buildChannelEnvChecks, buildCredentialChecks, extractSpecModel, provide
113
120
  // (which runs an argv switch on import) stays testable.
114
121
  import { buildInventory, claudeDesktopConfigPath, formatInventory, } from "./doctor-detect";
115
122
  import { formatFixPlan, planCrewhausDirs, planEnvStubs, planScaffoldSpec, planScopeFix, } from "./doctor-fix";
123
+ // v0.3.0 Goal 6 — `doctor --probe`: opt-in ~1-token live call per
124
+ // configured provider, catching unfunded/invalid keys before a long run.
125
+ import { buildProbePlan, probeResultsToChecks, runProviderProbes } from "./doctor-probe";
126
+ // v0.3.0 PR 14 — `crewhaus dream run|status|init` (design §6.3), in a
127
+ // side-effect-free module; only the dispatch registration lives here.
128
+ import { DREAM_CLI_SCHEMA, runDreamCommand } from "./dream-cli";
116
129
  // FR-006 — Pillar 3 sink-side egress-matcher selection (the substring/semantic
117
130
  // selector lowered to ir.security.egressMatcher). Side-effect-free + lazily
118
131
  // imports the optional semantic package only when "semantic" is selected, so
@@ -157,7 +170,7 @@ import { formatFewShotForPrompt, harvestFewShot, isFewShotExample, mergePools, p
157
170
  // module so it is unit-testable (this entry file runs an argv switch on
158
171
  // import). Discovery + aggregation are pure reads; the bulk runner spawns
159
172
  // per-harness `crewhaus` invocations through an injected seam.
160
- import { FleetError, buildFleetInventory, buildHarnessHealth, formatBulkReport, formatInventory as formatFleetInventory, formatHealth, runFleetBulk, } from "./fleet";
173
+ import { FleetError, buildFleetInventory, buildHarnessHealth, fleetSelfInvokeArgv, formatBulkReport, formatInventory as formatFleetInventory, formatHealth, runFleetBulk, } from "./fleet";
161
174
  // Item 45 — `crewhaus flywheel init|run`: the packaged nightly
162
175
  // self-improvement loop (knob/default resolution, the accept-then-write
163
176
  // loop with injected steps, the report, and the workflow scaffold), in a
@@ -173,10 +186,12 @@ import { DEFAULT_SUGGESTED_GRADERS_FILE, DEFAULT_SUGGEST_RUNS, FLOOR_GRADER_HINT
173
186
  // join, cost summary, eval-report-styled render), in a side-effect-free module
174
187
  // so it is unit-testable (this entry file runs an argv switch on import).
175
188
  import { assembleIncidentBundle, matchAuditRecordsByWindow, summarizeCost, } from "./incident";
176
- // Item 39 — `crewhaus init --interactive`: the harness-designer interview
177
- // (validate-and-retry loop + scripted no-credentials fallback) in a
178
- // side-effect-free module so this entry file stays testable.
179
- import { EMIT_SPEC_TOOL, buildInterviewSystemPrompt, buildScriptedSpec, isScriptedShape, runInterview, } from "./init-interactive";
189
+ // Item 39 / v0.3.0 §2.9 — `crewhaus init --interactive`: the conversational
190
+ // harness-designer interview (persisted, resumable runChatLoop session) plus
191
+ // the scripted no-credentials fallback, in importable modules so this entry
192
+ // file stays testable.
193
+ import { INIT_INTERVIEW_SAVED_NOTE, conversationalPathAllowed, runConversationalInterview, } from "./init-conversation";
194
+ import { buildScriptedSpec, isScriptedShape, } from "./init-interactive";
180
195
  // Item 67 — `crewhaus intents`: cluster user_message inputs across sessions +
181
196
  // rank by frequency / satisfaction / failure. Side-effect-free (this entry file
182
197
  // reads sessions + feedback and redacts the rendered examples).
@@ -220,6 +235,9 @@ import { MarketplaceCliError, buildPublishPrPlan, computeOutdated, createHttpMod
220
235
  // drift diff, quarantine decision — in a side-effect-free module (this entry
221
236
  // file runs an argv switch on import) mirroring doctor-checks.ts.
222
237
  import { buildSnapshot, decideQuarantine, diffSnapshots, formatDriftReport, formatHealthReport, quarantineNotice, safeMcpFileName, scoreMcpHealth, } from "./mcp-doctor";
238
+ // 0.3.0 memory release (design §3.4) — `crewhaus memory list|show|forget|sweep`
239
+ // helpers + the `crewhaus migrate memories` schema-v2 backfill.
240
+ import { MEMORIES_SUBDIR, formatMigrateMemoriesReport, listMemorySpecs, migrateMemories, renderMemoryList, renderMemoryShow, resolveMemorySpec, } from "./memory-cli";
223
241
  // Item 24 — market scan + doctor --models + pricing sync core, in a
224
242
  // side-effect-free module (this entry file runs an argv switch on import).
225
243
  import { buildMarketScan, buildModelChecks, buildProposalArtifact, buildScanCandidates, writeModelField, } from "./model-scan";
@@ -266,6 +284,11 @@ import { RetireError, buildRetirementPlan, formatPlan, formatRetirementResult, r
266
284
  // projection + $/score ranking); side-effect-free so it is unit-testable.
267
285
  import { buildRightSizeReport, enumerateSlotCandidates, } from "./right-size";
268
286
  import { runRoute } from "./route";
287
+ // v0.3.0 Goal 6 — canonical terminal-failure rendering: die() and the
288
+ // `crewhaus run` failure path both route through renderCliFailure so a
289
+ // RunFailedError prints its structured report + coded exit while every
290
+ // other fatal keeps the classic `crewhaus: <message>` one-liner + exit 1.
291
+ import { CONTINUE_NOTE, renderCliFailure } from "./run-failure";
269
292
  // Item 13 — `crewhaus scaffold-evals` + `init --with-evals`: day-one eval
270
293
  // assets generated from the spec (deterministic template / one-shot model
271
294
  // sample stubs + a single spec-goal llm_judge or floor grader), in a
@@ -303,6 +326,9 @@ import { buildSloSink, intakeGatePayload, lastKnownGoodFromAuditRecords } from "
303
326
  // import-time side effects so it is unit-testable (this entry file runs an
304
327
  // argv switch on import).
305
328
  import { StateBackupError, createStateBackup, defaultBackupFileName, mergeAllFromArchive, mergeFeedbackFromArchive, parseExcludeGlobs, restoreStateArchive, } from "./state-backup";
329
+ // v0.3.0 Goal 3 — `doctor --probe`'s thredz check (wiki_stats round-trip
330
+ // through the spec's synthesized/user-declared thredz MCP server).
331
+ import { probeThredz, thredzProbeTarget, thredzProbeToCheck } from "./thredz-probe";
306
332
  // Item 18 — `crewhaus tools` namespace (list/suggest/audit + the loadToolMap
307
333
  // ↔ BUILTIN_TOOL_MAP sync floor), in a side-effect-free module so it is
308
334
  // unit-testable (this entry file runs an argv switch on import).
@@ -324,6 +350,9 @@ import { cliVersion } from "./version";
324
350
  // (this entry file reads the replay JSONLs + writes the report).
325
351
  import { DEFAULT_VOICE_THRESHOLDS, VoiceEvalError, aggregateVoiceEval, gradeVoiceSession, parseReplayLog, renderVoiceReport, } from "./voice-eval";
326
352
  import { createWatchController, formatCycleLine } from "./watch";
353
+ // 0.3.0 memory release (design §3.1/§3.2, PR 9) — `crewhaus wiki
354
+ // list|show|search|stats` helpers over @crewhaus/wiki-store.
355
+ import { WIKI_SUBDIR, listWikiSpecs, renderWikiList, renderWikiSearch, renderWikiShow, renderWikiStats, resolveWikiSpec, } from "./wiki-cli";
327
356
  /**
328
357
  * crewhaus — slice-scope CLI.
329
358
  * Subcommands:
@@ -465,6 +494,13 @@ const INIT_SCHEMA = {
465
494
  // prefill the model from what's reachable.
466
495
  { name: "interactive", short: "i", takesValue: false },
467
496
  { name: "detect", takesValue: false },
497
+ // v0.3.0 §2.9 — restart a persisted interview session (the conversation
498
+ // is a runChatLoop session; a crashed/interrupted interview resumes with
499
+ // its ledger and transcript intact).
500
+ { name: "resume", takesValue: false },
501
+ // v0.3.0 §2.9 — skip the conversational interview (scripted
502
+ // questionnaire), mirroring the non-TTY behavior.
503
+ { name: "yes", short: "y", takesValue: false },
468
504
  { name: "help", short: "h" },
469
505
  ],
470
506
  };
@@ -546,6 +582,12 @@ const DOCTOR_SCHEMA = {
546
582
  // tools scope:external, append commented .env stubs). Dry-run is the
547
583
  // DEFAULT: without --fix, doctor prints the diff it WOULD apply.
548
584
  { name: "fix", takesValue: false },
585
+ // v0.3.0 Goal 6 — `--probe`: opt-in ~1-token live call per configured
586
+ // provider, catching present-but-invalid keys and unfunded accounts
587
+ // BEFORE a long run dies on them. Distinct from --detect's --no-probe
588
+ // (a localhost reachability knob): this one spends real (fractional-
589
+ // cent) tokens, so it is never on by default.
590
+ { name: "probe", takesValue: false },
549
591
  { name: "help", short: "h" },
550
592
  ],
551
593
  };
@@ -958,6 +1000,47 @@ const SESSIONS_SCHEMA = {
958
1000
  { name: "help", short: "h" },
959
1001
  ],
960
1002
  };
1003
+ // 0.3.0 memory release (design §3.4) — `crewhaus memory list|show|forget|sweep`.
1004
+ const MEMORY_SCHEMA = {
1005
+ flags: [
1006
+ // Which spec's memory file to operate on. `list`/`sweep` default to every
1007
+ // file under .crewhaus/memories/; `show`/`forget` auto-resolve only when a
1008
+ // single file exists.
1009
+ { name: "spec", takesValue: true },
1010
+ // forget — a text query instead of an exact id (forgets EVERY match).
1011
+ { name: "query", short: "q", takesValue: true },
1012
+ // forget / sweep --compact — skip the confirmation prompt (scripted/CI).
1013
+ { name: "yes", takesValue: false },
1014
+ // sweep — additionally rewrite the file(s) dropping tombstoned/expired
1015
+ // lines (atomic tmp+rename; the growth-bounding compaction).
1016
+ { name: "compact", takesValue: false },
1017
+ { name: "help", short: "h" },
1018
+ ],
1019
+ };
1020
+ // 0.3.0 memory release — `crewhaus migrate memories [--dry-run]`: backfill
1021
+ // schemaVersion + derivable provenance onto v1 entries, stamp .crewhaus/meta.json.
1022
+ const MIGRATE_MEMORIES_SCHEMA = {
1023
+ flags: [
1024
+ { name: "dry-run", takesValue: false },
1025
+ { name: "help", short: "h" },
1026
+ ],
1027
+ };
1028
+ // 0.3.0 memory release (design §3.1, PR 9) — `crewhaus wiki list|show|search|stats`.
1029
+ const WIKI_SCHEMA = {
1030
+ flags: [
1031
+ // Which spec's wiki to operate on. `list`/`stats` default to every wiki
1032
+ // under .crewhaus/wiki/; `show`/`search` auto-resolve only when a single
1033
+ // wiki exists.
1034
+ { name: "spec", takesValue: true },
1035
+ // list — filter to articles carrying EVERY listed tag (comma-separated).
1036
+ { name: "tags", takesValue: true },
1037
+ // list — filter by article status (draft|published|review|archived|all).
1038
+ { name: "status", takesValue: true },
1039
+ // search — max results to print. Default 10.
1040
+ { name: "k", short: "k", takesValue: true },
1041
+ { name: "help", short: "h" },
1042
+ ],
1043
+ };
961
1044
  // Item 12 — the dataset-registry CLI face (`datasets list|get|put`).
962
1045
  const DATASETS_SCHEMA = {
963
1046
  flags: [
@@ -1452,6 +1535,22 @@ function usageText() {
1452
1535
  " lessons update [--sessions N|all] mine corrections + failures into an auto-loaded LESSONS.md (#56)",
1453
1536
  " [--low-score F] [-o <LESSONS.md>] + per-user prefs under .crewhaus/preferences/",
1454
1537
  " sessions summarize [--before <date>] summarize sessions into a durable index before TTL eviction (#57)",
1538
+ " memory list [--spec <name>] inspect the per-spec fact stores: id/age/tags/provenance/status",
1539
+ " memory show <mem_id> full detail for one memory (provenance, expiry, supersededBy)",
1540
+ " memory forget <mem_id>|--query <q> explicitly forget memories (append-only supersede tombstones;",
1541
+ " [--spec <name>] [--yes] a query forgets EVERY match; prompts unless --yes)",
1542
+ " memory sweep [--compact] [--yes] tombstone TTL-expired memories; --compact rewrites the file",
1543
+ " dropping dead lines (atomic; the growth-bounding compaction)",
1544
+ " migrate memories [--dry-run] backfill the v2 memory schema + provenance; stamps .crewhaus/meta.json",
1545
+ " wiki list [--spec <name>] inspect the per-spec local wikis, stalest first (versions, signals,",
1546
+ " [--tags <t1,t2>] [--status <s>] tags; articles live under .crewhaus/wiki/<spec>/articles/)",
1547
+ " wiki show <slug> full frontmatter + body for one article (priors in versions/<slug>/)",
1548
+ " wiki search <q> [-k <n>] BM25 keyword search over a spec's articles",
1549
+ " wiki stats corpus health: articles/status/versions/tags/verified/links",
1550
+ " dream [run] <spec.yaml> [--force] scheduled memory consolidation (§6): deterministic pass + budget-",
1551
+ " capped model synthesis per memory.dream (cron-safe, window-idempotent)",
1552
+ " dream status <spec.yaml> schedule state + next due (.crewhaus/dream/<spec>/state.json)",
1553
+ " dream init <spec.yaml> [--force] scaffold the nightly consolidation cron (crewhaus-dream.yml)",
1455
1554
  " [--evicted] [--ttl-days N] --evicted indexes each session just before it is deleted",
1456
1555
  " datasets list all registered datasets + versions (Section 29)",
1457
1556
  " datasets get <name>[@version] print a dataset's samples as JSONL",
@@ -1547,9 +1646,17 @@ function help() {
1547
1646
  process.stdout.write(usageText());
1548
1647
  process.exit(0);
1549
1648
  }
1649
+ /**
1650
+ * Fatal-error exit. A plain string (or any non-RunFailed CrewhausError)
1651
+ * keeps the classic one-liner `crewhaus: <message>` + exit 1; a
1652
+ * `RunFailedError` (v0.3.0 Goal 6) renders its structured report via
1653
+ * `formatRunFailure()` and exits with the report's coded status, so
1654
+ * callers can simply `die(err)` and the taxonomy does the rest.
1655
+ */
1550
1656
  function die(message) {
1551
- process.stderr.write(`crewhaus: ${message}\n`);
1552
- process.exit(1);
1657
+ const rendered = renderCliFailure(message);
1658
+ process.stderr.write(`${rendered.text}\n`);
1659
+ process.exit(rendered.exitCode);
1553
1660
  }
1554
1661
  function printVersion() {
1555
1662
  // Resolution (embedded --define constant → apps/cli/package.json) lives in
@@ -2527,17 +2634,24 @@ function createLineReader() {
2527
2634
  };
2528
2635
  }
2529
2636
  /**
2530
- * Item 39 — `crewhaus init --interactive`. Promotes the demos harness-designer
2531
- * into core: interview the user and emit a `parseSpec`-validated crewhaus.yaml.
2637
+ * Item 39 / v0.3.0 §2.9 — `crewhaus init --interactive`. Interview the user
2638
+ * and emit a `parseSpec`-validated crewhaus.yaml.
2532
2639
  *
2533
- * Two paths, chosen by credential availability (like the merged scaffold path):
2534
- * - Credentials present → the MODEL interview. `runInterview` (side-effect-
2535
- * free) drives a validate-and-retry loop: the model calls `emit_spec` with
2536
- * a draft, `parseSpec` validates it in-process, and any structured error is
2537
- * fed back for a corrected re-emit. No demos dependency — the shape
2538
- * guidance is bundled in `init-interactive.ts`.
2539
- * - No credentials a scripted stdin questionnaire (name/shape/model/tools)
2540
- * that still emits a `parseSpec`-validated spec via `buildScriptedSpec`.
2640
+ * Three paths:
2641
+ * - Credentials + a TTY → the CONVERSATIONAL interview (PR 18): a
2642
+ * persisted, resumable `runChatLoop` session with `ask_user` +
2643
+ * `emit_spec` and no forced toolChoice, the continuity fabric on
2644
+ * (requirements ledger, focus pinning, handoff) spec-scoped as
2645
+ * `init-<dirname>` under the target directory. Validation errors return
2646
+ * to the conversation as tool errors; a terminal failure prints the
2647
+ * classified FailureReport plus the saved-interview resume hint;
2648
+ * `--resume` restarts the session with the ledger intact.
2649
+ * - Credentials but no TTY (or `--yes`) → the conversational path is
2650
+ * refused (same convention as the other interactive verbs) and the
2651
+ * scripted questionnaire runs instead.
2652
+ * - No credentials → the scripted stdin questionnaire
2653
+ * (name/shape/model/tools), byte-identical to pre-0.3.0, still emitting
2654
+ * a `parseSpec`-validated spec via `buildScriptedSpec`.
2541
2655
  *
2542
2656
  * Reuses `runInit`'s exists-check/refuse-overwrite + standalone-dir guidance,
2543
2657
  * and composes with `--detect` (#40) to prefill the default model from a
@@ -2545,12 +2659,14 @@ function createLineReader() {
2545
2659
  */
2546
2660
  async function runInitInteractive(args) {
2547
2661
  if (args.flags["help"]) {
2548
- process.stdout.write("usage: crewhaus init --interactive [name] [--detect]\n" +
2549
- " Interview-driven spec authoring. With credentials, an agent interviews you\n" +
2550
- " and emits a validated crewhaus.yaml (every draft is checked against the live\n" +
2551
- " spec schema and retried on error). Without credentials, a scripted\n" +
2662
+ process.stdout.write("usage: crewhaus init --interactive [name] [--detect] [--resume] [--yes]\n" +
2663
+ " Interview-driven spec authoring. With credentials and a TTY, an agent runs a\n" +
2664
+ " real multi-turn interview (persisted + resumable; every draft is validated\n" +
2665
+ " against the live spec schema in-conversation). Without credentials, a scripted\n" +
2552
2666
  " questionnaire (name/shape/model/tools) still emits a validated spec.\n" +
2553
- " --detect prefill the default model from a reachable local endpoint/provider.\n");
2667
+ " --detect prefill the default model from a reachable local endpoint/provider.\n" +
2668
+ " --resume continue a saved interview session where it stopped.\n" +
2669
+ " --yes skip the conversation; use the scripted questionnaire.\n");
2554
2670
  return;
2555
2671
  }
2556
2672
  const nameArg = args.positional[0];
@@ -2571,35 +2687,70 @@ async function runInitInteractive(args) {
2571
2687
  if (prefill !== undefined)
2572
2688
  defaultModel = prefill;
2573
2689
  }
2574
- // Credential-gated path selection: try to resolve the default model's adapter.
2575
- // A resolution failure (no credentials / missing optional adapter) degrades
2576
- // to the scripted questionnaire.
2690
+ // Credential-gated path selection: try to resolve the default model's
2691
+ // adapter. A resolution failure (no credentials / missing optional adapter)
2692
+ // degrades to the scripted questionnaire — byte-identical messaging.
2577
2693
  const interviewer = await tryBuildInterviewer(defaultModel);
2578
- const reader = createLineReader();
2694
+ const conversational = conversationalPathAllowed({
2695
+ stdinIsTTY: process.stdin.isTTY === true,
2696
+ assumeYes: args.flags["yes"] === true,
2697
+ });
2579
2698
  let yaml;
2580
- try {
2581
- if (interviewer !== undefined) {
2582
- process.stdout.write(`interactive spec authoring (model: ${interviewer.modelId}). Describe the agent you want to build.\n`);
2583
- const description = await reader.ask("> ");
2584
- if (description === "")
2585
- die("no description given aborting");
2586
- const result = await runInterview({
2587
- proposeSpec: (feedback) => interviewer.propose(description, feedback),
2699
+ let mappingLines = [];
2700
+ if (interviewer !== undefined && conversational.allowed) {
2701
+ // v0.3.0 §2.9 the conversational interview owns stdin via runChatLoop's
2702
+ // REPL (no line reader here: a second readline would steal input).
2703
+ process.stdout.write(args.flags["resume"] === true
2704
+ ? `interactive spec authoring (model: ${interviewer.modelId})resuming your saved interview.\n`
2705
+ : `interactive spec authoring (model: ${interviewer.modelId}). Describe the agent you want to build.\n`);
2706
+ try {
2707
+ const result = await runConversationalInterview({
2708
+ targetDir,
2709
+ specName: `init-${basename(targetDir)}`,
2710
+ model: defaultModel,
2711
+ resume: args.flags["resume"] === true,
2588
2712
  });
2589
2713
  yaml = result.yaml;
2590
- process.stdout.write(`validated after ${result.attempts} draft(s).\n`);
2714
+ mappingLines = result.mappingLines;
2591
2715
  }
2592
- else {
2593
- process.stdout.write("no model credentials detected falling back to the scripted questionnaire.\n");
2594
- yaml = (await runScriptedQuestionnaire({ reader, specName, defaultModel })).yaml;
2716
+ catch (err) {
2717
+ // A classified terminal failure (billing/auth/token exhaustion) prints
2718
+ // the PR-3 FailureReport with the saved-interview hint and the coded
2719
+ // exit — the session + ledger are on disk, `--resume` continues.
2720
+ if (err instanceof RunFailedError) {
2721
+ const rendered = renderCliFailure(err, { notes: [INIT_INTERVIEW_SAVED_NOTE] });
2722
+ process.stderr.write(`${rendered.text}\n`);
2723
+ process.exit(rendered.exitCode);
2724
+ }
2725
+ throw err;
2595
2726
  }
2596
2727
  }
2597
- finally {
2598
- reader.close();
2728
+ else {
2729
+ if (args.flags["resume"] === true) {
2730
+ die("crewhaus init --interactive --resume needs the conversational interview (model credentials and an interactive terminal)");
2731
+ }
2732
+ const reader = createLineReader();
2733
+ try {
2734
+ if (interviewer === undefined) {
2735
+ process.stdout.write("no model credentials detected — falling back to the scripted questionnaire.\n");
2736
+ }
2737
+ else {
2738
+ process.stdout.write(`${conversational.reason ?? "conversational interview unavailable"} — falling back to the scripted questionnaire.\n`);
2739
+ }
2740
+ yaml = (await runScriptedQuestionnaire({ reader, specName, defaultModel })).yaml;
2741
+ }
2742
+ finally {
2743
+ reader.close();
2744
+ }
2599
2745
  }
2600
2746
  mkdirSync(targetDir, { recursive: true });
2601
2747
  writeFileSync(targetFile, yaml);
2602
2748
  process.stdout.write(`wrote ${targetFile}\n`);
2749
+ // v0.3.0 §2.9 — the REQ → spec-field mapping the model produced before
2750
+ // emit_spec, surfaced as the interview's closing summary.
2751
+ if (mappingLines.length > 0) {
2752
+ process.stdout.write(`requirements → spec mapping (from the interview):\n${mappingLines.map((l) => ` ${l}`).join("\n")}\n`);
2753
+ }
2603
2754
  const rel = relative(process.cwd(), targetDir);
2604
2755
  const cd = rel === "" ? "" : `cd ${rel} && `;
2605
2756
  process.stdout.write(`next: ${cd}crewhaus run crewhaus.yaml\n`);
@@ -2639,38 +2790,19 @@ async function runScriptedQuestionnaire(opts) {
2639
2790
  return buildScriptedSpec(answers);
2640
2791
  }
2641
2792
  /**
2642
- * Item 39 attempt to build the model interviewer for `model`. Resolves the
2643
- * adapter via the same `resolveModel` path the scaffold-evals/mutator use;
2644
- * returns undefined when no credentials/adapter are available so the caller
2645
- * degrades to the scripted questionnaire. The returned `propose` closure runs
2646
- * one interview turn: it sends the system prompt + description + the running
2647
- * validation-feedback log and returns the `emit_spec` YAML (or undefined).
2793
+ * Item 39 / v0.3.0 §2.9 credential gate for the conversational interview.
2794
+ * Resolves the model's adapter via the same `resolveModel` path the
2795
+ * scaffold-evals/mutator use; returns undefined when no credentials/adapter
2796
+ * are available so the caller degrades to the scripted questionnaire. The
2797
+ * interview itself runs on `runChatLoop` (init-conversation.ts), which
2798
+ * resolves the model again through the normal router path — the single-shot
2799
+ * forced-`emit_spec` propose closure this used to carry is gone (PR 18).
2648
2800
  */
2649
2801
  async function tryBuildInterviewer(model) {
2650
2802
  try {
2651
2803
  const { resolveModel } = await import("@crewhaus/model-router");
2652
- const { collectFinalMessage, extractToolUse } = await import("@crewhaus/adapter-anthropic");
2653
2804
  const resolution = await resolveModel(model);
2654
- const system = buildInterviewSystemPrompt();
2655
- const propose = async (desc, feedback) => {
2656
- const feedbackText = feedback.length > 0
2657
- ? `\n\nYour previous draft(s) failed validation with:\n${feedback
2658
- .map((f) => `- ${f}`)
2659
- .join("\n")}\nEmit a corrected spec.`
2660
- : "";
2661
- const final = await collectFinalMessage(resolution.adapter.stream({
2662
- model: resolution.modelId,
2663
- system: [{ type: "text", text: system }],
2664
- messages: [{ role: "user", content: `Build this agent: ${desc}${feedbackText}` }],
2665
- tools: [EMIT_SPEC_TOOL],
2666
- toolChoice: { type: "tool", name: EMIT_SPEC_TOOL.name },
2667
- maxTokens: 4096,
2668
- }));
2669
- const toolUse = extractToolUse(final, EMIT_SPEC_TOOL.name);
2670
- const yaml = toolUse?.input?.yaml;
2671
- return typeof yaml === "string" && yaml.length > 0 ? yaml : undefined;
2672
- };
2673
- return { modelId: resolution.modelId, propose };
2805
+ return { modelId: resolution.modelId };
2674
2806
  }
2675
2807
  catch {
2676
2808
  return undefined;
@@ -3020,16 +3152,33 @@ async function runRunCli(args, ir, specPath) {
3020
3152
  // Item 38 — synthetic notice appended to the agent instructions when the MCP
3021
3153
  // auto-quarantine withdraws a server's tools (built in the block below).
3022
3154
  let mcpQuarantineNotice;
3155
+ // v0.3.0 Goal 3 — the thredz server (synthesized by the compiler, or the
3156
+ // user's own vendored entry) boots through connectThredz: its wiki+goals
3157
+ // tools land under their BARE names (§4.3, one vocabulary across
3158
+ // backends) and boot failure DEGRADES (null → wireMemory falls back to
3159
+ // local files with a warning) instead of failing the run.
3160
+ let thredzConn = null;
3161
+ const thredzWired = ir.thredz !== undefined && ir.mcp_servers["thredz"] !== undefined;
3023
3162
  if (Object.keys(ir.mcp_servers).length > 0) {
3024
3163
  const host = new McpHost({ logger });
3025
3164
  mcpHost = host;
3026
3165
  for (const [name, cfg] of Object.entries(ir.mcp_servers)) {
3027
- host.addServer(name, cfg);
3166
+ // 0.3.0 — env/header values are IrSecretRef; resolve them from the
3167
+ // interpreter process's environment (fail-fast, names the variable).
3168
+ host.addServer(name, resolveMcpServerConfig(cfg, { name }));
3028
3169
  }
3029
3170
  const tempCatalog = new ToolCatalog();
3030
3171
  for (const t of tools)
3031
3172
  tempCatalog.register(t);
3032
- await Promise.all(Object.keys(ir.mcp_servers).map((name) => registerMcpServer(host, name, tempCatalog, {
3173
+ if (thredzWired) {
3174
+ thredzConn = await connectThredz(host, tempCatalog, {
3175
+ log: (line) => process.stdout.write(line),
3176
+ ...(ir.thredz?.agentName !== undefined ? { agentName: ir.thredz.agentName } : {}),
3177
+ });
3178
+ }
3179
+ await Promise.all(Object.keys(ir.mcp_servers)
3180
+ .filter((name) => !(thredzWired && name === "thredz"))
3181
+ .map((name) => registerMcpServer(host, name, tempCatalog, {
3033
3182
  onRegister: ({ fullName }) => process.stdout.write(`[mcp] registered ${fullName}\n`),
3034
3183
  })));
3035
3184
  tools = tempCatalog.list().slice();
@@ -3134,11 +3283,77 @@ async function runRunCli(args, ir, specPath) {
3134
3283
  // a synthetic `Skill(name)` tool is appended to the tool list so the
3135
3284
  // model can lazily fetch each skill's body.
3136
3285
  const cwd = process.cwd();
3137
- const [hooks, skills, slashCommands] = await Promise.all([
3286
+ const [hooks, discoveredSkills, discoveredCommands] = await Promise.all([
3138
3287
  loadHooks({ cwd }),
3139
3288
  discoverSkills({ cwd }),
3140
3289
  loadCommands({ cwd }),
3141
3290
  ]);
3291
+ // Feature #53 / v0.3.0 PR 11 — memory + continuity, wired through the
3292
+ // composition root. The SAME `wireMemory(fragment, deps)` call a compiled
3293
+ // bundle emits runs here: it constructs the stores, registers the tools
3294
+ // (into this run's local tool list via the registrar shim), and returns
3295
+ // the runChatLoop seams. Continuity is DEFAULT-ON for the cli target —
3296
+ // `ir.continuity` is present unless the spec opted out with
3297
+ // `continuity: false` (the compiler dropped the key at lower time, so
3298
+ // opted-out specs run exactly the pre-0.3.0 path — no flag needed).
3299
+ // When continuity is on, the composition root owns the skill/command
3300
+ // surface (builtin `continuity` skill + commands merged at LOWEST
3301
+ // precedence with ~/.crewhaus + project entries) and the interpreter
3302
+ // adopts it wholesale, exactly like a compiled bundle — registering the
3303
+ // Skill tool from its own discovery would strand the builtin skill.
3304
+ let skills = discoveredSkills;
3305
+ let slashCommands = discoveredCommands;
3306
+ let memoryRunOpt;
3307
+ let continuityRunOpt;
3308
+ if ((ir.memory !== undefined && ir.memory.enabled !== false) ||
3309
+ ir.continuity !== undefined ||
3310
+ ir.thredz !== undefined) {
3311
+ const wiredMemory = await wireMemory(memoryFragmentFromIr(ir), {
3312
+ catalog: {
3313
+ register: (tool) => {
3314
+ tools.push(tool);
3315
+ },
3316
+ },
3317
+ cwd: process.cwd(),
3318
+ log: (line) => process.stdout.write(line),
3319
+ // v0.3.0 Goal 3 — the live connection (or null after a boot failure,
3320
+ // which wireMemory degrades from). Absent when thredz is off.
3321
+ ...(ir.thredz !== undefined ? { thredz: thredzConn } : {}),
3322
+ // v0.3.0 Goal 2 (§3.3, PR 17) — the first-class exam: `/exam` drives
3323
+ // the run_exam tool, which invokes the eval library programmatically
3324
+ // (no agent-shells-to-`crewhaus` hack, no Bash permission needed).
3325
+ ...(ir.learning?.exam !== undefined
3326
+ ? {
3327
+ examRunner: createExamRunner({
3328
+ specName: ir.name,
3329
+ model,
3330
+ instructions: ir.agent.instructions,
3331
+ fragment: memoryFragmentFromIr(ir),
3332
+ cwd: process.cwd(),
3333
+ ...(ir.thredz !== undefined ? { thredz: thredzConn } : {}),
3334
+ }),
3335
+ }
3336
+ : {}),
3337
+ });
3338
+ memoryRunOpt = wiredMemory.options.memory;
3339
+ continuityRunOpt = wiredMemory.options.continuity;
3340
+ if (wiredMemory.options.skills !== undefined)
3341
+ skills = wiredMemory.options.skills;
3342
+ if (wiredMemory.options.slashCommands !== undefined) {
3343
+ slashCommands = wiredMemory.options.slashCommands;
3344
+ }
3345
+ // v0.3.0 PR 14 (§6.3) — boot-time dream catch-up: when the schedule is
3346
+ // overdue, run the DETERMINISTIC phase only (sub-second, zero spend).
3347
+ // Unattended model spend is never a side effect of starting a session —
3348
+ // the full consolidation stays behind `crewhaus dream`.
3349
+ if (ir.memory?.dream !== undefined) {
3350
+ const dreamNote = await runDreamBootCatchUp(memoryFragmentFromIr(ir), {
3351
+ cwd: process.cwd(),
3352
+ });
3353
+ if (dreamNote !== null)
3354
+ process.stdout.write(`${dreamNote}\n`);
3355
+ }
3356
+ }
3142
3357
  if (skills.length > 0) {
3143
3358
  tools.push(createSkillTool(skills));
3144
3359
  process.stdout.write(`[skills] ${skills.length} available: ${skills.map((s) => s.name).join(", ")}\n`);
@@ -3298,48 +3513,6 @@ async function runRunCli(args, ir, specPath) {
3298
3513
  : {}),
3299
3514
  });
3300
3515
  }
3301
- // Feature #53 — first-class `memory:` block. Its presence wires Remember/
3302
- // Recall into the tool list (no hand-editing) and — via the auto-* switches
3303
- // — the runtime's auto-recall (system-prompt injection) and auto-capture
3304
- // (summarize durable outcomes into the store at teardown). Mirrors the
3305
- // codegen registration in @crewhaus/target-cli.
3306
- let memoryRunOpt;
3307
- if (ir.memory !== undefined && ir.memory.enabled !== false) {
3308
- const memoryStore = createMemoryStore({ specName: ir.name });
3309
- const memoryBundle = createMemoryTools({ specName: ir.name, store: memoryStore });
3310
- tools.push(memoryBundle.remember, memoryBundle.recall);
3311
- const decision = deriveMemoryDecision(ir.memory, Number.MAX_SAFE_INTEGER);
3312
- process.stdout.write(`[memory] Remember/Recall wired (autoRecall=${decision.recall}, autoCapture=${ir.memory.autoCapture === true})\n`);
3313
- memoryRunOpt = {
3314
- ...(decision.recall
3315
- ? {
3316
- autoRecall: true,
3317
- recallK: decision.recallK,
3318
- recall: async (query, k) => {
3319
- const results = await memoryStore.recall(query, k);
3320
- return results.map((r) => r.entry.text);
3321
- },
3322
- }
3323
- : {}),
3324
- ...(ir.memory.autoCapture === true
3325
- ? {
3326
- autoCapture: true,
3327
- onCapture: async (completedTurns, sessionId) => {
3328
- const { capture } = deriveMemoryDecision(ir.memory, completedTurns);
3329
- if (!capture)
3330
- return;
3331
- const events = parseJsonlObjects(readFileSync(sessionJsonlPath(sessionId), "utf-8"));
3332
- const turns = deriveTurns(events);
3333
- const facts = summarizeDurableFacts(turns);
3334
- const written = await captureFacts(memoryStore, facts, ["auto-capture", sessionId]);
3335
- if (written.length > 0) {
3336
- process.stdout.write(`[memory] auto-captured ${written.length} durable fact(s) into ${memoryStore.path()}\n`);
3337
- }
3338
- },
3339
- }
3340
- : {}),
3341
- };
3342
- }
3343
3516
  // Item #56 — resolve the current user's preference file (flag > env) so the
3344
3517
  // runtime injects `.crewhaus/preferences/<user>.md` at run start. Absent when
3345
3518
  // no user is identified or the file doesn't exist yet.
@@ -3393,6 +3566,11 @@ async function runRunCli(args, ir, specPath) {
3393
3566
  }
3394
3567
  }
3395
3568
  let oneShotResult;
3569
+ // v0.3.0 Goal 6 — a RunFailedError is remembered (not rethrown) so the
3570
+ // finally still disconnects MCP servers, then rendered below with the
3571
+ // resume hint + the report's coded exit. Everything else propagates to
3572
+ // the `case "run"` catch → die() one-liner, exactly as before.
3573
+ let runFailure;
3396
3574
  try {
3397
3575
  oneShotResult = await runChatLoop({
3398
3576
  model,
@@ -3478,16 +3656,45 @@ async function runRunCli(args, ir, specPath) {
3478
3656
  // (gated by CREWHAUS_INCIDENTS inside runtime-core).
3479
3657
  incidentSpec: { name: ir.name },
3480
3658
  ...(memoryRunOpt !== undefined ? { memory: memoryRunOpt } : {}),
3659
+ // v0.3.0 Goal 1 — the continuity seam (plan tail + requirements ledger
3660
+ // + teardown handoff), default-on unless the spec said
3661
+ // `continuity: false`.
3662
+ ...(continuityRunOpt !== undefined ? { continuity: continuityRunOpt } : {}),
3481
3663
  // Item #56 — inject the current user's preference file at run start (via
3482
3664
  // the project-memory auto-load path). LESSONS.md is auto-loaded already
3483
3665
  // as a canonical memory file, so no extra wiring is needed for it.
3484
3666
  ...(preferenceFiles.length > 0 ? { preferenceFiles } : {}),
3485
3667
  });
3486
3668
  }
3669
+ catch (err) {
3670
+ if (!(err instanceof RunFailedError))
3671
+ throw err;
3672
+ runFailure = err;
3673
+ }
3487
3674
  finally {
3488
3675
  if (mcpHost)
3489
3676
  await mcpHost.disconnectAll();
3490
3677
  }
3678
+ // v0.3.0 Goal 6 — render the classified terminal report (billing/auth/
3679
+ // rate-limit/…) with the resume hint when this spec has a persisted
3680
+ // session, then exit with the report's coded status. The design §8.2
3681
+ // out-of-funding example ends exactly like this.
3682
+ if (runFailure !== undefined) {
3683
+ const notes = [];
3684
+ try {
3685
+ const store = createSessionStore();
3686
+ const sessions = await store.list();
3687
+ if (sessions.some((s) => s.name === ir.name)) {
3688
+ notes.push(CONTINUE_NOTE);
3689
+ }
3690
+ }
3691
+ catch {
3692
+ // Best-effort: the resume hint must never mask the real failure.
3693
+ }
3694
+ const rendered = renderCliFailure(runFailure, { notes });
3695
+ process.stderr.write(`${rendered.text}\n`);
3696
+ process.exit(rendered.exitCode);
3697
+ }
3491
3698
  // One-shot (`--prompt`): emit the final assistant message to stdout so it
3492
3699
  // can be piped/captured. The REPL path streams as it goes and returns the
3493
3700
  // same trailing text, so only print it in one-shot mode.
@@ -3721,7 +3928,7 @@ function runContext(args) {
3721
3928
  async function runDoctor(args) {
3722
3929
  if (args.flags["help"]) {
3723
3930
  process.stdout.write("usage: crewhaus doctor [--philosophy-alignment [--json] [--baseline | --accept-baseline]]\n" +
3724
- " [--liveness] [--context-pressure [--sessions N]]\n" +
3931
+ " [--liveness] [--context-pressure [--sessions N]] [--probe]\n" +
3725
3932
  " --philosophy-alignment audit the codebase + examples against the three architectural pillars\n" +
3726
3933
  " --json persist findings (stable ids) to .crewhaus/scope-audit/<date>.json\n" +
3727
3934
  " and print the snapshot JSON (item 49)\n" +
@@ -3752,6 +3959,16 @@ async function runDoctor(args) {
3752
3959
  " mark outward tools scope:external via a CST spec-patch, and\n" +
3753
3960
  " append commented .env stubs. DRY-RUN IS THE DEFAULT — without\n" +
3754
3961
  " --fix, doctor prints the diff it WOULD apply.\n" +
3962
+ " --probe OPT-IN live credential probe: one ~1-token call per\n" +
3963
+ " configured provider (the cwd spec's model verbatim, plus a\n" +
3964
+ " cheap default for other providers with env set), catching\n" +
3965
+ " unfunded accounts (billing) and invalid keys (auth) before\n" +
3966
+ " a long run dies on them. Spends fractional-cent tokens, so\n" +
3967
+ " it is never on by default. Failures fail doctor (exit 1).\n" +
3968
+ " When the cwd spec carries `thredz:`, also runs one live\n" +
3969
+ " wiki_stats round-trip through the thredz MCP server —\n" +
3970
+ " disabled keys (thredz_billing) and plan caps (thredz_quota)\n" +
3971
+ " surface here instead of degrading a long run.\n" +
3755
3972
  "\n" +
3756
3973
  " Credential checks are model-aware: doctor parses the cwd crewhaus.yaml's\n" +
3757
3974
  " agent.model (claude-*, openai/*, gemini/*, bedrock/*, local/<m>@<url>) and\n" +
@@ -3815,6 +4032,30 @@ async function runDoctor(args) {
3815
4032
  if (specText !== undefined) {
3816
4033
  checks.push(...buildChannelEnvChecks(specText, process.env));
3817
4034
  }
4035
+ // v0.3.0 Goal 6 — `--probe`: opt-in ~1-token live call per configured
4036
+ // provider (the spec's model verbatim + cheap defaults for other
4037
+ // configured providers), catching unfunded/invalid keys pre-run. Failures
4038
+ // classify through recovery-engine (billing/auth/rate_limit) so the ✗
4039
+ // line names the fix class, and they gate doctor's exit like any check.
4040
+ if (args.flags["probe"]) {
4041
+ const plan = buildProbePlan(specModel, process.env);
4042
+ if (plan.length === 0) {
4043
+ process.stdout.write("~ live probe: no configured providers to probe\n");
4044
+ }
4045
+ else {
4046
+ checks.push(...probeResultsToChecks(await runProviderProbes(plan)));
4047
+ }
4048
+ // v0.3.0 Goal 3 (§4.4) — when the cwd spec carries `thredz:`, probe the
4049
+ // configured server with one live wiki_stats round-trip so a disabled
4050
+ // key (billing lapse) or plan cap surfaces BEFORE a long run degrades
4051
+ // on it. Classified through the same choke point the run path uses.
4052
+ if (specText !== undefined) {
4053
+ const thredzTarget = thredzProbeTarget(specText);
4054
+ if (thredzTarget !== undefined) {
4055
+ checks.push(thredzProbeToCheck(await probeThredz(thredzTarget)));
4056
+ }
4057
+ }
4058
+ }
3818
4059
  const bunCheck = checkBunVersion(Bun.version);
3819
4060
  checks.push({
3820
4061
  label: `Bun runtime (${Bun.version})`,
@@ -4241,6 +4482,9 @@ async function collectPhilosophyFindings() {
4241
4482
  { name: "compaction-autocompact", path: "packages/compaction-autocompact/src/index.ts" },
4242
4483
  { name: "federation-router", path: "packages/federation-router/src/index.ts" },
4243
4484
  { name: "channel-adapter-base", path: "packages/channel-adapter-base/src/index.ts" },
4485
+ // 0.3.0 memory release: recalled wiki bodies re-enter the model context
4486
+ // across a session boundary — classified at the "memory" origin.
4487
+ { name: "tool-wiki", path: "packages/tool-wiki/src/index.ts" },
4244
4488
  ];
4245
4489
  for (const site of boundarySites) {
4246
4490
  const filePath = join(process.cwd(), site.path);
@@ -8687,6 +8931,268 @@ async function runSessions(args) {
8687
8931
  }
8688
8932
  process.stdout.write(`[sessions] summarized ${indexed} session(s) into ${indexDir}\n`);
8689
8933
  }
8934
+ // -------- 0.3.0 memory release: crewhaus memory + migrate memories --------
8935
+ /**
8936
+ * `crewhaus memory list|show <id>|forget <id|--query <q>>|sweep [--compact]`
8937
+ * (design §3.4) — inspect and explicitly forget the per-spec fact stores
8938
+ * under `.crewhaus/memories/`. Forgetting is append-only (supersede
8939
+ * tombstones, never a hard delete); `sweep --compact` is the growth-bounding
8940
+ * rewrite. Destructive verbs prompt unless `--yes`. NOTE: `clear|restore`
8941
+ * (trash + undo) deliberately do NOT live here — they arrive with the
8942
+ * continuity store (design §2.6).
8943
+ */
8944
+ async function runMemory(args) {
8945
+ const action = args.positional[0];
8946
+ if (args.flags["help"] === true && action === undefined) {
8947
+ process.stdout.write("usage: crewhaus memory list [--spec <name>]\n" +
8948
+ " crewhaus memory show <mem_id> [--spec <name>]\n" +
8949
+ " crewhaus memory forget <mem_id> | --query <q> [--spec <name>] [--yes]\n" +
8950
+ " crewhaus memory sweep [--compact] [--spec <name>] [--yes]\n" +
8951
+ " Inspect + explicitly forget the per-spec memory stores under .crewhaus/memories/.\n" +
8952
+ " forget appends supersede tombstones (append-only; never a hard delete); a --query\n" +
8953
+ " forgets EVERY matching memory. sweep tombstones TTL-expired entries; --compact\n" +
8954
+ " additionally rewrites the file dropping dead lines (atomic tmp+rename).\n");
8955
+ return;
8956
+ }
8957
+ const memoriesDir = join(process.cwd(), MEMORIES_SUBDIR);
8958
+ const nowMs = Date.now();
8959
+ const specFlag = strFlag(args, "spec");
8960
+ const assumeYes = args.flags["yes"] === true;
8961
+ const storeFor = (specName) => createMemoryStore({ specName, rootDir: memoriesDir });
8962
+ const confirmOrDie = async (prompt) => {
8963
+ if (assumeYes)
8964
+ return true;
8965
+ if (process.stdin.isTTY !== true) {
8966
+ die(`memory ${action}: refusing a destructive operation without --yes in a non-TTY session`);
8967
+ }
8968
+ return await promptYesNo(prompt);
8969
+ };
8970
+ switch (action) {
8971
+ case "list": {
8972
+ const specs = specFlag !== undefined
8973
+ ? [resolveMemorySpec(memoriesDir, specFlag)]
8974
+ : listMemorySpecs(memoriesDir);
8975
+ if (specs.length === 0)
8976
+ die(`no memory files under ${memoriesDir}`);
8977
+ for (const spec of specs) {
8978
+ const items = await storeFor(spec).list();
8979
+ for (const line of renderMemoryList(spec, items, nowMs)) {
8980
+ process.stdout.write(`${line}\n`);
8981
+ }
8982
+ }
8983
+ return;
8984
+ }
8985
+ case "show": {
8986
+ const id = args.positional[1];
8987
+ if (id === undefined)
8988
+ die("usage: crewhaus memory show <mem_id> [--spec <name>]");
8989
+ const specs = specFlag !== undefined
8990
+ ? [resolveMemorySpec(memoriesDir, specFlag)]
8991
+ : listMemorySpecs(memoriesDir);
8992
+ for (const spec of specs) {
8993
+ const item = (await storeFor(spec).list()).find((i) => i.entry.id === id);
8994
+ if (item !== undefined) {
8995
+ process.stdout.write(`spec: ${spec}\n`);
8996
+ for (const line of renderMemoryShow(item))
8997
+ process.stdout.write(`${line}\n`);
8998
+ return;
8999
+ }
9000
+ }
9001
+ die(`no memory with id ${id} under ${memoriesDir}`);
9002
+ return;
9003
+ }
9004
+ case "forget": {
9005
+ const id = args.positional[1];
9006
+ const query = strFlag(args, "query");
9007
+ if ((id === undefined) === (query === undefined)) {
9008
+ die("memory forget: provide exactly one of <mem_id> or --query <q>");
9009
+ }
9010
+ if (id !== undefined && !/^mem_[0-9a-f]{16}$/.test(id)) {
9011
+ die(`memory forget: "${id}" is not a memory id (mem_…) — text goes via --query`);
9012
+ }
9013
+ const spec = resolveMemorySpec(memoriesDir, specFlag);
9014
+ const store = storeFor(spec);
9015
+ // Preview the exact match set forget() will tombstone: for an id the
9016
+ // live entry, for a query every positive BM25 match (recall's set).
9017
+ const items = await store.list();
9018
+ const matches = id !== undefined
9019
+ ? items.filter((i) => i.status === "live" && i.entry.id === id)
9020
+ : (await store.recall(query, 10_000)).map((r) => ({
9021
+ entry: r.entry,
9022
+ status: "live",
9023
+ }));
9024
+ if (matches.length === 0) {
9025
+ process.stdout.write(`[memory] nothing to forget — no live memory matched ${id ?? `"${query}"`} in ${spec}\n`);
9026
+ return;
9027
+ }
9028
+ process.stdout.write(`[memory] will forget ${matches.length} memory(ies) from ${spec}:\n`);
9029
+ for (const line of renderMemoryList(spec, matches, nowMs).slice(1)) {
9030
+ process.stdout.write(`${line}\n`);
9031
+ }
9032
+ const go = await confirmOrDie(`forget ${matches.length} memory(ies) from ${spec}? [y/N] `);
9033
+ if (!go) {
9034
+ process.stdout.write("[memory] aborted — nothing forgotten\n");
9035
+ return;
9036
+ }
9037
+ const forgotten = await store.forget((id ?? query), {
9038
+ reason: "crewhaus memory forget",
9039
+ });
9040
+ process.stdout.write(`[memory] forgot ${forgotten.length} memory(ies) (superseded tombstones in ${store.path()})\n`);
9041
+ return;
9042
+ }
9043
+ case "sweep": {
9044
+ const specs = specFlag !== undefined
9045
+ ? [resolveMemorySpec(memoriesDir, specFlag)]
9046
+ : listMemorySpecs(memoriesDir);
9047
+ if (specs.length === 0)
9048
+ die(`no memory files under ${memoriesDir}`);
9049
+ for (const spec of specs) {
9050
+ const result = await storeFor(spec).sweep();
9051
+ process.stdout.write(`[memory] ${spec}: swept ${result.swept} expired memory(ies), ${result.live} live\n`);
9052
+ }
9053
+ if (args.flags["compact"] === true) {
9054
+ const go = await confirmOrDie(`compact ${specs.length} memory file(s) (rewrites dropping tombstoned/expired lines)? [y/N] `);
9055
+ if (!go) {
9056
+ process.stdout.write("[memory] compact aborted — files untouched\n");
9057
+ return;
9058
+ }
9059
+ for (const spec of specs) {
9060
+ const result = await storeFor(spec).compact();
9061
+ process.stdout.write(`[memory] ${spec}: compacted — kept ${result.kept} line(s), dropped ${result.dropped}\n`);
9062
+ }
9063
+ }
9064
+ return;
9065
+ }
9066
+ default:
9067
+ die(`memory: unknown action "${action ?? ""}" — supported: list, show, forget, sweep`);
9068
+ }
9069
+ }
9070
+ /**
9071
+ * `crewhaus migrate memories [--dry-run]` — the schema-v2 backfill over
9072
+ * `.crewhaus/memories/*.jsonl` via the Section-28 migration-engine chain:
9073
+ * stamps `schemaVersion: 2` on v1 entries, derives `provenance.sessionId`
9074
+ * where the v1 auto-capture tags carry it, preserves every other line
9075
+ * verbatim, and records the store version in `.crewhaus/meta.json`.
9076
+ * Idempotent — a re-run migrates 0 entries.
9077
+ */
9078
+ async function runMigrateMemories(args) {
9079
+ if (args.flags["help"] === true) {
9080
+ process.stdout.write("usage: crewhaus migrate memories [--dry-run]\n" +
9081
+ " Backfill the v2 memory-entry schema (schemaVersion + derivable provenance)\n" +
9082
+ " onto .crewhaus/memories/*.jsonl and stamp .crewhaus/meta.json. Idempotent;\n" +
9083
+ " --dry-run reports the plan without writing.\n");
9084
+ return;
9085
+ }
9086
+ const report = migrateMemories(process.cwd(), {
9087
+ dryRun: args.flags["dry-run"] === true,
9088
+ });
9089
+ if (report.files.length === 0) {
9090
+ process.stdout.write(`[migrate] no memory files under ${join(process.cwd(), MEMORIES_SUBDIR)} — nothing to do\n`);
9091
+ return;
9092
+ }
9093
+ for (const line of formatMigrateMemoriesReport(report))
9094
+ process.stdout.write(`${line}\n`);
9095
+ }
9096
+ // -------- 0.3.0 memory release: crewhaus wiki --------
9097
+ /**
9098
+ * `crewhaus wiki list|show <slug>|search <q>|stats` (design §3.1/§3.2, PR 9)
9099
+ * — inspect the per-spec local wikis under `.crewhaus/wiki/`. Read-only
9100
+ * verbs only: articles are written by the wiki_write tool (versioned,
9101
+ * supersede-never-delete), `clear|restore` ride the continuity trash
9102
+ * machinery, and `push|pull --thredz` arrives with the Thredz PR.
9103
+ */
9104
+ async function runWiki(args) {
9105
+ const action = args.positional[0];
9106
+ if (args.flags["help"] === true && action === undefined) {
9107
+ process.stdout.write("usage: crewhaus wiki list [--spec <name>] [--tags <t1,t2>] [--status <s>]\n" +
9108
+ " crewhaus wiki show <slug> [--spec <name>]\n" +
9109
+ " crewhaus wiki search <q> [--spec <name>] [-k <n>]\n" +
9110
+ " crewhaus wiki stats [--spec <name>]\n" +
9111
+ " Inspect the per-spec local wikis under .crewhaus/wiki/ (versioned articles;\n" +
9112
+ " priors are kept under versions/<slug>/ — superseded, never deleted). list is\n" +
9113
+ " stalest-first (the REFLECT order); search is BM25 keyword ranking.\n");
9114
+ return;
9115
+ }
9116
+ const wikiDir = join(process.cwd(), WIKI_SUBDIR);
9117
+ const nowMs = Date.now();
9118
+ const specFlag = strFlag(args, "spec");
9119
+ const storeFor = (specName) => createWikiStore({ specName, rootDir: wikiDir });
9120
+ switch (action) {
9121
+ case "list": {
9122
+ const specs = specFlag !== undefined ? [resolveWikiSpec(wikiDir, specFlag)] : listWikiSpecs(wikiDir);
9123
+ if (specs.length === 0)
9124
+ die(`no wikis under ${wikiDir}`);
9125
+ const statusFlag = strFlag(args, "status");
9126
+ const status = statusFlag === "draft" ||
9127
+ statusFlag === "published" ||
9128
+ statusFlag === "review" ||
9129
+ statusFlag === "archived" ||
9130
+ statusFlag === "all"
9131
+ ? statusFlag
9132
+ : undefined;
9133
+ if (statusFlag !== undefined && status === undefined) {
9134
+ die(`wiki list: unknown --status "${statusFlag}" (draft|published|review|archived|all)`);
9135
+ }
9136
+ const tags = (strFlag(args, "tags") ?? "")
9137
+ .split(",")
9138
+ .map((t) => t.trim())
9139
+ .filter((t) => t !== "");
9140
+ for (const spec of specs) {
9141
+ const refs = await storeFor(spec).list({
9142
+ staleFirst: true,
9143
+ ...(status !== undefined ? { status } : {}),
9144
+ ...(tags.length > 0 ? { tags } : {}),
9145
+ });
9146
+ for (const line of renderWikiList(spec, refs, nowMs)) {
9147
+ process.stdout.write(`${line}\n`);
9148
+ }
9149
+ }
9150
+ return;
9151
+ }
9152
+ case "show": {
9153
+ const slug = args.positional[1];
9154
+ if (slug === undefined)
9155
+ die("usage: crewhaus wiki show <slug> [--spec <name>]");
9156
+ const specs = specFlag !== undefined ? [resolveWikiSpec(wikiDir, specFlag)] : listWikiSpecs(wikiDir);
9157
+ for (const spec of specs) {
9158
+ const article = await storeFor(spec).get(slug);
9159
+ if (article !== null) {
9160
+ process.stdout.write(`spec: ${spec}\n`);
9161
+ for (const line of renderWikiShow(article))
9162
+ process.stdout.write(`${line}\n`);
9163
+ return;
9164
+ }
9165
+ }
9166
+ die(`no wiki article with slug "${slug}" under ${wikiDir}`);
9167
+ return;
9168
+ }
9169
+ case "search": {
9170
+ const query = args.positional[1];
9171
+ if (query === undefined)
9172
+ die("usage: crewhaus wiki search <q> [--spec <name>] [-k <n>]");
9173
+ const k = Math.max(1, Number.parseInt(strFlag(args, "k") ?? "10", 10) || 10);
9174
+ const spec = resolveWikiSpec(wikiDir, specFlag);
9175
+ const refs = await storeFor(spec).search(query);
9176
+ for (const line of renderWikiSearch(spec, query, refs.slice(0, k))) {
9177
+ process.stdout.write(`${line}\n`);
9178
+ }
9179
+ return;
9180
+ }
9181
+ case "stats": {
9182
+ const specs = specFlag !== undefined ? [resolveWikiSpec(wikiDir, specFlag)] : listWikiSpecs(wikiDir);
9183
+ if (specs.length === 0)
9184
+ die(`no wikis under ${wikiDir}`);
9185
+ for (const spec of specs) {
9186
+ for (const line of renderWikiStats(spec, await storeFor(spec).stats())) {
9187
+ process.stdout.write(`${line}\n`);
9188
+ }
9189
+ }
9190
+ return;
9191
+ }
9192
+ default:
9193
+ die(`wiki: unknown action "${action ?? ""}" — supported: list, show, search, stats`);
9194
+ }
9195
+ }
8690
9196
  // -------- item 4: crewhaus graders suggest --------
8691
9197
  /**
8692
9198
  * Item 4 — `crewhaus graders suggest`: cluster the failure rationale
@@ -9276,10 +9782,14 @@ async function runFleet(args, action) {
9276
9782
  const assumeYes = args.flags["yes"] === true;
9277
9783
  // Production runner: spawn `crewhaus <argv>` in the harness dir (the cwd
9278
9784
  // every subcommand resolves `.crewhaus/` state from). No shell — an argv
9279
- // array through Bun.spawn. The child inherits this process's argv[0]
9280
- // (the running CLI) so a bulk `doctor` runs the SAME binary.
9785
+ // array through Bun.spawn. `fleetSelfInvokeArgv` re-invokes THIS CLI so a
9786
+ // bulk `doctor` runs the SAME binary, correct under both `bun run` and a
9787
+ // `bun build --compile` single-file binary: the compiled binary
9788
+ // re-injects its own `/$bunfs/…` entry, so we must NOT pass our `Bun.main`
9789
+ // as a subcommand (see fleet.ts — that was the v0.2.4 fleet-run bug).
9281
9790
  const runner = async ({ cwd, argv }) => {
9282
- const proc = Bun.spawn([process.execPath, process.argv[1], ...argv], {
9791
+ const spawnArgv = fleetSelfInvokeArgv({ execPath: process.execPath, entryPath: Bun.main }, argv);
9792
+ const proc = Bun.spawn(spawnArgv, {
9283
9793
  cwd,
9284
9794
  stdin: "ignore",
9285
9795
  stdout: "pipe",
@@ -11033,6 +11543,9 @@ async function runUpgrade(args) {
11033
11543
  " Detect the spec's schema-version drift vs this CLI's current spec version\n" +
11034
11544
  " and run the migration chain (each migrated spec is validated via parseSpec\n" +
11035
11545
  " before it can be written). Defaults to ./crewhaus.yaml and to a dry-run diff.\n" +
11546
+ " Also prints the 0.2.x → 0.3.0 release notes that apply to this spec\n" +
11547
+ " (default-on continuity + the `continuity: false` pin, `crewhaus migrate\n" +
11548
+ " memories`, MCP env/header secret lowering) — informational only.\n" +
11036
11549
  " --write apply the migration in place (rewrites the spec file).\n");
11037
11550
  return;
11038
11551
  }
@@ -11523,10 +12036,26 @@ async function runMcpDoctor(args) {
11523
12036
  if (Object.keys(mcpServers).length > 0) {
11524
12037
  mkdirSync(mcpDir, { recursive: true });
11525
12038
  const host = new McpHost({ logger });
11526
- for (const [name, cfg] of Object.entries(mcpServers))
11527
- host.addServer(name, cfg);
12039
+ for (const [name, cfg] of Object.entries(mcpServers)) {
12040
+ // 0.3.0 — env/header values are IrSecretRef; an unresolvable env
12041
+ // ref becomes a per-server probe failure (named variable) rather
12042
+ // than crashing the whole doctor run.
12043
+ try {
12044
+ host.addServer(name, resolveMcpServerConfig(cfg, { name }));
12045
+ }
12046
+ catch (err) {
12047
+ driftEntries.push({
12048
+ server: name,
12049
+ lines: [
12050
+ ` ✗ ${name}: probe failed — ${err instanceof Error ? err.message : String(err)}`,
12051
+ ],
12052
+ });
12053
+ driftDetected = true;
12054
+ }
12055
+ }
12056
+ const probeNames = Object.keys(mcpServers).filter((name) => host.has(name));
11528
12057
  const nowIso = new Date().toISOString();
11529
- await Promise.all(Object.keys(mcpServers).map(async (name) => {
12058
+ await Promise.all(probeNames.map(async (name) => {
11530
12059
  const lines = [];
11531
12060
  try {
11532
12061
  const client = host.getClient(name);
@@ -12914,15 +13443,16 @@ switch (subcommand) {
12914
13443
  // Mirror runCompile's policy: every structured failure in the run
12915
13444
  // pipeline (ConfigError from the model-router, ProviderAuthError from
12916
13445
  // an adapter, RuntimeError, …) extends CrewhausError — route the
12917
- // family through die() for a clean one-line error + exit 1 instead of
12918
- // a raw stack trace. A non-CrewhausError (a genuine bug) still
12919
- // propagates with its full stack for debugging.
13446
+ // family through die(), which prints the classic one-liner + exit 1
13447
+ // for most of them and (v0.3.0 Goal 6) the full structured report +
13448
+ // coded exit for a RunFailedError. A non-CrewhausError (a genuine
13449
+ // bug) still propagates with its full stack for debugging.
12920
13450
  try {
12921
13451
  await runRun(parseFor(rest, RUN_SCHEMA));
12922
13452
  }
12923
13453
  catch (err) {
12924
13454
  if (err instanceof CrewhausError)
12925
- die(err.message);
13455
+ die(err);
12926
13456
  throw err;
12927
13457
  }
12928
13458
  break;
@@ -13129,6 +13659,64 @@ switch (subcommand) {
13129
13659
  throw err;
13130
13660
  }
13131
13661
  break;
13662
+ case "memory":
13663
+ // 0.3.0 memory release (design §3.4) — inspect/forget/sweep the per-spec
13664
+ // fact stores. Structured failures route through die().
13665
+ try {
13666
+ await runMemory(parseFor(rest, MEMORY_SCHEMA));
13667
+ }
13668
+ catch (err) {
13669
+ if (err instanceof CrewhausError)
13670
+ die(err.message);
13671
+ throw err;
13672
+ }
13673
+ break;
13674
+ case "wiki":
13675
+ // 0.3.0 memory release (design §3.1, PR 9) — inspect the per-spec local
13676
+ // wikis (read-only verbs). Structured failures route through die().
13677
+ try {
13678
+ await runWiki(parseFor(rest, WIKI_SCHEMA));
13679
+ }
13680
+ catch (err) {
13681
+ if (err instanceof CrewhausError)
13682
+ die(err.message);
13683
+ throw err;
13684
+ }
13685
+ break;
13686
+ case "dream": {
13687
+ // 0.3.0 memory release (design §6.3, PR 14) — scheduled consolidation
13688
+ // verbs. A bare `crewhaus dream <spec>` defaults to `run` (the design's
13689
+ // transcript); the verbs themselves live in dream-cli.ts.
13690
+ const dreamFirst = rest[0];
13691
+ const dreamAction = dreamFirst === "run" || dreamFirst === "status" || dreamFirst === "init" ? dreamFirst : "run";
13692
+ const dreamRest = dreamFirst === dreamAction ? rest.slice(1) : rest;
13693
+ try {
13694
+ await runDreamCommand(parseFor(dreamRest, DREAM_CLI_SCHEMA), dreamAction);
13695
+ }
13696
+ catch (err) {
13697
+ if (err instanceof CrewhausError)
13698
+ die(err.message);
13699
+ throw err;
13700
+ }
13701
+ break;
13702
+ }
13703
+ case "migrate": {
13704
+ // 0.3.0 memory release — `migrate memories`: the v2 schema backfill over
13705
+ // .crewhaus/memories/. Spec migrations stay on migrate-all/upgrade.
13706
+ const migrateAction = rest[0] ?? "";
13707
+ if (migrateAction !== "memories") {
13708
+ die(`migrate action must be "memories" (got "${migrateAction}") — spec migrations use migrate-all/upgrade`);
13709
+ }
13710
+ try {
13711
+ await runMigrateMemories(parseFor(rest.slice(1), MIGRATE_MEMORIES_SCHEMA));
13712
+ }
13713
+ catch (err) {
13714
+ if (err instanceof CrewhausError)
13715
+ die(err.message);
13716
+ throw err;
13717
+ }
13718
+ break;
13719
+ }
13132
13720
  case "scaffold-evals":
13133
13721
  // Mirror `run`'s policy: structured failures (SpecParseError, model
13134
13722
  // routing/auth on the one-shot generation call) extend CrewhausError —