wicked-crew 0.9.4 → 0.9.6

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.
Files changed (121) hide show
  1. package/dist/api/delivery-assurance.d.ts.map +1 -1
  2. package/dist/api/delivery-assurance.js +14 -3
  3. package/dist/api/delivery-assurance.js.map +1 -1
  4. package/dist/api/discovery-source.d.ts +4 -4
  5. package/dist/api/discovery-source.d.ts.map +1 -1
  6. package/dist/api/discovery-source.js +14 -8
  7. package/dist/api/discovery-source.js.map +1 -1
  8. package/dist/api/eval-sample.d.ts +12 -12
  9. package/dist/api/governance-steering.d.ts +2 -2
  10. package/dist/api/governance-steering.js +2 -2
  11. package/dist/api/governance-steering.js.map +1 -1
  12. package/dist/api/mcp.d.ts +4 -4
  13. package/dist/api/onboarding-capture.d.ts +1 -1
  14. package/dist/api/onboarding-capture.d.ts.map +1 -1
  15. package/dist/api/onboarding-capture.js +1 -1
  16. package/dist/api/onboarding-capture.js.map +1 -1
  17. package/dist/api/routes.d.ts +133 -131
  18. package/dist/api/routes.d.ts.map +1 -1
  19. package/dist/api/routes.js +37 -34
  20. package/dist/api/routes.js.map +1 -1
  21. package/dist/api/server.d.ts +6 -0
  22. package/dist/api/server.d.ts.map +1 -1
  23. package/dist/api/server.js +36 -1
  24. package/dist/api/server.js.map +1 -1
  25. package/dist/api/testing.js +1 -1
  26. package/dist/api/testing.js.map +1 -1
  27. package/dist/campaigns/plan.d.ts +8 -1
  28. package/dist/campaigns/plan.d.ts.map +1 -1
  29. package/dist/campaigns/plan.js +15 -3
  30. package/dist/campaigns/plan.js.map +1 -1
  31. package/dist/campaigns/routes.d.ts +8 -8
  32. package/dist/core/adapter.d.ts +29 -89
  33. package/dist/core/adapter.d.ts.map +1 -1
  34. package/dist/core/adapter.js +86 -609
  35. package/dist/core/adapter.js.map +1 -1
  36. package/dist/core/builtin-catalog.d.ts +24 -0
  37. package/dist/core/builtin-catalog.d.ts.map +1 -0
  38. package/dist/core/builtin-catalog.js +54 -0
  39. package/dist/core/builtin-catalog.js.map +1 -0
  40. package/dist/core/council-settings.d.ts +18 -0
  41. package/dist/core/council-settings.d.ts.map +1 -0
  42. package/dist/core/council-settings.js +37 -0
  43. package/dist/core/council-settings.js.map +1 -0
  44. package/dist/core/deliver-text.d.ts +1 -1
  45. package/dist/core/deliver-text.d.ts.map +1 -1
  46. package/dist/core/deliver-text.js +3 -2
  47. package/dist/core/deliver-text.js.map +1 -1
  48. package/dist/core/deliver.d.ts +0 -6
  49. package/dist/core/deliver.d.ts.map +1 -1
  50. package/dist/core/deliver.js +0 -6
  51. package/dist/core/deliver.js.map +1 -1
  52. package/dist/core/run-identity.d.ts +1 -1
  53. package/dist/core/run-identity.d.ts.map +1 -1
  54. package/dist/core/run-identity.js +7 -3
  55. package/dist/core/run-identity.js.map +1 -1
  56. package/dist/editors/csp.js +2 -2
  57. package/dist/editors/csp.js.map +1 -1
  58. package/dist/editors/pack-install.d.ts +35 -14
  59. package/dist/editors/pack-install.d.ts.map +1 -1
  60. package/dist/editors/pack-install.js +171 -24
  61. package/dist/editors/pack-install.js.map +1 -1
  62. package/dist/editors/registry.d.ts +151 -13
  63. package/dist/editors/registry.d.ts.map +1 -1
  64. package/dist/editors/registry.js +257 -20
  65. package/dist/editors/registry.js.map +1 -1
  66. package/dist/editors/routes.d.ts +10 -5
  67. package/dist/editors/routes.d.ts.map +1 -1
  68. package/dist/editors/routes.js +30 -20
  69. package/dist/editors/routes.js.map +1 -1
  70. package/dist/learn/routes.d.ts +42 -0
  71. package/dist/learn/routes.d.ts.map +1 -0
  72. package/dist/learn/routes.js +87 -0
  73. package/dist/learn/routes.js.map +1 -0
  74. package/dist/learn/schedule-store.d.ts +41 -0
  75. package/dist/learn/schedule-store.d.ts.map +1 -0
  76. package/dist/learn/schedule-store.js +137 -0
  77. package/dist/learn/schedule-store.js.map +1 -0
  78. package/dist/learn/scheduler.d.ts +71 -0
  79. package/dist/learn/scheduler.d.ts.map +1 -0
  80. package/dist/learn/scheduler.js +222 -0
  81. package/dist/learn/scheduler.js.map +1 -0
  82. package/dist/learn/topics.d.ts +14 -0
  83. package/dist/learn/topics.d.ts.map +1 -0
  84. package/dist/learn/topics.js +95 -0
  85. package/dist/learn/topics.js.map +1 -0
  86. package/dist/projects/graph.d.ts +14 -0
  87. package/dist/projects/graph.d.ts.map +1 -1
  88. package/dist/projects/graph.js +41 -2
  89. package/dist/projects/graph.js.map +1 -1
  90. package/dist/projects/routes.d.ts +2 -2
  91. package/dist/projects/state-home-registry.d.ts.map +1 -1
  92. package/dist/projects/state-home-registry.js +2 -0
  93. package/dist/projects/state-home-registry.js.map +1 -1
  94. package/dist/qe/acceptance.d.ts +5 -0
  95. package/dist/qe/acceptance.d.ts.map +1 -1
  96. package/dist/qe/acceptance.js +4 -2
  97. package/dist/qe/acceptance.js.map +1 -1
  98. package/dist/qe/author-workflow.d.ts +6 -83
  99. package/dist/qe/author-workflow.d.ts.map +1 -1
  100. package/dist/qe/author-workflow.js +6 -300
  101. package/dist/qe/author-workflow.js.map +1 -1
  102. package/dist/qe/ledger.d.ts +20 -0
  103. package/dist/qe/ledger.d.ts.map +1 -1
  104. package/dist/qe/ledger.js +28 -2
  105. package/dist/qe/ledger.js.map +1 -1
  106. package/dist/skills/core-closure.d.ts +3 -5
  107. package/dist/skills/core-closure.d.ts.map +1 -1
  108. package/dist/skills/core-closure.js +3 -5
  109. package/dist/skills/core-closure.js.map +1 -1
  110. package/dist/standing-orders/routes.d.ts +4 -4
  111. package/dist/standing-orders/store.d.ts +2 -2
  112. package/dist/studio/assets/index-C0ZMi_8d.js +583 -0
  113. package/dist/studio/assets/index-DYLo25MW.css +32 -0
  114. package/dist/studio/editors/wicked-page/editor.json +1 -1
  115. package/dist/studio/editors/wicked-page/index.html +1 -1
  116. package/dist/studio/index.html +2 -2
  117. package/dist/studio/testid-inventory.json +566 -36
  118. package/endpoint-manifest.json +59 -2
  119. package/package.json +4 -4
  120. package/dist/studio/assets/index-BGfFmWw4.css +0 -32
  121. package/dist/studio/assets/index-BMRUMU4X.js +0 -567
@@ -1,7 +1,8 @@
1
+ import { builtinDefs as deriveBuiltinDefs } from './builtin-catalog.js';
1
2
  import { createRequire } from 'node:module';
2
3
  import { attachEngineBus, detachEngineBus } from './bus.js';
3
4
  import { mkdir, access, readFile, writeFile, chmod, rm, link, rename, realpath, stat } from 'node:fs/promises';
4
- import { existsSync, readdirSync, readFileSync, renameSync } from 'node:fs';
5
+ import { existsSync } from 'node:fs';
5
6
  import { join, dirname, resolve, isAbsolute, relative, sep } from 'node:path';
6
7
  import { isPlainRunId, WALKTHROUGH_AUTHOR_SUBDIR, walkthroughRootDir } from './walkthrough-root.js';
7
8
  import { crewStateHome, isDefaultStateHome } from '../projects/state-home.js';
@@ -11,10 +12,8 @@ import { randomUUID } from 'node:crypto';
11
12
  import { DEFAULT_SETTINGS } from './types.js';
12
13
  import { BASE_SKILL_REF_SHAPE } from '../skills/base-skill.js';
13
14
  import { execCapped } from './exec.js';
14
- import { BUG_FIX_SWEEP_INSTRUCTIONS, composeDeliverWorkflow, DELIVER_PHASE_ID, deliverPresetStep, deliverIdentityFor, deliverRepoFor, EVIDENCE_FLOOR_PIN, isGitHubLogin, pushIdentityOf, readDeliverOriginUrl } from './deliver.js';
15
+ import { composeDeliverWorkflow, DELIVER_PHASE_ID, deliverPresetStep, deliverIdentityFor, deliverRepoFor, isGitHubLogin, pushIdentityOf, readDeliverOriginUrl } from './deliver.js';
15
16
  import { engineCampaignDef, engineRosterJson } from './engine-roster.js';
16
- import { QE_AUTHOR_TESTS_WORKFLOW_DEF } from '../qe/author-workflow.js';
17
- import { CAMPAIGN_WORKFLOW_PREFIX } from '../campaigns/plan.js';
18
17
  import { composeDeliverableFloor } from './deliverable-floor.js';
