crewhaus 0.2.4 → 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/ci-scaffold.d.ts +18 -0
- package/dist/ci-scaffold.js +74 -0
- package/dist/doctor-probe.d.ts +72 -0
- package/dist/doctor-probe.js +92 -0
- package/dist/dream-cli.d.ts +53 -0
- package/dist/dream-cli.js +329 -0
- package/dist/fleet.d.ts +50 -0
- package/dist/fleet.js +78 -2
- package/dist/index.js +716 -128
- package/dist/init-conversation.d.ts +87 -0
- package/dist/init-conversation.js +306 -0
- package/dist/init-interactive.d.ts +39 -39
- package/dist/init-interactive.js +66 -56
- package/dist/lint.js +17 -1
- package/dist/memory-cli.d.ts +92 -0
- package/dist/memory-cli.js +278 -0
- package/dist/run-failure.d.ts +34 -0
- package/dist/run-failure.js +38 -0
- package/dist/sessions-index.d.ts +7 -19
- package/dist/sessions-index.js +7 -43
- package/dist/thredz-probe.d.ts +36 -0
- package/dist/thredz-probe.js +93 -0
- package/dist/upgrade.d.ts +42 -0
- package/dist/upgrade.js +110 -6
- package/dist/wiki-cli.d.ts +40 -0
- package/dist/wiki-cli.js +133 -0
- package/package.json +92 -89
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
|
-
|
|
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
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
|
|
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
|
-
|
|
1552
|
-
process.
|
|
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`.
|
|
2531
|
-
*
|
|
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
|
-
*
|
|
2534
|
-
* - Credentials
|
|
2535
|
-
*
|
|
2536
|
-
*
|
|
2537
|
-
*
|
|
2538
|
-
*
|
|
2539
|
-
*
|
|
2540
|
-
*
|
|
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
|
|
2550
|
-
"
|
|
2551
|
-
"
|
|
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
|
|
2575
|
-
// A resolution failure (no credentials / missing optional adapter)
|
|
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
|
|
2694
|
+
const conversational = conversationalPathAllowed({
|
|
2695
|
+
stdinIsTTY: process.stdin.isTTY === true,
|
|
2696
|
+
assumeYes: args.flags["yes"] === true,
|
|
2697
|
+
});
|
|
2579
2698
|
let yaml;
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
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
|
-
|
|
2714
|
+
mappingLines = result.mappingLines;
|
|
2591
2715
|
}
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
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
|
-
|
|
2598
|
-
|
|
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
|
|
2643
|
-
* adapter via the same `resolveModel` path the
|
|
2644
|
-
* returns undefined when no credentials/adapter
|
|
2645
|
-
* degrades to the scripted questionnaire. The
|
|
2646
|
-
*
|
|
2647
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
9280
|
-
//
|
|
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
|
|
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
|
-
|
|
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(
|
|
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()
|
|
12918
|
-
//
|
|
12919
|
-
//
|
|
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
|
|
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 —
|