19
18
  import { deliverCredentialsProbe } from './deliver-credentials.js';
20
19
  import { isSyntheticWorkflowId, resolveRunIdentity, verifiedEvidenceCatalog, wireIdentity, withPresetSystemFlag, withSystemFlag, } from './run-identity.js';
@@ -22,6 +21,7 @@ import { isSyntheticWorkflowId, resolveRunIdentity, verifiedEvidenceCatalog, wir
22
21
  const TERMINAL_RUN_STATUSES = new Set(['completed', 'cancelled', 'failed']);
23
22
  import { resolveProjectGraphBinding } from '../projects/graph.js';
24
23
  import { applyGovernanceStoreEnv, isStoreSpec } from './governance-store.js';
24
+ import { isCouncilSize, isCouncilStaggerMs } from './council-settings.js';
25
25
  /** Resolved path under the user's home directory. */
26
26
  function wickedDir(...parts) {
27
27
  const home = process.env.HOME ?? process.env.USERPROFILE ?? '/tmp';
@@ -44,20 +44,6 @@ function parseEngineJson(raw, method) {
44
44
  throw new Error(`${method}: engine returned invalid JSON (${e instanceof Error ? e.message : String(e)})`);
45
45
  }
46
46
  }
47
- /**
48
- * Workflow overlay directory — mirrors the Rust `workflow_overlay_dir()` logic in
49
- * `pipeline.rs`. The Rust actor reads drop-in workflow JSONs from this path at startup
50
- * and (with registerWorkflow NAPI) at runtime. TS must write to the same location so
51
- * the files are picked up on the next daemon start.
52
- * • `$WICKED_WORKFLOWS_DIR` — explicit override (matches Rust env check)
53
- * • `~/.config/wicked-core/workflows` — default (matches Rust default)
54
- */
55
- function workflowOverlayDir() {
56
- if (process.env.WICKED_WORKFLOWS_DIR)
57
- return process.env.WICKED_WORKFLOWS_DIR;
58
- const home = process.env.HOME ?? process.env.USERPROFILE ?? '/tmp';
59
- return join(home, '.config', 'wicked-core', 'workflows');
60
- }
61
47
  /** Where the daemon persists system settings. Exported so tests write fixtures to the ONE
62
48
  * spelling of this path (the tests/ tree is audited against spelling core paths itself).
63
49
  *
@@ -167,83 +153,6 @@ async function publishSettingsFile(linkPath, body, replace) {
167
153
  * the number a third time so a change to either side breaks a test rather than a deployment.
168
154
  */
169
155
  const STUDIO_SETTINGS_MAX_BYTES = 512 * 1024;
170
- /** Read user-registered workflow overlays from `dir` (the same dir `registerWorkflow` writes to).
171
- *
172
- * Skips: files whose id matches a built-in (those are `_writeBuiltinOverlay` artifacts written FOR
173
- * the Rust actor, not user workflows — including them would duplicate a built-in in `listWorkflows`),
174
- * non-`.json` files, and any file that does not parse into a `{id, phases[]}` shape (the Rust actor
175
- * skips an unreadable overlay too, so crew must not surface one it can't). A missing dir yields `[]`.
176
- *
177
- * Exported so the FINDING-002 restart-hydration path is unit-testable without spawning a Core. */
178
- export function readOverlayWorkflows(dir, builtinIds) {
179
- let files;
180
- try {
181
- files = readdirSync(dir);
182
- }
183
- catch {
184
- return []; // no overlay dir yet → nothing registered
185
- }
186
- const out = [];
187
- for (const file of files) {
188
- if (!file.endsWith('.json'))
189
- continue;
190
- const id = file.slice(0, -'.json'.length);
191
- if (builtinIds.has(id))
192
- continue; // a built-in overlay, not a user workflow
193
- // A composed campaign-node workflow (TH-9): persisted here so the Rust actor reloads it at
194
- // startup (a fresh-process resumeCampaign re-dispatches pending nodes by workflow id), but
195
- // it is one node's private plumbing, not a user workflow — keep it out of the catalog.
196
- if (id.startsWith(CAMPAIGN_WORKFLOW_PREFIX))
197
- continue;
198
- try {
199
- const def = JSON.parse(readFileSync(join(dir, file), 'utf8'));
200
- if (def && typeof def.id === 'string' && Array.isArray(def.phases))
201
- out.push(def);
202
- }
203
- catch {
204
- // Unparseable overlay — core would skip it at load, so crew skips it too.
205
- }
206
- }
207
- return out;
208
- }
209
- /** Ask CORE's parser which drop-in defs it will actually honour (crew#718).
210
- *
211
- * {@link readOverlayWorkflows} checks a `{id, phases[]}` shape, which is not the engine's contract:
212
- * `WorkflowRegistry::load_dir` runs `refuse_reserved_id` + `WorkflowDef::validate()` and SKIPS the
213
- * file it rejects, loudly, on the daemon's stderr. So the catalog listed defs the engine had thrown
214
- * away at boot and the launch then 400'd `unknown workflow` — the MCP S8 dogfood's F-3, on a
215
- * hand-authored drop-in whose `executes_code` phase pinned no validator ("gate evaluates nothing").
216
- *
217
- * The same doctrine {@link CoreAdapter.registerWorkflow} states for the WRITE path applies here:
218
- * core's parser is the authority, so it is what we ask. Enumerating `validate()`'s rules in
219
- * TypeScript would be a second copy of core's schema — the drift that produced this defect.
220
- * `register` is the engine's `registerWorkflow` binding, which validates before it registers and
221
- * is idempotent on id, so re-offering a def the engine already loaded at boot is a same-content
222
- * overwrite; a def added to the dir after boot becomes launchable rather than being listed and
223
- * refused.
224
- *
225
- * `is_system` is stripped first, exactly as the two write paths do: it is crew's own display flag
226
- * and core's strict def parser rejects the key.
227
- *
228
- * Pure (no adapter state) so it is unit-testable without spawning a Core, like
229
- * {@link readOverlayWorkflows}.
230
- */
231
- export async function judgeOverlayWorkflows(defs, register) {
232
- const accepted = [];
233
- const refused = [];
234
- for (const def of defs) {
235
- const overlayDef = { ...def };
236
- delete overlayDef.is_system;
237
- try {
238
- await register(JSON.stringify(overlayDef));
239
- accepted.push(def);
240
- }
241
- catch (err) {
242
- refused.push({ id: def.id, reason: err instanceof Error ? err.message : String(err) });
243
- }
244
- }
245
- return { accepted, refused };
246
- }
247
156
  // The native addon is a CommonJS cdylib (`index.node`); load it with a CJS
248
157
  // require even though this daemon is ESM. This module is the ONLY place that
249
158
  // touches wicked-core-ts (DES-STUDIO-001 §5.2/§5.3), so the FINALIZING
@@ -432,241 +341,9 @@ function addonAtLeast(maj, min, pat) {
432
341
  return false;
433
342
  }
434
343
  }
435
- // ── Built-in workflow definitions (crew#44) ──────────────────────────────────
436
- // Static mirrors of wicked-core workflow defs: feature, bug, migration and domain-extraction, plus
437
- // crew's own defs. `chat` and `onboarding` are engine built-in PRESETS (DES-TEAMING-002 M3/M4),
438
- // launched by the same name with no def here; `survey-repo`, `memories`, `domain-graph-slice` and
439
- // `collab` are deleted (nothing launched them).
440
- // Swap for `this.core.listWorkflowsJson()` / `this.core.getWorkflowJson(id)` once
441
- // the wicked-core-ts NAPI methods land.
442
- /**
443
- * The ids wicked-core seeds itself, in `WorkflowRegistry::with_defaults()`.
444
- *
445
- * `launchRun`'s generic drop-in overlay write SKIPS every id in this set.
446
- * A file in that dir shadows the compiled built-in
447
- * *wholesale* — `register` overwrites by id and `load_dir` runs after `with_defaults` — so writing
448
- * this hand-transcribed mirror over the real def silently replaces it with a copy missing whatever
449
- * the def has grown since the mirror was transcribed. That is not hypothetical: the mirror predated
450
- * the evidence floors, so the write took `validator_pin` back off `feature.adversarial-review`,
451
- * `bug.verify` and `migration.verify` — the entire content of core's gate-floor change, undone by a
452
- * file write, with no error and a workflow still reporting the right id and phases (FINDING-049).
453
- *
454
- * The write exists for the ids core does NOT seed (domain-extraction and crew's own defs): for
455
- * those the overlay is the only reason they resolve at all, so it stays.
456
- */
457
- const CORE_SEEDED_WORKFLOWS = new Set(['feature', 'bug', 'migration']);
458
- // `EVIDENCE_FLOOR_PIN` (imported from ./deliver.js, defined once) is carried on the Evaluator phase
459
- // of feature/bug/migration AND, since wicked-core F-039, on their code-writing Creator phases
460
- // (`build`/`fix`/`execute`) — so the gate that was supposed to make the change re-derives its diff
461
- // and a distinct seat judges it, instead of approving nothing. What `listWorkflows()` serves IS what
462
- // `GET /api/v1/workflows` and the work-mode selector show, and a `null` here reads as "this phase
463
- // is ungated" — the opposite of the truth. Since wicked-core#414 the engine also REFUSES a mirror
464
- // that lags (a code phase whose gate evaluates nothing is rejected as authored), so these values
465
- // are part of the contract with the engine, not display only: the drift guards in
466
- // `tests/armed-workflow-served.test.ts` and `tests/builtin-overlay-shadow.test.ts` fail loudly
467
- // the moment core's defs move. As of FINDING-049 these defs are never written to core's overlay dir
468
- // (see CORE_SEEDED_WORKFLOWS).
469
- // `is_system` is NOT spelled on these defs: `withSystemFlag` stamps it from `SYSTEM_WORKFLOWS`
470
- // (core/run-identity.ts), the one list keyed by name that also classifies served runs (seam X2).
471
- /** QE-IN-APP-WORKFLOWS (operator ruling 2026-10-10): what a workflow that makes application changes
472
- * requires — the defaults plus `qe_acceptance` — exactly as core's `feature`/`bug`/`migration`/
473
- * `mcp-server` defs declare it (the mirror guards compare field for field). */
474
- const APP_CHANGE_INSTRUMENTS = ['distinct_evaluator', 'judge', 'qe_acceptance'];
475
- export const BUILTIN_WORKFLOWS = [
476
- {
477
- id: 'feature',
478
- phases: [
479
- { id: 'clarify', kind: 'recon', gate_type: 'value', gate: { human_confirm: { unconditional: false } }, executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: [], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
480
- { id: 'design', kind: 'recon', gate_type: 'strategy', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['clarify'], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
481
- { id: 'build', kind: 'build', gate_type: 'execution', gate: 'auto', executes_code: true, verified_evidence: false, required_deliverables: [], depends_on: ['design'], role: 'creator', skill_ref: null, allowed_skills: [], validator_pin: EVIDENCE_FLOOR_PIN },
482
- { id: 'adversarial-review', kind: 'review', gate_type: 'execution', gate: { human_confirm: { unconditional: false } }, executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['build'], role: 'evaluator', skill_ref: null, allowed_skills: [], validator_pin: EVIDENCE_FLOOR_PIN },
483
- { id: 'test', kind: 'test', gate_type: 'execution', gate: { human_confirm_if: 'verdict_not_pass' }, executes_code: false, verified_evidence: true, required_deliverables: [], depends_on: ['build'], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: EVIDENCE_FLOOR_PIN },
484
- { id: 'review', kind: 'review', gate_type: 'execution', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['test'], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
485
- ],
486
- required_instruments: APP_CHANGE_INSTRUMENTS,
487
- },
488
- {
489
- id: 'bug',
490
- phases: [
491
- { id: 'triage', kind: 'recon', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: [], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
492
- { id: 'reproduce', kind: 'test', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['triage'], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
493
- // DES-L9 (BC-60, core#432): the retired-behaviour sweep — the SAME literal core's `bug_def()` carries (wicked-core #522); pinned by a test
494
- // so the two carriers cannot drift. `builtin-overlay-shadow.test.ts` tolerates exactly this one field while core MAIN has not merged #522
495
- // (crew CI compares this mirror with core main); row 6.9 (the `^0.7.27` pin) removes that tolerance.
496
- { id: 'fix', kind: 'build', instructions: BUG_FIX_SWEEP_INSTRUCTIONS, gate_type: 'execution', gate: 'auto', executes_code: true, verified_evidence: false, required_deliverables: [], depends_on: ['reproduce'], role: 'creator', skill_ref: null, allowed_skills: [], validator_pin: EVIDENCE_FLOOR_PIN },
497
- { id: 'verify', kind: 'test', gate_type: 'execution', gate: { human_confirm_if: 'verdict_not_pass' }, executes_code: false, verified_evidence: true, required_deliverables: [], depends_on: ['fix'], role: 'evaluator', skill_ref: null, allowed_skills: [], validator_pin: EVIDENCE_FLOOR_PIN },
498
- ],
499
- required_instruments: APP_CHANGE_INSTRUMENTS,
500
- },
501
- {
502
- id: 'migration',
503
- phases: [
504
- { id: 'plan', kind: 'recon', gate_type: 'strategy', gate: { human_confirm: { unconditional: false } }, executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: [], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
505
- { id: 'execute', kind: 'build', gate_type: 'execution', gate: 'auto', executes_code: true, verified_evidence: false, required_deliverables: [], depends_on: ['plan'], role: 'creator', skill_ref: null, allowed_skills: [], validator_pin: EVIDENCE_FLOOR_PIN },
506
- { id: 'cutover', kind: 'build', gate_type: 'execution', gate: { human_confirm: { unconditional: true } }, executes_code: true, verified_evidence: false, required_deliverables: [], depends_on: ['execute'], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
507
- { id: 'verify', kind: 'test', gate_type: 'execution', gate: { human_confirm_if: 'verdict_not_pass' }, executes_code: false, verified_evidence: true, required_deliverables: [], depends_on: ['cutover'], role: 'evaluator', skill_ref: null, allowed_skills: [], validator_pin: EVIDENCE_FLOOR_PIN },
508
- { id: 'cleanup', kind: 'build', gate_type: null, gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['verify'], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
509
- ],
510
- required_instruments: APP_CHANGE_INSTRUMENTS,
511
- },
512
- {
513
- // capture-learnings (DES-MEM-FACETED-001 write side, onboarding): survey a just-indexed repo,
514
- // then propose its durable learnings — BOTH faceted MEMORIES and repo POLICIES — as inert estate
515
- // `proposal.submit` proposals (through garden's estate shim) a human later reviews.
516
- //
517
- // ONE workflow, not four. "Go multi-workflow" is realized as multi-PHASE composition inside a
518
- // single governed run, NOT as separate churn-analysis / hotspot-read / derive-memories /
519
- // derive-policies RUNS, because:
520
- // • Context threading: the learning method is a dependent chain (churn ranking → hotspot
521
- // cross-reference → capture). Crew threads each phase's output into the next phase's prompt
522
- // automatically (plan.rs folds prior context); separate runs share NOTHING, so a split would
523
- // sever that thread and each run would re-establish repo understanding from scratch.
524
- // • Council cost: every phase convenes a ~6-seat council (the ecosystem's spikiest operation,
525
- // serialized on purpose). Four runs multiply that; three phases in one run pay it once each.
526
- // • The Memories-vs-Policies review split is a proposal-KIND concern, not a workflow-identity
527
- // one: `kind_type` routes memory→studio Memories and policy:<type>→Steering downstream, so a
528
- // SINGLE `capture` phase emits both from the one shared understanding — splitting derive-
529
- // memories / derive-policies would re-run a council over the same context for no new evidence.
530
- // • Reuse already lives below the run: the reusable unit is the SKILL (and hotspot-read is
531
- // already a reusable capability via `wicked-garden-search`).
532
- //
533
- // The METHOD lives in the garden skill `wicked-garden-repo-learn`, referenced per-phase by
534
- // `skill_ref` — the engine emits only a short `Invoke your skill "wicked-garden:repo-learn"…`
535
- // directive and the worker loads SKILL.md from the installed plugin. The bounded git-churn
536
- // sampling, the estate tool names (reached through the shim), and the proposal payload schemas that used to sit inline as
537
- // ~600-column prose now live in that skill; the inline `instructions` here are a one-line phase
538
- // ORIENTATION only. That matters because a governed worker's prompt rides a single PTY line capped
539
- // at 1022 bytes (>=1023B is SILENTLY discarded — wicked-core execute_wrapped.rs), and the planner
540
- // folds this text onto that line alongside the run intent, so long inline prose here would blow
541
- // the line. Crew-only (NOT core-seeded), so the overlay write is the only def the engine resolves
542
- // — no core mirror, and deliberately NOT in builtin-overlay-shadow's MIRRORED_IDS.
543
- //
544
- // The shim's `wicked-estate-mcp --readonly` opens the operator GLOBAL memory store and permits
545
- // `proposal.submit` (a safe write, provenance server-stamped from the WICKED_RUN_* markers on the
546
- // worker env — DES-L4 PR-③/⑦; there is no CLI-registered estate MCP on the worker any more),
547
- // so proposals land in the same queue the studio Memories/Policies surfaces review. Onboarding IS
548
- // about the repo, so the skill tags learnings `repo:`/`project:`.
549
- id: 'capture-learnings',
550
- phases: [
551
- { id: 'churn', kind: 'recon', instructions: "Phase 1/3 CHURN: produce a ranked list of this repo's most actively-changed files and directories over the last ~12 months, plus the repo's real name (manifest or git remote) and parent project. Use the skill's bounded/sampled git-churn method — never stream the whole history. Do not read code deeply yet; the next phase targets these areas.", gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: [], role: 'neutral', skill_ref: 'wicked-garden-repo-learn', allowed_skills: [], validator_pin: null },
552
- { id: 'hotspots', kind: 'recon', instructions: "Phase 2/3 HOTSPOTS: cross-reference the prior churn ranking with wicked-estate hotspot / blast-radius signals to find the load-bearing code, then READ it through the estate shim (`wicked-garden run scripts/_estate_client.py --readonly call …`, the skill's grounding path) to build a real technical understanding of how the system fits together — not a file listing. Reuse wicked-garden-search for the hotspot signals; follow the skill.", gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['churn'], role: 'neutral', skill_ref: 'wicked-garden-repo-learn', allowed_skills: [], validator_pin: null },
553
- // (BC-80, wicked-core#535) `requires_capture_report`: the engine's capture-report floor reads
554
- // the phase's output marker, so a capture run whose skill loaded and never ran can no longer
555
- // report `completed` with 0 proposals — a missing marker, a failed submission and proposals
556
- // derived but never submitted each deny into the human gate. An honest 0 passes. The
557
- // instruction MANDATES the marker; garden's repo-learn skill carries the same contract, and
558
- // the counts land on the unit (`capture_report`), which `GET /runs/:id` serves.
559
- { id: 'capture', kind: 'build', instructions: "Phase 3/3 CAPTURE: from the prior churn + hotspot understanding, submit durable learnings as estate proposals through the shim's `propose` per the skill's capture contract — BOTH memories (facts / how-it-works) and policies (enforced conventions), one proposal per item, tagged repo/project. Each is inert until human review; never include secrets or personal data. END with `wicked-capture-report {\"derived\": N, \"submitted\": M, \"failed\": K}` — always, even on a degrade or a legitimate 0 (which is acceptable).", gate_type: 'value', gate: 'auto', executes_code: false, requires_capture_report: true, verified_evidence: false, required_deliverables: [], depends_on: ['hotspots'], role: 'creator', skill_ref: 'wicked-garden-repo-learn', allowed_skills: [], validator_pin: null },
560
- ],
561
- },
562
- {
563
- // "Add with chat" for the Steering surface (STEERING program) — the dedicated entry point
564
- // behind POST /governance/steering/author. TH-12 propose-as-gate: the run analyzes the
565
- // operator's intent + the named files/dirs, then the TERMINAL `propose` phase emits the
566
- // PROPOSED steering rules and its unconditional human gate pauses the run `awaiting_human`
567
- // (core evaluates a terminal phase's own gate before finalize — seam finding #4), so the
568
- // operator approves/amends/rejects via the standard POST /runs/:id/gate. Approved rules land
569
- // CREW-SIDE on that approve: the gate handler writes them through the governed rules seam
570
- // with `provenance.source: "chat"` (api/steering-landing.ts, crew#388) — the run itself must
571
- // never write the store, which is why both phases say so out loud. The propose phase hands
572
- // the rules back in its REPLY — one ```json fenced array — and the landing reads them from the
573
- // unit's stored output (#789). It used to be told to write the array to a file in the per-run
574
- // inbox under the home config; a seat's sandbox refuses a write outside its workspace (codex's
575
- // "approval request failed"), so every author run reported a failed write and the rules only
576
- // landed through the transcript anyway. The reply is now the designed path, not a fallback.
577
- //
578
- // Crew-authored drop-in: NOT in CORE_SEEDED_WORKFLOWS, so launchRun's
579
- // `_writeBuiltinOverlay` write is the only way core resolves the id — the same delivery
580
- // mechanism every crew drop-in uses.
581
- id: 'steering-author',
582
- phases: [
583
- { id: 'analyze', kind: 'recon', instructions: 'Read the operator intent and every file or directory listed in the problem statement. Identify candidate steering rules: durable, prescriptive statements a coding agent must follow, each classified into one steering type (architecture, development, security, testing, operations, compliance, design-ux). For each candidate note the statement, steering type, severity, and the evidence in the source material. Analysis only — do not write any rule to any store, and do not emit final rule JSON yet.', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: [], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
584
- { id: 'propose', kind: 'recon', instructions: 'From the prior analysis, emit the PROPOSED steering rules as one JSON array. Each entry is a conformance-rule object: id (PAT-<digits> for rule_type "pattern", POL-<digits> for "policy"), rule_type, statement, severity (info|warn|error|critical), confidence (a NUMBER 0..1), steering_type (default to the type named in the problem statement), provenance {"source":"chat"}, and — only where the source material supports them — the enforcement fields applies_to (array of phase tokens or globs), excludes, weight, obligations (array of strings), criteria (ONE string, never a list). Omit targets, effect and trigger unless you can express them in the store schema exactly: targets is a {language, layer, framework} facet OBJECT (never a file list — files belong in applies_to), and trigger is a structured condition object (never prose). Put that JSON array in your reply, ONCE, as a single ```json fenced block (the whole array, valid JSON): your reply IS the proposal — crew reads the array from it when the human approves. Do not write it to any file and do not create files for it. This output is a PROPOSAL for the human gate: rules land in the governance store only after approval, written crew-side — do not write any rule to any store yourself.', gate_type: 'value', gate: { human_confirm: { unconditional: true } }, executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['analyze'], role: 'creator', skill_ref: null, allowed_skills: [], validator_pin: null },
585
- ],
586
- },
587
- // The governed test-authoring workflow (wave 6 — F-7R2-003/004/005/012/014/015, R4-r2): recon →
588
- // author (creator, evidence-floor pinned) → verify (a TOOL phase that RUNS every produced test
589
- // under the repository's own harness and fails the unit when one fails or never ran) → review
590
- // (evaluator ≠ creator). Delivery is appended per run by the engine-side composition (`deliver:
591
- // "pr"`), never performed by a worker. Def + verify script live in `qe/author-workflow.ts` so the
592
- // e2e can assert the script's behaviour without reaching into this array. Crew-only drop-in (NOT
593
- // core-seeded); the `wicked-garden-qe` skill carries the method (plan/author/review actions).
594
- QE_AUTHOR_TESTS_WORKFLOW_DEF,
595
- // The one workflow that ARMS the dual-validator gate: `coverage` carries an approved
596
- // `validator_pin`, so layer 1 is live here and inert in every entry above. Transcribed
597
- // field-for-field from the source of truth, `wicked-core/workflows/domain-extraction.json`
598
- // (core ships it as a *drop-in*, not a seeded built-in, and exposes no dump command — hence a
599
- // hand-transcribed mirror, like every other entry in this array).
600
- //
601
- // The pin is a content hash over the validator's criterion + script + approved flag. Core
602
- // re-derives it in `domain_extraction.rs` and fails its own test if it drifts; if that test ever
603
- // forces core's constant to change, THIS literal must change with it or crew will write an
604
- // overlay that fails closed at plan time.
605
- //
606
- // Running it needs a one-time, idempotent `wicked-core seed-domain-validators` to vault + approve
607
- // that validator. That step is deliberately manual — approval is an audited act a human/council
608
- // owns, not something a daemon does unattended — and until it is run, a launch fails CLOSED at
609
- // plan time rather than running the phase ungated. Not `is_system`: this is an operator-selectable
610
- // work mode, unlike the dedicated-entry-point workflows above.
611
- {
612
- id: 'domain-extraction',
613
- phases: [
614
- // required_deliverables reconciled with core (wicked-core/workflows/domain-extraction.json):
615
- // survey/analyze/extract annotate the estate STORE and domain-graph now PERSISTS the graph
616
- // into the store (not a JSON file), so their evidence is DB state — verified by the coverage
617
- // gate (reads the store) and domain-graph's fail-closed-on-coverage<1.0 — not a worktree file.
618
- // Only coverage emits a genuine standalone report the deterministic floor reads. Declaring
619
- // phantom files failed every phase under core's FINDING-101 deliverable gate.
620
- //
621
- // `coverage` is `executes_code: true` (wicked-core#414): it WRITES `coverage-report.json`
622
- // into the worktree for its pinned validator to read, and an `executes_code: false` phase
623
- // may write nothing there — the worktree guard has no exemptions, declared deliverables
624
- // included. Its role stays `evaluator`; the deliver default keys off code-writing
625
- // NON-EVALUATOR phases (`executes_code && role !== 'evaluator'`), so this def never delivers.
626
- { id: 'survey', kind: 'recon', gate_type: null, gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: [], role: 'neutral', skill_ref: 'wicked-garden-domain', allowed_skills: [], validator_pin: null },
627
- { id: 'analyze', kind: 'recon', gate_type: null, gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['survey'], role: 'neutral', skill_ref: 'wicked-garden-domain', allowed_skills: [], validator_pin: null },
628
- { id: 'extract', kind: 'recon', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['analyze'], role: 'creator', skill_ref: 'wicked-garden-domain-extractor', allowed_skills: [], validator_pin: null },
629
- { id: 'coverage', kind: 'test', gate_type: 'execution', gate: { human_confirm_if: 'verdict_not_pass' }, executes_code: true, verified_evidence: true, required_deliverables: ['coverage-report.json'], depends_on: ['extract'], role: 'evaluator', skill_ref: 'wicked-garden-domain-coverage', allowed_skills: [], validator_pin: 'bfe4020a365c598b' },
630
- // domain-graph is a DETERMINISTIC Tool that runs `wicked-core domain-graph`, which PERSISTS the
631
- // domain/requirement/rule graph into the repo store (core#237) — not an LLM skill that could hit
632
- // a non-persisting hermetic fallback. Mirrors wicked-core/workflows/domain-extraction.json.
633
- { id: 'domain-graph', executor: { type: 'tool', cmd: ['wicked-core', 'domain-graph', '--db', '{code_graph_db}', '--out', 'requirements_graph.json'] }, kind: 'build', gate_type: 'strategy', gate: { human_confirm: { unconditional: false } }, executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['coverage'], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
634
- ],
635
- },
636
- // The MCP-server drop-in (DES-mcp-server-workflow): transcribed field for field — every
637
- // `instructions` string verbatim, the gates, pins, roles, skill_refs, depends_on and the install
638
- // phase's `executor` — from the source of truth, `wicked-core/workflows/mcp-server.json` (core ships
639
- // it as a drop-in, not a seeded built-in, so it is NOT in CORE_SEEDED_WORKFLOWS: launchRun writes
640
- // this mirror to the overlay and hot-registers it, as for domain-extraction). It carries only the
641
- // evidence-floor pin, so no seed step is needed. Deliver is composed per run and placed BEFORE
642
- // `install` (`composeDeliverWorkflow` / `placeDeliverBeforeInstall`), so the pull request exists
643
- // when the install gate asks. The install gate is `consent_before` (core#801, crew#888): the engine
644
- // pauses with `gateKind: 'consent'` BEFORE the install runs and nothing pauses after it; the
645
- // command execs the admitted garden at `WICKED_GARDEN_ROOT` (core#802), never PATH. The install
646
- // Tool phase runs on the daemon host as the daemon user (the garden install script builds into
647
- // ~/.wicked/mcp-servers/<key>, registers the server in this daemon's MCP tools and writes the CLI
648
- // configurations through wicked-installer). core#820: an `install-plan` Tool phase dry-runs the
649
- // install first (`--dry-run --json`, one plan line), the gate offers its choices
650
- // (`consent:worker` default, `consent:operator`, `reject`) with each one's write targets, and the
651
- // install runs with the chosen `--target "$WICKED_CONSENT_CHOICE"`. Field-for-field equal to
652
- // core's `workflows/mcp-server.json` (tests/builtin-overlay-shadow.test.ts).
653
- // Not `is_system`: an operator-selectable work mode.
654
- {
655
- id: 'mcp-server',
656
- phases: [
657
- { id: "scope", kind: "recon", gate_type: "value", gate: { "human_confirm": { "unconditional": false } }, executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: [], role: "neutral", skill_ref: "wicked-garden-mcp-scaffold", allowed_skills: [], validator_pin: null, instructions: "Decide what this MCP server exposes (tools, resources, prompts), the upstream's authentication scheme (bearer, API key, basic, OAuth2 client credentials — the server reads ONE secret variable <SERVER>_TOKEN and every other auth parameter from its committed mcp-server.config.json), the transport (stdio by default; httpStream only when the intent asks for a hosted server), and the target: this run's repository, at its root when the repository is new and near-empty, or in a named subdirectory when it already holds code. Say whether an OpenAPI document was given (URL or file), and whether the operator wants the server installed for running when the run completes (the install phase asks again at its gate). If the intent names a repository that is not this run's, say that the operator must create it with an initial commit and register it before a run can deliver into it; do not try to create one." },
658
- { id: "source-discovery", kind: "recon", gate_type: null, gate: "auto", executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ["scope"], role: "neutral", skill_ref: "wicked-garden-mcp-scaffold", allowed_skills: [], validator_pin: null, instructions: "With an OpenAPI document: run the skill's openapi action against the conversion service and report every tool it yields with its class (read, write, destructive), its input schema summary and its request mapping, plus the skipped operations and why. Without one: inventory the integration surface (SDK calls, CLI, database, events) into a candidate tool list with the same columns. Do not write code in this phase." },
659
- { id: "design", kind: "recon", gate_type: "strategy", gate: { "human_confirm": { "unconditional": false } }, executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ["source-discovery"], role: "neutral", skill_ref: "wicked-garden-mcp-scaffold", allowed_skills: [], validator_pin: null, instructions: "Produce the design: the final tool table (name, description, input schema, annotations, class), the resources and prompts if any, the authentication plan (the one secret variable, the committed non-secret parameters, which header, what fails at startup), and the observability plan (span names and attributes, the three instruments, log fields, what leaves the process and only when). Check each against the MCP-server steering rules MCPS-1001 to MCPS-1007 (recall them with rules.recall, scope wiki:governance, when an estate store is present; otherwise use the checklist in the skill) and against MCP-D2 and MCP-D4. If a rule cannot be recalled, say so, cite the skill's checklist instead, and never state what a rule id means when you could not read it. Name every deviation and why." },
660
- { id: "build", kind: "build", gate_type: "execution", gate: "auto", executes_code: true, verified_evidence: false, required_deliverables: [], depends_on: ["design"], role: "creator", skill_ref: "wicked-garden-mcp-scaffold", allowed_skills: [], validator_pin: EVIDENCE_FLOOR_PIN, instructions: "Scaffold the TypeScript server with the skill (--lang typescript) into the target directory. Branch A, with a tools.json from the conversion service: the generated tools ride the skeleton's REST runtime; adapt names, descriptions and annotations to the design. Branch B, without one: write each tool by hand with a zod schema on the same skeleton. Either way: the one secret variable and the committed mcp-server.config.json the design named, the conformance smoke and one contract test per tool under npm test, the typecheck, lint and test scripts present, an SPDX header on every source file, telemetry exporters armed only by OTEL_EXPORTER_OTLP_ENDPOINT. Build it and run the skill's probe; put the probe JSON in your output." },
661
- { id: "test", kind: "test", gate_type: "execution", gate: { "human_confirm_if": "verdict_not_pass" }, executes_code: false, verified_evidence: true, required_deliverables: [], depends_on: ["build"], role: "neutral", skill_ref: "wicked-garden-qe-contract-testing-engineer", allowed_skills: [], validator_pin: EVIDENCE_FLOOR_PIN, instructions: "Judge the verify floor's report of the repository's own checks (typecheck, lint, test) and the tests themselves: every tool in the design has a contract test, the conformance smoke covers initialize, tools/list, tools/call, the missing-credential startup failure and the httpStream 401 path, and nothing in the tree holds a secret value. A tool without a test, or a smoke that does not exercise the failure paths, is a FAIL with the tool named." },
662
- { id: "security-review", kind: "review", gate_type: "execution", gate: { "human_confirm": { "unconditional": false } }, executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ["test"], role: "evaluator", skill_ref: "wicked-garden-platform-security-engineer", allowed_skills: [], validator_pin: EVIDENCE_FLOOR_PIN, instructions: "Review the diff cold against MCPS-1004 and MCPS-1005 and MCP-D2: the one secret read from the environment only and never logged, returned or accepted as an argument; non-secret auth parameters in the committed config; authentication on every tool and resource (authenticate on httpStream, canAccess on every tool); input validated before the handler runs; upstream requests pinned to the configured base URL with allowlisted arguments and no redirects; a rate limit that answers a user error. Cite file and line for every finding." },
663
- { id: "observability-review", kind: "review", gate_type: "execution", gate: { "human_confirm_if": "verdict_not_pass" }, executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ["test"], role: "evaluator", skill_ref: "wicked-garden-qe-observability-test-engineer", allowed_skills: [], validator_pin: EVIDENCE_FLOOR_PIN, instructions: "Review the diff cold against MCPS-1002 and MCPS-1003: one span per tool call with the server, tool, request id and outcome attributes and a child span per upstream request; the three instruments recorded; logs through loglayer with trace and span ids stamped, a request-scoped child logger per call, console output on stderr only; nothing exported unless the operator sets an OTLP endpoint. Cite file and line for every finding." },
664
- { id: "install-plan", kind: "build", gate_type: "execution", gate: "auto", executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ["security-review", "observability-review"], role: "neutral", skill_ref: null, allowed_skills: [], validator_pin: null, executor: { "type": "tool", "cmd": ["bash", "-c", "exec \"${WICKED_GARDEN_ROOT:?the engine handed this Tool phase no garden root; refusing to resolve wicked-garden from PATH}/scripts/wicked-garden\" run scripts/mcp/install.py --from-run --dry-run --json"] }, instructions: "Plan the install without writing anything: resolve, on this host and as the daemon user, every file and directory each install choice would write — Install for workers (the default: the staged copy under ~/.wicked/mcp-servers/<key>/current, the wicked-crew MCP tools registry and the worker homes' CLI configurations under ~/.wicked-worker) and Also install into my CLIs (the worker set plus your own CLI configurations, such as ~/.claude.json, ~/.codex/config.toml and the opencode configuration) — and print them as one JSON plan line, so the install's consent gate can list them before you answer." },
665
- { id: "install", kind: "build", gate_type: "execution", gate: "consent_before", executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ["install-plan"], role: "neutral", skill_ref: null, allowed_skills: [], validator_pin: null, executor: { "type": "tool", "cmd": ["bash", "-c", "exec \"${WICKED_GARDEN_ROOT:?the engine handed this Tool phase no garden root; refusing to resolve wicked-garden from PATH}/scripts/wicked-garden\" run scripts/mcp/install.py --from-run --target \"${WICKED_CONSENT_CHOICE:?the consent gate recorded no install choice}\" --json"] }, instructions: "Install for running — or update the installed copy when this server key is already installed. Nothing has been installed yet: this asks before the install runs, and each answer lists exactly the files it would write (from the install-plan dry run). Install for workers (the default) builds the server from this run's tree into ~/.wicked/mcp-servers/<key>/current, registers it in wicked-crew's MCP tools under its key (an existing entry is re-registered in place; the first use of a changed tool still asks) and writes it into the worker homes' CLI MCP configurations — program-owned files only. Also install into my CLIs does the same and also writes it into your own CLI MCP configurations through wicked-installer (one entry per key, never a second copy; CLI entries carry no secret). Both probe tools/list. The one secret variable <SERVER>_TOKEN is named, never written: when the daemon cannot resolve it the registration is reported as pending with the remedy. Writes happen on the daemon host as the daemon user. Decline for not now — the pull request already delivered stays as it is." },
666
- ],
667
- required_instruments: APP_CHANGE_INSTRUMENTS,
668
- },
669
- ].map(withSystemFlag);
344
+ // ── Built-in workflows (X-MIG M11) ───────────────────────────────────────────
345
+ // No mirrors: the built-ins are the engine's built-in presets, read once at boot and laid over the
346
+ // phase catalog (`core/builtin-catalog.ts`, `loadBuiltinCatalog`). One source; nothing to drift.
670
347
  /**
671
348
  * Chat is not available in this deployment at all — a capability gap, never a bad request.
672
349
  *
@@ -806,95 +483,6 @@ export class GovernanceReplayUnsupportedError extends Error {
806
483
  }
807
484
  /** The engine's own way of reporting a build that cannot do chat, raised at call time. */
808
485
  const ENGINE_CHAT_UNSUPPORTED = /chat unsupported/i;
809
- /**
810
- * Quarantine a pre-#197 `onboarding.json` left in the overlay dir.
811
- *
812
- * This package used to write that file on every launch, baked with ONE repo's absolute paths. It no
813
- * longer does — core declares `{repo_root}` / `{code_graph_db}` and binds them per run
814
- * (wicked-core#179). But the overlay dir is PERSISTENT STATE, and the engine's `load_dir` registers
815
- * whatever it finds there, replacing a compiled def by id, wholesale.
816
- *
817
- * So an upgraded deployment keeps running the last file the old code wrote. Not intermittently:
818
- * EVERY onboarding run indexes whichever repo happened to be registered last before the upgrade.
819
- * Observed exactly that on this host after #197 merged — three fresh registrations in three
820
- * different orgs all indexed `agentic-products/eliza`, the last repo seeded before the fix.
821
- *
822
- * Renamed rather than deleted. The file is almost certainly machine-written, but the overlay dir is
823
- * an operator-facing extension point and silently destroying something out of it is not this
824
- * process's call. The rename is enough to stop the shadow, and leaves the evidence in place.
825
- */
826
- function quarantineStaleOnboardingOverlay() {
827
- const stale = join(workflowOverlayDir(), 'onboarding.json');
828
- if (!existsSync(stale))
829
- return; // the ordinary case on a clean install
830
- // Only a PRE-#197 artifact, never an operator's override. `registerWorkflow()` writes user
831
- // definitions into this same directory, and parking one on every boot would delete a deliberate
832
- // customization each time it was re-registered.
833
- //
834
- // The signature is specific: old crew baked one repo's ABSOLUTE paths into the tool commands. A
835
- // def carrying `{repo_root}` / `{code_graph_db}`, or agent phases, or relative commands, is not
836
- // what this is looking for and is left alone. An operator who hand-writes absolute paths into a
837
- // shared def has written the same bug, and gets the same treatment for the same reason.
838
- let bakedPaths;
839
- try {
840
- const def = JSON.parse(readFileSync(stale, 'utf8'));
841
- bakedPaths = (def.phases ?? [])
842
- .flatMap((p) => (p.executor?.type === 'tool' ? (p.executor.cmd ?? []) : []))
843
- .filter((arg) => arg.startsWith('/'));
844
- }
845
- catch {
846
- // Unparseable: not ours to judge. The engine reports its own load failure.
847
- return;
848
- }
849
- if (bakedPaths.length === 0)
850
- return;
851
- const parked = `${stale}.superseded-by-crew197`;
852
- try {
853
- renameSync(stale, parked);
854
- console.warn(`[onboarding] removed a stale overlay that would have hijacked every onboarding run: ` +
855
- `${stale} → ${parked}. It baked ${bakedPaths[0]} into a def shared by every repo, which is ` +
856
- `what a pre-#197 crew wrote; the engine resolves it in preference to the built-in ` +
857
- `(FINDING-075).`);
858
- }
859
- catch (err) {
860
- // Loud, and non-fatal: the daemon still starts, but every onboarding on this host is wrong
861
- // until the file goes, so the operator has to be told rather than left to discover it.
862
- console.error(`[onboarding] FAILED to remove the stale overlay at ${stale}: ${err instanceof Error ? err.message : String(err)}. Until it is removed by hand, every onboarding run will index the repo baked into it, ` +
863
- `whatever repo the run names (FINDING-075).`);
864
- }
865
- }
866
- /**
867
- * The drop-in ids crew wrote into the overlay dir before DES-TEAMING-002 wave 1 retired them:
868
- * `chat` is the engine's built-in preset now, and `survey-repo`, `memories` and
869
- * `domain-graph-slice` are deleted. Exported for the quarantine test.
870
- */
871
- export const RETIRED_OVERLAY_IDS = ['chat', 'survey-repo', 'memories', 'domain-graph-slice'];
872
- /**
873
- * Park the overlay files older crews wrote for {@link RETIRED_OVERLAY_IDS}.
874
- *
875
- * `_writeBuiltinOverlay` wrote each of these on first launch, and the overlay dir is PERSISTENT: on
876
- * an upgraded host the engine's startup `load_dir` would keep the deleted workflows launchable, and
877
- * `hydrateFromOverlay` would serve them on `GET /workflows` as user workflows, where they are no
878
- * longer on the system list (review of wicked-crew#688). Renamed, not deleted, like the onboarding
879
- * quarantine above: the evidence stays, and a name retired by operator decision (2026-09-26) is
880
- * not a workflow any more — save a preset instead.
881
- */
882
- function quarantineRetiredOverlays() {
883
- for (const id of RETIRED_OVERLAY_IDS) {
884
- const stale = join(workflowOverlayDir(), `${id}.json`);
885
- if (!existsSync(stale))
886
- continue;
887
- const parked = `${stale}.retired-des-teaming-002`;
888
- try {
889
- renameSync(stale, parked);
890
- console.warn(`[workflows] parked ${stale} → ${parked}: \`${id}\` is retired (DES-TEAMING-002 wave 1` +
891
- `${id === 'chat' ? '; chat is the engine\'s built-in preset' : '; the workflow is deleted'}).`);
892
- }
893
- catch (err) {
894
- console.error(`[workflows] FAILED to park the retired overlay ${stale}: ${err instanceof Error ? err.message : String(err)}. The engine will keep resolving \`${id}\` from it until it is removed by hand.`);
895
- }
896
- }
897
- }
898
486
  /** The ids of a workflow's phases that carry a HUMAN gate (`human_confirm` unconditional, the
899
487
  * conditional `human_confirm_if`, or `consent_before` — crew#888, which pauses BEFORE the phase
900
488
  * runs) — i.e. the phases that will PAUSE for a person.
@@ -935,8 +523,6 @@ export class CoreAdapter {
935
523
  listeners = new Set();
936
524
  launchListeners = new Set();
937
525
  closed = false;
938
- /** Built-in workflow ids whose overlay JSON has been written this process lifetime. */
939
- _builtinOverlayWritten = new Set();
940
526
  /**
941
527
  * The estate db the Core actor was spawned over (`CoreAdapterOptions.dbPath` — the `--db`
942
528
  * flag / `~/.wicked-crew/core.db` default). Kept because some engine bindings take the db
@@ -1029,10 +615,6 @@ export class CoreAdapter {
1029
615
  // it publishes `task.dispatched` / consumes `task.completed` around WHATEVER step runner the engine
1030
616
  // wires (the production wrapped-CLI runner OR the deterministic stub), so a stub engine can arm it
1031
617
  // for a fast, offline, deterministic proof of the event path.
1032
- // BEFORE the Core spawns: the actor reads the overlay dir at startup, so a stale
1033
- // `onboarding.json` has to be out of the way by then or it shadows the built-in def.
1034
- quarantineStaleOnboardingOverlay();
1035
- quarantineRetiredOverlays();
1036
618
  // DES-TEAMING-002 T0: the engine gets the daemon's bus on EVERY boot (`WICKED_BUS_DB`); exec
1037
619
  // mediation stays a separate switch (`WICKED_BUS_EXEC`), set only under `engineExec`, and the
1038
620
  // engine's bus gate-judge path follows that switch — never the mere presence of a bus.
@@ -1692,29 +1274,6 @@ export class CoreAdapter {
1692
1274
  workflowId = composed.id;
1693
1275
  }
1694
1276
  opts.workflow = workflowId;
1695
- if (workflowId === input.workflow) {
1696
- // Ensure DROP-IN workflow definitions are present in the Rust overlay dir on first use.
1697
- // Uses a dedicated helper (not registerWorkflow) to avoid adding built-ins to userWorkflows,
1698
- // which would duplicate them in listWorkflows(). The write is skipped after the first call
1699
- // per process lifetime.
1700
- //
1701
- // Ids core seeds itself are excluded: writing them shadows the real def with this stale
1702
- // mirror — see CORE_SEEDED_WORKFLOWS. Core resolves those from its own registry, so there is
1703
- // nothing to write and never was.
1704
- //
1705
- // Skipped entirely on the per-run deliver path above (workflowId !== input.workflow):
1706
- // the composed def carries the base's full phase list, so the run resolves its own
1707
- // registration and the base drop-in is not consulted.
1708
- const builtinDef = CORE_SEEDED_WORKFLOWS.has(input.workflow)
1709
- ? undefined
1710
- : BUILTIN_WORKFLOWS.find((w) => w.id === input.workflow);
1711
- if (builtinDef && !this._builtinOverlayWritten.has(input.workflow)) {
1712
- // Mark before await so concurrent launchRun() calls for the same builtin
1713
- // don't both pass the has() check and race to write the same file.
1714
- this._builtinOverlayWritten.add(input.workflow);
1715
- await this._writeBuiltinOverlay(builtinDef);
1716
- }
1717
- }
1718
1277
  }
1719
1278
  else if (input.deliver === 'pr' && input.plan === undefined) {
1720
1279
  // (A plan carries its deliver step above.) Fail loud, not silent: dropping the option would run to completion with the caller
@@ -1839,11 +1398,9 @@ export class CoreAdapter {
1839
1398
  * (snake_case) — `campaigns/plan.ts` is the one producer of it in this daemon.
1840
1399
  *
1841
1400
  * `workflows` are the composed per-node Tool workflows the def's run_specs reference; they
1842
- * are armed FIRST (hot-register + overlay persist) so the engine can resolve them both now
1843
- * and after a restart — a campaign is durable, so its node workflows must be too. Unlike a
1844
- * deliver-composed def they are NOT purely ephemeral (`_armPerRunWorkflow`), and unlike a
1845
- * user workflow they never enter `userWorkflows` or the catalog (the hydration filter in
1846
- * `readOverlayWorkflows` skips the `campaign-` prefix). */
1401
+ * are armed FIRST, as system presets (X-MIG M11), so the engine resolves them both now and
1402
+ * after a restart — a campaign is durable, so its node workflows must be too. They never enter
1403
+ * `userWorkflows` or the work-mode catalog. */
1847
1404
  async launchCampaign(def, workflows = []) {
1848
1405
  const surface = this._campaigns('Launching a campaign');
1849
1406
  // (review-L1-598 M1) `denial_gate` is a 0.7.27 def field; an older addon's serde IGNORES it and
@@ -1867,21 +1424,22 @@ export class CoreAdapter {
1867
1424
  // the campaign is what the daemon can account for: pinned under `def.id` until its terminal frame.
1868
1425
  return this.handedToEngine('campaign', def.id, () => surface.launchCampaign(JSON.stringify(engineDef)));
1869
1426
  }
1870
- /** Arm one composed campaign-node workflow: validate-then-persist (the FINDING-002 ordering —
1871
- * core's parser is the authority and runs BEFORE the overlay write), overlay file for
1872
- * restart durability, no `userWorkflows` entry. */
1427
+ /** Arm one composed campaign-node workflow as a PRESET (X-MIG M11): the preset store is the
1428
+ * engine's durable record, so a resumed campaign finds its node by name after a restart without
1429
+ * any file overlay. The node's one Tool phase becomes one `run` step carrying its command; the
1430
+ * engine composes it (its step rules apply), and the name keeps it a system preset, off every
1431
+ * picker (`isSystemWorkflow`). */
1873
1432
  async _armCampaignWorkflow(def) {
1874
- const core = this.core;
1875
- const register = core['registerWorkflow'];
1876
- if (typeof register !== 'function') {
1877
- // Unreachable on any addon that carries launchCampaign (registerWorkflow predates it by
1878
- // several releases), but the guard keeps the failure loud if that ever un-holds.
1433
+ if (!this.presetsSupported())
1879
1434
  throw new CampaignsUnsupportedError('Arming a campaign node workflow');
1880
- }
1881
- await register.call(this.core, JSON.stringify(def));
1882
- const dir = workflowOverlayDir();
1883
- await mkdir(dir, { recursive: true });
1884
- await writeFile(join(dir, `${def.id}.json`), JSON.stringify(def, null, 2), 'utf8');
1435
+ const steps = def.phases.map((p) => ({
1436
+ catalog: 'run',
1437
+ id: p.id,
1438
+ kind: p.kind,
1439
+ gate_type: p.gate_type,
1440
+ ...(p.executor !== undefined ? { executor: p.executor } : {}),
1441
+ }));
1442
+ await this.putPreset(def.id, steps, undefined, 'crew-campaign');
1885
1443
  }
1886
1444
  /** Resume a campaign from its persisted state → the campaign status token. */
1887
1445
  resumeCampaign(id) {
@@ -2815,82 +2373,46 @@ export class CoreAdapter {
2815
2373
  return parseEngineJson(await importFn.call(this.core, JSON.stringify({ name, samples, ...(knowledgeDb !== undefined ? { knowledgeDb } : {}) })), 'governanceCorpusImport');
2816
2374
  }
2817
2375
  // ── Workflow viewer + builder (crew#44) ────────────────────────────────────
2818
- // Built-ins are static TypeScript mirrors of workflow.rs. User-registered
2819
- // workflows are added to `userWorkflows` and persisted to disk; the Rust actor
2820
- // picks them up via `register_workflow` NAPI (when available) for immediate use.
2821
- userWorkflows = new Map();
2822
- /** Whether {@link hydrateFromOverlay} has run this process lifetime. */
2823
- overlayHydrated = false;
2824
- /** Load user-registered workflows persisted to the overlay dir into `userWorkflows`, ONCE.
2825
- *
2826
- * FINDING-002 residual: `registerWorkflow` writes each def to the overlay dir AND to the in-memory
2827
- * `userWorkflows` Map, but the Map is process-local and empty on every daemon restart, and nothing
2828
- * read the dir back. So after a restart the Rust actor (which DOES load the overlay dir at startup)
2829
- * would launch a user workflow that `listWorkflows()`/`GET /workflows` no longer showed — it
2830
- * vanished from the registry while remaining runnable. Hydrating from the same dir the writer uses
2831
- * makes the two views agree again. */
2832
- hydrateFromOverlay() {
2833
- if (this.overlayHydrated)
2834
- return;
2835
- this.overlayHydrated = true;
2836
- const builtinIds = new Set(BUILTIN_WORKFLOWS.map((w) => w.id));
2837
- for (const def of readOverlayWorkflows(workflowOverlayDir(), builtinIds)) {
2838
- if (!this.userWorkflows.has(def.id))
2839
- this.userWorkflows.set(def.id, def);
2840
- }
2841
- }
2842
- /** Core's verdict on each hydrated drop-in, run ONCE per process (the same lifetime as
2843
- * {@link hydrateFromOverlay} and as core's own boot-time `load_dir`). */
2844
- overlayVerdicts = null;
2845
- /** id → core's refusal reason, for the drop-ins `judgeOverlayOnce` dropped. */
2846
- overlayRefusals = new Map();
2847
- /** Drop the refused defs from `userWorkflows` and remember why, asking core's parser (crew#718).
2848
- *
2849
- * A build with no `registerWorkflow` binding has no validator to ask, so nothing is judged and
2850
- * the catalog reads exactly as it did before — the same floor {@link registerWorkflow} draws,
2851
- * on the read side where there is no state to corrupt. `registerWorkflow` has been declared
2852
- * (non-optional) in wicked-core-ts since 0.4.0. */
2853
- judgeOverlayOnce() {
2854
- this.overlayVerdicts ??= (async () => {
2855
- this.hydrateFromOverlay();
2856
- const core = this.core;
2857
- const register = core['registerWorkflow'];
2858
- if (typeof register !== 'function')
2376
+ // Built-ins are the engine's built-in presets laid over its phase catalog (X-MIG M11:
2377
+ // `loadBuiltinCatalog`, read once at boot). User-registered workflows are added to
2378
+ // `userWorkflows` and hot-registered with the engine.
2379
+ /** The built-in presets' defs as crew serves them (`core/builtin-catalog.ts`). Empty until
2380
+ * {@link loadBuiltinCatalog} ran — a directly-driven adapter (a test double, the CLI) serves none. */
2381
+ builtinDefs = [];
2382
+ /**
2383
+ * Read the engine's built-in presets and its phase catalog ONCE and derive the built-in workflows
2384
+ * crew serves (`GET /workflows`, the deliver text, delivery candidacy, acceptance). The built-ins
2385
+ * are compiled into the engine, so one read holds until restart. An engine without presets or the
2386
+ * catalog binding serves none (logged), never a guess.
2387
+ */
2388
+ async loadBuiltinCatalog(warn = (m) => console.warn(m)) {
2389
+ try {
2390
+ if (!this.presetsSupported())
2859
2391
  return;
2860
- const { refused } = await judgeOverlayWorkflows([...this.userWorkflows.values()], (json) => register.call(this.core, json));
2861
- for (const r of refused) {
2862
- this.userWorkflows.delete(r.id);
2863
- this.overlayRefusals.set(r.id, r.reason);
2864
- console.warn(`wicked-crew: drop-in workflow '${r.id}' is NOT in the catalog — the engine refused it: ${r.reason}`);
2865
- }
2866
- })();
2867
- return this.overlayVerdicts;
2392
+ const [presets, entries] = await Promise.all([this.listPresets(), this.catalog()]);
2393
+ this.builtinDefs = deriveBuiltinDefs(presets, entries, warn).map(withSystemFlag);
2394
+ }
2395
+ catch (err) {
2396
+ warn(`wicked-crew: could not read the engine's built-in presets — no built-in workflow is served: ${err instanceof Error ? err.message : String(err)}`);
2397
+ }
2868
2398
  }
2869
- /** The workflow catalog `GET /workflows` serves: the defs the ENGINE accepted, plus the drop-ins
2870
- * it refused with its own reason, so an unlaunchable def is named instead of offered (crew#718). */
2399
+ userWorkflows = new Map();
2400
+ /** The workflow catalog `GET /workflows` serves. `unavailable` stays on the wire, always empty:
2401
+ * it named the operator drop-ins the engine refused (crew#718), and drop-ins are retired
2402
+ * (X-MIG M11 — a file in the old overlay dir never runs; the engine names it once at boot). */
2871
2403
  async workflowCatalog() {
2872
- await this.judgeOverlayOnce();
2873
- return {
2874
- workflows: this.listWorkflows(),
2875
- unavailable: [...this.overlayRefusals].map(([id, reason]) => ({ id, reason })),
2876
- };
2877
- }
2878
- /** Core's refusal reason for `id`, or `null` when the engine never refused it (crew#718). */
2879
- async workflowRefusal(id) {
2880
- await this.judgeOverlayOnce();
2881
- return this.overlayRefusals.get(id) ?? null;
2404
+ return { workflows: this.listWorkflows(), unavailable: [] };
2882
2405
  }
2883
2406
  listWorkflows() {
2884
- this.hydrateFromOverlay();
2885
- // Builtins first (stable ordering), but user-registered workflows take precedence when
2886
- // ids conflict — consistent with getWorkflow() which prefers userWorkflows.get().
2407
+ // Built-ins first, and a built-in WINS a name conflict (X-MIG M11, codex r2 on crew A): a launch
2408
+ // naming it resolves the engine's preset before any registered def, so the reader must serve
2409
+ // what runs. Consistent with getWorkflow().
2887
2410
  const seen = new Set();
2888
2411
  const result = [];
2889
- for (const w of BUILTIN_WORKFLOWS) {
2890
- const override = this.userWorkflows.get(w.id);
2412
+ for (const w of this.builtinDefs) {
2891
2413
  if (!seen.has(w.id)) {
2892
2414
  seen.add(w.id);
2893
- result.push(withSystemFlag(override ?? w));
2415
+ result.push(withSystemFlag(w));
2894
2416
  }
2895
2417
  }
2896
2418
  for (const w of this.userWorkflows.values()) {
@@ -2901,50 +2423,22 @@ export class CoreAdapter {
2901
2423
  }
2902
2424
  return result;
2903
2425
  }
2426
+ /** The runtime-registered defs only (the user's and crew's seams'), without the built-in presets. */
2427
+ listRuntimeWorkflows() {
2428
+ return [...this.userWorkflows.values()].map(withSystemFlag);
2429
+ }
2904
2430
  getWorkflow(id) {
2905
- this.hydrateFromOverlay();
2906
- const def = this.userWorkflows.get(id) ?? BUILTIN_WORKFLOWS.find((w) => w.id === id) ?? null;
2431
+ // Preset first, as the engine resolves a launch (see listWorkflows).
2432
+ const def = this.builtinDefs.find((w) => w.id === id) ?? this.userWorkflows.get(id) ?? null;
2907
2433
  return def === null ? null : withSystemFlag(def);
2908
2434
  }
2909
- /** Write a built-in workflow definition to the Rust overlay dir (and hot-register when possible).
2910
- * Unlike registerWorkflow(), this does NOT touch userWorkflows, avoiding duplicates in listWorkflows(). */
2911
- async _writeBuiltinOverlay(def) {
2912
- const dir = workflowOverlayDir();
2913
- await mkdir(dir, { recursive: true });
2914
- const overlayDef = { ...def };
2915
- delete overlayDef.is_system;
2916
- const json = JSON.stringify(overlayDef);
2917
- const core = this.core;
2918
- const register = core['registerWorkflow'];
2919
- // Same validate-before-persist ordering as registerWorkflow (FINDING-002). This path had the
2920
- // identical defect — write first, validate last — which is the P3 shape this campaign keeps
2921
- // finding: N paths, one hardened. A mirror that drifted far enough for core to reject it would
2922
- // otherwise leave an unparseable *.json in the dispatch overlay dir, and core would skip it at
2923
- // the next load. Letting the rejection propagate instead fails the launch with core's own
2924
- // reason, which beats dispatching against a workflow core will silently drop.
2925
- if (typeof register === 'function') {
2926
- await register.call(this.core, json);
2927
- }
2928
- // Deliberately NOT the refusal registerWorkflow makes when the binding is absent, and the
2929
- // difference is the input, not the caller:
2930
- // - a user def is arbitrary runtime input no test has ever seen, so unvalidatable means
2931
- // unsafe to persist;
2932
- // - a built-in mirror is asserted field-for-field against wicked-core's own
2933
- // workflows/<id>.json by tests/builtin-overlay-shadow.test.ts, so its parseability is
2934
- // established at build time rather than needing a runtime check.
2935
- // Refusing here would also break DELIVERY: this write is the only way core resolves a drop-in
2936
- // id, so a refusal turns a silent ungating into a hard "unknown workflow" — exactly the
2937
- // regression FINDING-084's first attempted fix caused.
2938
- await writeFile(join(dir, `${def.id}.json`), JSON.stringify(overlayDef, null, 2), 'utf8');
2939
- }
2940
2435
  /**
2941
2436
  * Arm a PER-RUN composed workflow def (crew#293 deliver, crew#311 deliverable floor) with the
2942
2437
  * engine — hot registration
2943
- * ONLY. Deliberately neither of the other two paths:
2438
+ * ONLY. Deliberately not the other path:
2944
2439
  * - not `registerWorkflow()`: the composed def must not enter `userWorkflows` or the overlay
2945
2440
  * dir — it is launch input for one run, and persisting it would grow the catalog and the
2946
- * overlay dir by one entry per delivered run;
2947
- * - not `_writeBuiltinOverlay()`: same reason, no file.
2441
+ * overlay dir by one entry per delivered run.
2948
2442
  * The engine's `registerWorkflow` binding validates the def server-side and makes it visible
2949
2443
  * to the next `launchRun` with no restart — exactly the lifetime a per-run def needs. (The
2950
2444
  * def is consumed at PLANNING time; a later daemon restart resumes the run from its persisted
@@ -2964,56 +2458,32 @@ export class CoreAdapter {
2964
2458
  await register.call(this.core, JSON.stringify(def));
2965
2459
  }
2966
2460
  /**
2967
- * Register a user-authored workflow: persist to the Rust workflow overlay dir
2968
- * (`~/.config/wicked-core/workflows/<id>.json` or `$WICKED_WORKFLOWS_DIR`),
2969
- * update the in-memory registry, and (when core supports it) register in the
2970
- * Rust actor so runs using this workflow work immediately without a restart.
2461
+ * Register a RUNTIME workflow def with the engine (X-MIG M11): crew's interactive seams that still
2462
+ * hold a def (review, theme) register it at boot. The engine validates it and holds it in memory
2463
+ * for this process; nothing is written to disk (operator drop-ins are retired, and a user saves a
2464
+ * preset instead). Refused for a built-in preset's name, which a launch would never resolve to it.
2971
2465
  */
2972
2466
  async registerWorkflow(def) {
2973
2467
  const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
2974
2468
  if (!SAFE_ID.test(def.id) || def.id.length > 128) {
2975
2469
  throw new Error('workflow id must start with a letter/digit and contain only letters, digits, dots, hyphens, and underscores');
2976
2470
  }
2977
- const dir = workflowOverlayDir();
2978
- await mkdir(dir, { recursive: true });
2979
- const path = join(dir, `${def.id}.json`);
2980
- // Strip `is_system` before writing — the Rust core's overlay format does not recognise that
2981
- // field and silently drops any workflow whose JSON it cannot fully deserialise.
2982
- const overlayDef = { ...def };
2983
- delete overlayDef.is_system;
2984
- const json = JSON.stringify(overlayDef);
2985
- // VALIDATE BEFORE PERSISTING (FINDING-002). This ordering is the whole fix.
2986
- //
2987
- // The write used to come first and `registerWorkflow` last, so core's parser — the only thing
2988
- // that actually knows the overlay schema — ran AFTER the state was already mutated. Observed
2989
- // end to end: POST /api/v1/workflows answered
2990
- // 400 invalid workflow JSON: unknown field `name`, expected `id` or `phases`
2991
- // and the file was on disk anyway, `name` included, and served from `userWorkflows` as though
2992
- // registered. On the next daemon start core could not deserialise its own overlay file:
2993
- // wicked-core: skipping workflow file .../probe-002-persist.json
2994
- // and the workflow VANISHED while its file remained. That is FINDING-002's root cause: not
2995
- // "registration is not durable" but "a rejected request persisted a def core cannot read".
2996
- //
2997
- // Core's parser is the authority, so it is what we ask. Enumerating the accepted fields in TS
2998
- // instead would be a second copy of core's schema — the exact drift this codebase keeps paying
2999
- // for, and `is_system` above is already one hand-maintained instance of it.
2471
+ if (this.builtinDefs.some((w) => w.id === def.id)) {
2472
+ // A def of a built-in preset's name would never run: a launch resolves the preset first.
2473
+ throw new Error(`workflow id '${def.id}' is a built-in preset: a registered def of that name never runs (a launch resolves the preset first) — save your own preset under another name (PUT /presets/:name)`);
2474
+ }
2475
+ // `is_system` is crew's display flag; core's strict def parser rejects the key.
2476
+ const engineDef = { ...def };
2477
+ delete engineDef.is_system;
3000
2478
  const core = this.core;
3001
2479
  const register = core['registerWorkflow'];
3002
2480
  if (typeof register !== 'function') {
3003
- // No validator, so no safe way to persist: an unvalidated def written here is a file core
3004
- // may silently skip at load. Refusing is the honest outcome — and it is loud, unlike the
3005
- // vanishing act it replaces. `registerWorkflow` has been declared (non-optional) in
3006
- // wicked-core-ts since 0.4.0, so this is a real floor, not a routine path.
3007
- throw new Error('this wicked-core build exposes no registerWorkflow binding, so a workflow cannot be ' +
3008
- 'validated before it is written; refusing to persist an unvalidated definition');
3009
- }
3010
- // Throws on a def core rejects — before anything is written or registered.
3011
- await register.call(this.core, json);
3012
- await writeFile(path, JSON.stringify(overlayDef, null, 2), 'utf8');
2481
+ // No validator: refusing is the honest outcome (declared non-optional since core-ts 0.4.0).
2482
+ throw new Error('this wicked-core build exposes no registerWorkflow binding, so a workflow cannot be validated and registered');
2483
+ }
2484
+ // Throws on a def core rejects — before crew's registry holds it.
2485
+ await register.call(this.core, JSON.stringify(engineDef));
3013
2486
  this.userWorkflows.set(def.id, def);
3014
- // (crew#718) This def just passed core's own validator, so any earlier refusal under the same
3015
- // id is stale — the catalog must offer the replacement, not keep naming the version it refused.
3016
- this.overlayRefusals.delete(def.id);
3017
2487
  return def.id;
3018
2488
  }
3019
2489
  /**
@@ -3138,6 +2608,13 @@ export class CoreAdapter {
3138
2608
  // worker_config_root (seat sign-in): absolute path or "" (= engine default). A hand-edited
3139
2609
  // relative path is dropped rather than exported as WICKED_WORKER_HOME, where the engine
3140
2610
  // would resolve it against an arbitrary spawn cwd.
2611
+ // councilSize / councilSpawnStaggerMs (wicked-core#888): the same bounds PUT /settings
2612
+ // enforces; a hand-edited invalid value falls back to the engine default.
2613
+ if ('councilSize' in parsed && !isCouncilSize(parsed.councilSize))
2614
+ delete parsed.councilSize;
2615
+ if ('councilSpawnStaggerMs' in parsed && !isCouncilStaggerMs(parsed.councilSpawnStaggerMs)) {
2616
+ delete parsed.councilSpawnStaggerMs;
2617
+ }
3141
2618
  if ('worker_config_root' in parsed) {
3142
2619
  const r = parsed.worker_config_root;
3143
2620
  if (typeof r !== 'string' || (r !== '' && !isAbsolute(r)))