wicked-crew 0.5.0 → 0.7.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.
Files changed (167) hide show
  1. package/dist/api/audit.d.ts +70 -0
  2. package/dist/api/audit.d.ts.map +1 -0
  3. package/dist/api/audit.js +131 -0
  4. package/dist/api/audit.js.map +1 -0
  5. package/dist/api/auth.d.ts +192 -0
  6. package/dist/api/auth.d.ts.map +1 -0
  7. package/dist/api/auth.js +515 -0
  8. package/dist/api/auth.js.map +1 -0
  9. package/dist/api/gate-cache.d.ts +8 -0
  10. package/dist/api/gate-cache.d.ts.map +1 -1
  11. package/dist/api/gate-cache.js +10 -0
  12. package/dist/api/gate-cache.js.map +1 -1
  13. package/dist/api/guidance-index.d.ts +39 -0
  14. package/dist/api/guidance-index.d.ts.map +1 -0
  15. package/dist/api/guidance-index.js +67 -0
  16. package/dist/api/guidance-index.js.map +1 -0
  17. package/dist/api/open-path.d.ts +34 -0
  18. package/dist/api/open-path.d.ts.map +1 -0
  19. package/dist/api/open-path.js +101 -0
  20. package/dist/api/open-path.js.map +1 -0
  21. package/dist/api/retry-index.d.ts +30 -0
  22. package/dist/api/retry-index.d.ts.map +1 -0
  23. package/dist/api/retry-index.js +45 -0
  24. package/dist/api/retry-index.js.map +1 -0
  25. package/dist/api/routes.d.ts +164 -1
  26. package/dist/api/routes.d.ts.map +1 -1
  27. package/dist/api/routes.js +769 -21
  28. package/dist/api/routes.js.map +1 -1
  29. package/dist/api/run-files.d.ts +63 -0
  30. package/dist/api/run-files.d.ts.map +1 -0
  31. package/dist/api/run-files.js +271 -0
  32. package/dist/api/run-files.js.map +1 -0
  33. package/dist/api/seat-health.d.ts +55 -0
  34. package/dist/api/seat-health.d.ts.map +1 -0
  35. package/dist/api/seat-health.js +273 -0
  36. package/dist/api/seat-health.js.map +1 -0
  37. package/dist/api/seat-signin.d.ts +27 -0
  38. package/dist/api/seat-signin.d.ts.map +1 -0
  39. package/dist/api/seat-signin.js +143 -0
  40. package/dist/api/seat-signin.js.map +1 -0
  41. package/dist/api/server.d.ts +190 -3
  42. package/dist/api/server.d.ts.map +1 -1
  43. package/dist/api/server.js +365 -11
  44. package/dist/api/server.js.map +1 -1
  45. package/dist/api/stall-watchdog.d.ts +62 -0
  46. package/dist/api/stall-watchdog.d.ts.map +1 -0
  47. package/dist/api/stall-watchdog.js +138 -0
  48. package/dist/api/stall-watchdog.js.map +1 -0
  49. package/dist/cli/index.js +171 -12
  50. package/dist/cli/index.js.map +1 -1
  51. package/dist/cli/mcp.d.ts +14 -0
  52. package/dist/cli/mcp.d.ts.map +1 -0
  53. package/dist/cli/mcp.js +119 -0
  54. package/dist/cli/mcp.js.map +1 -0
  55. package/dist/core/adapter.d.ts +87 -10
  56. package/dist/core/adapter.d.ts.map +1 -1
  57. package/dist/core/adapter.js +347 -28
  58. package/dist/core/adapter.js.map +1 -1
  59. package/dist/core/bridge-reaper.d.ts +134 -0
  60. package/dist/core/bridge-reaper.d.ts.map +1 -0
  61. package/dist/core/bridge-reaper.js +286 -0
  62. package/dist/core/bridge-reaper.js.map +1 -0
  63. package/dist/core/deliver.d.ts +118 -0
  64. package/dist/core/deliver.d.ts.map +1 -0
  65. package/dist/core/deliver.js +241 -0
  66. package/dist/core/deliver.js.map +1 -0
  67. package/dist/core/deliverable-floor.d.ts +103 -0
  68. package/dist/core/deliverable-floor.d.ts.map +1 -0
  69. package/dist/core/deliverable-floor.js +173 -0
  70. package/dist/core/deliverable-floor.js.map +1 -0
  71. package/dist/core/exec.d.ts +2 -0
  72. package/dist/core/exec.d.ts.map +1 -1
  73. package/dist/core/exec.js.map +1 -1
  74. package/dist/core/types.d.ts +92 -352
  75. package/dist/core/types.d.ts.map +1 -1
  76. package/dist/core/types.js +14 -4
  77. package/dist/core/types.js.map +1 -1
  78. package/dist/interactive/bridge-pool.d.ts +99 -0
  79. package/dist/interactive/bridge-pool.d.ts.map +1 -0
  80. package/dist/interactive/bridge-pool.js +244 -0
  81. package/dist/interactive/bridge-pool.js.map +1 -0
  82. package/dist/interactive/bridge-root.d.ts +36 -0
  83. package/dist/interactive/bridge-root.d.ts.map +1 -0
  84. package/dist/interactive/bridge-root.js +48 -0
  85. package/dist/interactive/bridge-root.js.map +1 -0
  86. package/dist/interactive/chat-events.d.ts +207 -0
  87. package/dist/interactive/chat-events.d.ts.map +1 -0
  88. package/dist/interactive/chat-events.js +769 -0
  89. package/dist/interactive/chat-events.js.map +1 -0
  90. package/dist/interactive/demo-events.d.ts +283 -0
  91. package/dist/interactive/demo-events.d.ts.map +1 -0
  92. package/dist/interactive/demo-events.js +889 -0
  93. package/dist/interactive/demo-events.js.map +1 -0
  94. package/dist/interactive/draft-events.d.ts +224 -0
  95. package/dist/interactive/draft-events.d.ts.map +1 -0
  96. package/dist/interactive/draft-events.js +793 -0
  97. package/dist/interactive/draft-events.js.map +1 -0
  98. package/dist/interactive/edit-events.d.ts +194 -0
  99. package/dist/interactive/edit-events.d.ts.map +1 -0
  100. package/dist/interactive/edit-events.js +601 -0
  101. package/dist/interactive/edit-events.js.map +1 -0
  102. package/dist/interactive/ledger.d.ts +39 -0
  103. package/dist/interactive/ledger.d.ts.map +1 -0
  104. package/dist/interactive/ledger.js +93 -0
  105. package/dist/interactive/ledger.js.map +1 -0
  106. package/dist/interactive/proxy-routes.d.ts +39 -0
  107. package/dist/interactive/proxy-routes.d.ts.map +1 -0
  108. package/dist/interactive/proxy-routes.js +189 -0
  109. package/dist/interactive/proxy-routes.js.map +1 -0
  110. package/dist/interactive/repo-snapshot.d.ts +100 -0
  111. package/dist/interactive/repo-snapshot.d.ts.map +1 -0
  112. package/dist/interactive/repo-snapshot.js +289 -0
  113. package/dist/interactive/repo-snapshot.js.map +1 -0
  114. package/dist/interactive/ws-relay.d.ts +85 -0
  115. package/dist/interactive/ws-relay.d.ts.map +1 -0
  116. package/dist/interactive/ws-relay.js +191 -0
  117. package/dist/interactive/ws-relay.js.map +1 -0
  118. package/dist/projects/activity.d.ts +29 -0
  119. package/dist/projects/activity.d.ts.map +1 -0
  120. package/dist/projects/activity.js +172 -0
  121. package/dist/projects/activity.js.map +1 -0
  122. package/dist/projects/charter.d.ts +28 -0
  123. package/dist/projects/charter.d.ts.map +1 -0
  124. package/dist/projects/charter.js +53 -0
  125. package/dist/projects/charter.js.map +1 -0
  126. package/dist/projects/events.d.ts +55 -0
  127. package/dist/projects/events.d.ts.map +1 -0
  128. package/dist/projects/events.js +141 -0
  129. package/dist/projects/events.js.map +1 -0
  130. package/dist/projects/graph-paths.d.ts +92 -0
  131. package/dist/projects/graph-paths.d.ts.map +1 -0
  132. package/dist/projects/graph-paths.js +130 -0
  133. package/dist/projects/graph-paths.js.map +1 -0
  134. package/dist/projects/graph.d.ts +179 -0
  135. package/dist/projects/graph.d.ts.map +1 -0
  136. package/dist/projects/graph.js +775 -0
  137. package/dist/projects/graph.js.map +1 -0
  138. package/dist/projects/membership-index.d.ts +25 -0
  139. package/dist/projects/membership-index.d.ts.map +1 -0
  140. package/dist/projects/membership-index.js +46 -0
  141. package/dist/projects/membership-index.js.map +1 -0
  142. package/dist/projects/routes.d.ts +97 -0
  143. package/dist/projects/routes.d.ts.map +1 -0
  144. package/dist/projects/routes.js +510 -0
  145. package/dist/projects/routes.js.map +1 -0
  146. package/dist/projects/settings.d.ts +32 -0
  147. package/dist/projects/settings.d.ts.map +1 -0
  148. package/dist/projects/settings.js +64 -0
  149. package/dist/projects/settings.js.map +1 -0
  150. package/dist/qe/acceptance.d.ts +137 -0
  151. package/dist/qe/acceptance.d.ts.map +1 -0
  152. package/dist/qe/acceptance.js +249 -0
  153. package/dist/qe/acceptance.js.map +1 -0
  154. package/dist/qe/gate-events.d.ts +111 -0
  155. package/dist/qe/gate-events.d.ts.map +1 -0
  156. package/dist/qe/gate-events.js +168 -0
  157. package/dist/qe/gate-events.js.map +1 -0
  158. package/dist/qe/ledger.d.ts +100 -0
  159. package/dist/qe/ledger.d.ts.map +1 -0
  160. package/dist/qe/ledger.js +154 -0
  161. package/dist/qe/ledger.js.map +1 -0
  162. package/dist/studio/assets/index-8p8uwCxG.js +530 -0
  163. package/dist/studio/assets/index-D6S9zUtO.css +32 -0
  164. package/dist/studio/index.html +5 -3
  165. package/package.json +10 -4
  166. package/dist/studio/assets/index-DaaUU8Ep.css +0 -32
  167. package/dist/studio/assets/index-Fu5DRC00.js +0 -423
@@ -7,6 +7,8 @@ import { homedir } from 'node:os';
7
7
  import { randomUUID } from 'node:crypto';
8
8
  import { DEFAULT_SETTINGS } from './types.js';
9
9
  import { execCapped } from './exec.js';
10
+ import { composeDeliverWorkflow, DELIVER_PHASE_ID } from './deliver.js';
11
+ import { composeDeliverableFloor } from './deliverable-floor.js';
10
12
  /** Resolved path under the user's home directory. */
11
13
  function wickedDir(...parts) {
12
14
  const home = process.env.HOME ?? process.env.USERPROFILE ?? '/tmp';
@@ -43,7 +45,9 @@ function workflowOverlayDir() {
43
45
  const home = process.env.HOME ?? process.env.USERPROFILE ?? '/tmp';
44
46
  return join(home, '.config', 'wicked-core', 'workflows');
45
47
  }
46
- function settingsFilePath() {
48
+ /** Where the daemon persists system settings. Exported so tests write fixtures to the ONE
49
+ * spelling of this path (the tests/ tree is audited against spelling core paths itself). */
50
+ export function settingsFilePath() {
47
51
  return join(homedir(), '.config', 'wicked-core', 'settings.json');
48
52
  }
49
53
  /** Read user-registered workflow overlays from `dir` (the same dir `registerWorkflow` writes to).
@@ -109,6 +113,58 @@ function locateWickedCoreExe() {
109
113
  // `subscribe` seam has a blast radius of exactly one file.
110
114
  const require = createRequire(import.meta.url);
111
115
  const { Core } = require('wicked-core-ts');
116
+ /**
117
+ * Does the installed wicked-core-ts addon understand `LaunchOptions.extraWriteRoots` (≥ 0.6.1)?
118
+ *
119
+ * A napi object silently IGNORES fields the addon doesn't declare, so passing the option to an
120
+ * older addon would launch a run whose boundary never widened — the worker then gets a boundary
121
+ * deny on the very deliverable path the caller declared (the crew#263 failure, resurrected
122
+ * silently). Fail CLOSED on version instead: same doctrine as the `projectId` guard below.
123
+ */
124
+ function addonSupportsExtraWriteRoots() {
125
+ return addonAtLeast(0, 6, 1);
126
+ }
127
+ /**
128
+ * Does the installed addon understand `LaunchOptions.projectGraph` (≥ 0.7.1)?
129
+ *
130
+ * Same doctrine, different cost of being wrong. A silently-dropped binding does not deny anything —
131
+ * it launches a run whose workers quietly see one repo while the launcher recorded that they see
132
+ * the project. The daemon would then log "bound to the project graph as 'wicked-core'" for a run
133
+ * that never was, which is worse than not binding at all: the operator's evidence about what the
134
+ * run could see becomes false. Fail closed and keep the log honest.
135
+ */
136
+ function addonSupportsProjectGraph() {
137
+ return addonAtLeast(0, 7, 1);
138
+ }
139
+ /**
140
+ * Is the installed `wicked-core-ts` at least `maj.min.pat`?
141
+ *
142
+ * ONE version reader for every capability gate, because the parsing rule here is load-bearing and
143
+ * was already got wrong once: `split('.').map(Number)` returns NaN on a pre-release or build suffix
144
+ * (`0.6.1-beta.1`), which fails a perfectly capable addon closed (Copilot on #263). Matching only
145
+ * the numeric MAJOR.MINOR.PATCH prefix fixes that, and a second copy of the fix is a second chance
146
+ * to lose it. No match ⇒ unparseable ⇒ fail closed.
147
+ */
148
+ function addonAtLeast(maj, min, pat) {
149
+ try {
150
+ const pkg = require('wicked-core-ts/package.json');
151
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(pkg.version ?? '');
152
+ if (!m)
153
+ return false;
154
+ const got = [Number(m[1]), Number(m[2]), Number(m[3])];
155
+ const want = [maj, min, pat];
156
+ for (let i = 0; i < 3; i += 1) {
157
+ if (got[i] > want[i])
158
+ return true;
159
+ if (got[i] < want[i])
160
+ return false;
161
+ }
162
+ return true;
163
+ }
164
+ catch {
165
+ return false;
166
+ }
167
+ }
112
168
  // ── Built-in workflow definitions (crew#44) ──────────────────────────────────
113
169
  // Static mirrors of wicked-core workflow defs: feature, bug, migration, survey-repo,
114
170
  // domain-graph-slice, memories, collab, onboarding, chat, and domain-extraction.
@@ -149,7 +205,7 @@ const CORE_SEEDED_WORKFLOWS = new Set(['feature', 'bug', 'migration', 'onboardin
149
205
  * moves. This is display only: as of FINDING-049 these defs are never written to core's overlay dir
150
206
  * (see CORE_SEEDED_WORKFLOWS), so a stale value here cannot reach the engine.
151
207
  */
152
- const EVIDENCE_FLOOR_PIN = '2fcde907d57f3ee2';
208
+ const EVIDENCE_FLOOR_PIN = 'e2e7af1db9e48454';
153
209
  export const BUILTIN_WORKFLOWS = [
154
210
  {
155
211
  id: 'chat',
@@ -184,7 +240,7 @@ export const BUILTIN_WORKFLOWS = [
184
240
  { 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 },
185
241
  { 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: null },
186
242
  { 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 },
187
- { 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: null },
243
+ { 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 },
188
244
  { 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 },
189
245
  ],
190
246
  },
@@ -278,7 +334,7 @@ export const BUILTIN_WORKFLOWS = [
278
334
  { 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 },
279
335
  { 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 },
280
336
  { 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 },
281
- { id: 'coverage', kind: 'test', gate_type: 'execution', gate: { human_confirm_if: 'verdict_not_pass' }, executes_code: false, verified_evidence: true, required_deliverables: ['coverage-report.json'], depends_on: ['extract'], role: 'evaluator', skill_ref: 'wicked-garden-domain-coverage', allowed_skills: [], validator_pin: '49b61ba3ab5264e4' },
337
+ { id: 'coverage', kind: 'test', gate_type: 'execution', gate: { human_confirm_if: 'verdict_not_pass' }, executes_code: false, verified_evidence: true, required_deliverables: ['coverage-report.json'], depends_on: ['extract'], role: 'evaluator', skill_ref: 'wicked-garden-domain-coverage', allowed_skills: [], validator_pin: 'bfe4020a365c598b' },
282
338
  // domain-graph is a DETERMINISTIC Tool that runs `wicked-core domain-graph`, which PERSISTS the
283
339
  // domain/requirement/rule graph into the repo store (core#237) — not an LLM skill that could hit
284
340
  // a non-persisting hermetic fallback. Mirrors wicked-core/workflows/domain-extraction.json.
@@ -304,6 +360,17 @@ export class ChatUnsupportedError extends Error {
304
360
  this.name = 'ChatUnsupportedError';
305
361
  }
306
362
  }
363
+ /**
364
+ * The project surface (DES-PROJECT-001) is not available in this deployment — the installed
365
+ * wicked-core-ts predates 0.6.0. Typed for the same reason as `ChatUnsupportedError`: the routes
366
+ * answer 501 ("upgrade the engine") on this, never 400 ("fix your request").
367
+ */
368
+ export class ProjectsUnsupportedError extends Error {
369
+ constructor(what) {
370
+ super(`${what} is not supported by this wicked-core build (needs wicked-core-ts >= 0.6.0)`);
371
+ this.name = 'ProjectsUnsupportedError';
372
+ }
373
+ }
307
374
  /**
308
375
  * `resolveElicitation` is not available in this deployment — the NAPI binding has not
309
376
  * landed in the installed `wicked-core-ts` yet (DES-002 §4 P-1 stub).
@@ -376,13 +443,6 @@ function quarantineStaleOnboardingOverlay() {
376
443
  `whatever repo the run names (FINDING-075).`);
377
444
  }
378
445
  }
379
- /**
380
- * The single isolation boundary over wicked-core-ts. It holds the ONE `Core`
381
- * handle, makes the ONE `subscribe()` call for the whole process, parses each
382
- * CoreEvent, and re-emits it to registered in-daemon listeners. Every REST
383
- * endpoint and the WS fan-out funnel through this stable API — so when the
384
- * in-flight core-ts subscribe/teardown signature lands, only this file changes.
385
- */
386
446
  /** The ids of a workflow's phases that carry a HUMAN gate (`human_confirm` unconditional, or the
387
447
  * conditional `human_confirm_if`) — i.e. the phases that will PAUSE for a person.
388
448
  *
@@ -398,6 +458,13 @@ export function humanGatePhaseIds(wf) {
398
458
  ('human_confirm' in p.gate || 'human_confirm_if' in p.gate))
399
459
  .map((p) => p.id);
400
460
  }
461
+ /**
462
+ * The single isolation boundary over wicked-core-ts. It holds the ONE `Core`
463
+ * handle, makes the ONE `subscribe()` call for the whole process, parses each
464
+ * CoreEvent, and re-emits it to registered in-daemon listeners. Every REST
465
+ * endpoint and the WS fan-out funnel through this stable API — so when the
466
+ * in-flight core-ts subscribe/teardown signature lands, only this file changes.
467
+ */
401
468
  export class CoreAdapter {
402
469
  core;
403
470
  subscription;
@@ -490,25 +557,138 @@ export class CoreAdapter {
490
557
  opts.humanConfirm = input.humanConfirm;
491
558
  if (input.repoRef !== undefined)
492
559
  opts.repoRef = input.repoRef;
493
- if (input.workflow !== undefined) {
494
- opts.workflow = input.workflow;
495
- // Ensure DROP-IN workflow definitions are present in the Rust overlay dir on first use.
496
- // Uses a dedicated helper (not registerWorkflow) to avoid adding built-ins to userWorkflows,
497
- // which would duplicate them in listWorkflows(). The write is skipped after the first call
498
- // per process lifetime.
560
+ if (input.projectId !== undefined) {
561
+ // Fail CLOSED on an old addon: silently dropping projectId would launch an unfiled run the
562
+ // caller believed was filed — the exact failure §2.2 exists to prevent.
563
+ if (typeof this.core.projectCreate !== 'function') {
564
+ throw new ProjectsUnsupportedError('Filing a run into a project');
565
+ }
566
+ opts.projectId = input.projectId;
567
+ }
568
+ if (input.extraWriteRoots !== undefined && input.extraWriteRoots.length > 0) {
569
+ // Fail CLOSED on an old addon (napi ignores undeclared fields): a silently-dropped
570
+ // widening resurrects the crew#263 boundary deny on the declared deliverable path.
571
+ if (!addonSupportsExtraWriteRoots()) {
572
+ throw new Error('extraWriteRoots needs wicked-core-ts >= 0.6.1; the installed addon would silently ' +
573
+ 'ignore it and the run would be denied writing its own deliverable');
574
+ }
575
+ opts.extraWriteRoots = input.extraWriteRoots;
576
+ }
577
+ if (input.projectGraph !== undefined) {
578
+ // Fail CLOSED on an old addon (napi ignores undeclared fields): the run would silently get
579
+ // its repo's graph while everything on this side recorded that it got the project's.
580
+ if (!addonSupportsProjectGraph()) {
581
+ throw new Error('projectGraph needs wicked-core-ts >= 0.7.1; the installed addon would silently ignore ' +
582
+ 'it and the run would see one repo while the daemon logged that it saw the project');
583
+ }
584
+ // The two invariants the TYPE cannot express, enforced where the launch is actually
585
+ // assembled (Copilot on #327). Both are cross-field, so `projectGraph`'s own shape can never
586
+ // carry them, and `core/types.ts` says as much rather than pretending the compiler helps.
499
587
  //
500
- // Ids core seeds itself are excluded: writing them shadows the real def with this stale
501
- // mirror — see CORE_SEEDED_WORKFLOWS. Core resolves those from its own registry, so there is
502
- // nothing to write and never was.
503
- const builtinDef = CORE_SEEDED_WORKFLOWS.has(input.workflow)
504
- ? undefined
505
- : BUILTIN_WORKFLOWS.find((w) => w.id === input.workflow);
506
- if (builtinDef && !this._builtinOverlayWritten.has(input.workflow)) {
507
- // Mark before await so concurrent launchRun() calls for the same builtin
508
- // don't both pass the has() check and race to write the same file.
509
- this._builtinOverlayWritten.add(input.workflow);
510
- await this._writeBuiltinOverlay(builtinDef);
588
+ // Loud, not lenient. Every one of these is a CALLER bug, and the failure it would otherwise
589
+ // produce is the silent kind this whole slice exists to end: a run that reports one thing
590
+ // about what it could see and observes another. `resolveProjectGraphBinding` — the only
591
+ // producer today — satisfies both on every arm, so a throw here means a new caller got it
592
+ // wrong, which is exactly when you want to hear about it.
593
+ if (input.projectId === undefined) {
594
+ throw new Error('projectGraph is the PROJECT\'s graph, so the run must be filed into that project: ' +
595
+ 'pass projectId alongside it, or omit projectGraph and let the run use its repo graph');
596
+ }
597
+ if (input.repoRef !== undefined && input.projectGraph.repoLabel === undefined) {
598
+ throw new Error(`a repo-bound run needs projectGraph.repoLabel: without it the engine cannot confirm the ` +
599
+ `project graph holds '${input.repoRef}', and a graph that does not would answer ` +
600
+ `"not found" about the run's own worktree`);
601
+ }
602
+ opts.projectGraph = input.projectGraph;
603
+ }
604
+ if (input.workflow !== undefined) {
605
+ let workflowId = input.workflow;
606
+ // Both per-run compositions (deliver + deliverable floor) fold into ONE def and ONE
607
+ // registration: composing twice would arm two ids and launch the second, leaving the
608
+ // first as engine litter. `composed` stays null until something actually composes, which
609
+ // is what keeps the undelivered/unfloored launch byte-for-byte the old path.
610
+ const requireDeliverables = input.requireDeliverables ?? [];
611
+ let composed = null;
612
+ if (input.deliver === 'pr' || requireDeliverables.length > 0) {
613
+ const base = this.getWorkflow(input.workflow);
614
+ if (base === null) {
615
+ throw new Error(input.deliver === 'pr'
616
+ ? `deliver: "pr" needs a registered workflow to append the deliver phase to — ` +
617
+ `'${input.workflow}' is not registered`
618
+ : `requireDeliverables needs a registered workflow to append the deliverable ` +
619
+ `floor to — '${input.workflow}' is not registered`);
620
+ }
621
+ composed = base;
622
+ }
623
+ if (composed !== null && requireDeliverables.length > 0) {
624
+ // THE DELIVERABLE FLOOR (crew#311): "done" is re-derived from the artifact. The
625
+ // launcher declared what this run must produce, so a deterministic Tool phase appended
626
+ // here re-verifies that those files EXIST and carry bytes, and fails the run naming
627
+ // what was expected and what was found when they do not. Composed BEFORE the deliver
628
+ // phase below so the floor sits ahead of it: a run that produced nothing must fail
629
+ // rather than open a pull request over an empty branch.
630
+ composed = composeDeliverableFloor(composed, input.sessionId, requireDeliverables);
511
631
  }
632
+ if (composed !== null && input.deliver === 'pr') {
633
+ // First-class delivery (crew#293): compose a PER-RUN def — the selected workflow's
634
+ // phases plus the hardened deliver Tool phase appended last — and arm it with the
635
+ // engine under a run-scoped id. The shared def is NEVER mutated, and the composed def
636
+ // is hot-registered only (no overlay file, no userWorkflows entry): it exists for this
637
+ // launch, not for the catalog.
638
+ if (composed.phases.some((p) => p.id === DELIVER_PHASE_ID)) {
639
+ // The def already delivers (an operator's own feature-pr-style overlay). Appending a
640
+ // second `deliver` phase would collide on id — launch the def as-is; the intent
641
+ // ("this run opens its PR") is already satisfied.
642
+ }
643
+ else {
644
+ // The run's intent rides along so the commit the deliver phase makes NAMES what it
645
+ // delivered (`wicked-crew run <id>: <intent>`, #318) instead of being an anonymous
646
+ // blob. Composition stays DEFERRED (#319): deliver and the deliverable floor fold
647
+ // into one def and one registration below, never two armed ids.
648
+ composed = composeDeliverWorkflow(composed, input.sessionId, input.problem);
649
+ }
650
+ }
651
+ if (composed !== null && composed.id !== input.workflow) {
652
+ await this._armPerRunWorkflow(composed);
653
+ workflowId = composed.id;
654
+ }
655
+ opts.workflow = workflowId;
656
+ if (workflowId === input.workflow) {
657
+ // Ensure DROP-IN workflow definitions are present in the Rust overlay dir on first use.
658
+ // Uses a dedicated helper (not registerWorkflow) to avoid adding built-ins to userWorkflows,
659
+ // which would duplicate them in listWorkflows(). The write is skipped after the first call
660
+ // per process lifetime.
661
+ //
662
+ // Ids core seeds itself are excluded: writing them shadows the real def with this stale
663
+ // mirror — see CORE_SEEDED_WORKFLOWS. Core resolves those from its own registry, so there is
664
+ // nothing to write and never was.
665
+ //
666
+ // Skipped entirely on the per-run deliver path above (workflowId !== input.workflow):
667
+ // the composed def carries the base's full phase list, so the run resolves its own
668
+ // registration and the base drop-in is not consulted.
669
+ const builtinDef = CORE_SEEDED_WORKFLOWS.has(input.workflow)
670
+ ? undefined
671
+ : BUILTIN_WORKFLOWS.find((w) => w.id === input.workflow);
672
+ if (builtinDef && !this._builtinOverlayWritten.has(input.workflow)) {
673
+ // Mark before await so concurrent launchRun() calls for the same builtin
674
+ // don't both pass the has() check and race to write the same file.
675
+ this._builtinOverlayWritten.add(input.workflow);
676
+ await this._writeBuiltinOverlay(builtinDef);
677
+ }
678
+ }
679
+ }
680
+ else if (input.deliver === 'pr') {
681
+ // Fail loud, not silent: dropping the option would run to completion with the caller
682
+ // believing a PR opens at the end — the exact operator gap crew#293 closes.
683
+ throw new Error('deliver: "pr" requires a workflow — a free-text run has no def to append the deliver phase to');
684
+ }
685
+ else if ((input.requireDeliverables ?? []).length > 0) {
686
+ // Same rule for the floor (Copilot, #319): a caller that declares what a run MUST produce
687
+ // and gets no enforcement is worse off than one that never declared it — it would watch a
688
+ // free-text run complete and believe the artifacts were re-derived. There is no def to
689
+ // append the floor phase to, so refuse the launch instead of silently ignoring the option.
690
+ throw new Error('requireDeliverables requires a workflow — a free-text run has no def to append the ' +
691
+ 'deliverable floor to');
512
692
  }
513
693
  return this.core.launchRun(opts);
514
694
  }
@@ -524,6 +704,20 @@ export class CoreAdapter {
524
704
  cancelRun(runId) {
525
705
  return this.core.cancelRun(runId);
526
706
  }
707
+ /**
708
+ * Archive (or unarchive) a TERMINAL run (crew#265) — write-off, not delete. Resolves `true`
709
+ * when the session existed, `false` for an unknown id (→ 404); REJECTS when the run is
710
+ * non-terminal (the route answers 409). Fail-closed on an old addon: method-presence guard,
711
+ * same doctrine as `injectWorkerMessage`.
712
+ */
713
+ async archiveRun(runId, archived, note) {
714
+ const archive = this.core.archiveRun;
715
+ if (typeof archive !== 'function') {
716
+ throw new Error('Run archival needs wicked-core-ts >= 0.6.2; this build does not support it');
717
+ }
718
+ // JSON-encoded bool by contract (parse, never truthiness-test — 'false' is truthy).
719
+ return JSON.parse(await archive.call(this.core, runId, archived, note ?? null));
720
+ }
527
721
  /** Inject an operator message into a run's active PTY worker(s). target="all" or a CLI key. */
528
722
  injectWorkerMessage(runId, message, target) {
529
723
  if (typeof this.core.injectWorkerMessage !== 'function') {
@@ -588,6 +782,87 @@ export class CoreAdapter {
588
782
  async workOutput(unitId) {
589
783
  return JSON.parse(await this.core.workOutput(unitId));
590
784
  }
785
+ // ── Projects (DES-PROJECT-001) ──────────────────────────────────────────────
786
+ // 1:1 maps of the 0.6.0 engine surface. Writes ride the single-writer actor;
787
+ // reads are read-only store opens inside the addon. Every method throws
788
+ // ProjectsUnsupportedError on a pre-0.6.0 addon (routes answer 501).
789
+ /** True when the installed addon carries the project surface (0.6.0+). */
790
+ projectsSupported() {
791
+ return typeof this.core.projectCreate === 'function';
792
+ }
793
+ requireProjects(fn, what) {
794
+ if (typeof fn !== 'function')
795
+ throw new ProjectsUnsupportedError(what);
796
+ return fn;
797
+ }
798
+ async projectCreate(name, description) {
799
+ const fn = this.requireProjects(this.core.projectCreate, 'Creating a project');
800
+ return JSON.parse(await fn.call(this.core, name, description ?? null));
801
+ }
802
+ async projectUpdate(id, patch) {
803
+ const fn = this.requireProjects(this.core.projectUpdate, 'Updating a project');
804
+ return JSON.parse(await fn.call(this.core, id, patch.name ?? null, patch.description ?? null, patch.status ?? null));
805
+ }
806
+ /** Every stored project (all statuses, newest first). The `default` project is NOT here — the
807
+ * route layer synthesizes it (ADR §7: computed, never stored). */
808
+ async projectList() {
809
+ const fn = this.requireProjects(this.core.projectList, 'Listing projects');
810
+ return JSON.parse(await fn.call(this.core));
811
+ }
812
+ async projectGet(id) {
813
+ const fn = this.requireProjects(this.core.projectGet, 'Reading a project');
814
+ return JSON.parse(await fn.call(this.core, id));
815
+ }
816
+ async projectMembers(projectId) {
817
+ const fn = this.requireProjects(this.core.projectMembers, 'Listing project members');
818
+ return JSON.parse(await fn.call(this.core, projectId));
819
+ }
820
+ /** Attach a member. `created:false` = the idempotent duplicate hit (emit no event for it). */
821
+ async projectMemberAttach(projectId, kind, ref, meta, attachedBy) {
822
+ const fn = this.requireProjects(this.core.projectMemberAttach, 'Attaching a project member');
823
+ return JSON.parse(await fn.call(this.core, projectId, kind, ref, meta !== undefined ? JSON.stringify(meta) : null, attachedBy ?? null));
824
+ }
825
+ /** Detach (tombstone). `false` = no such live member on that project (the route's 404). */
826
+ async projectMemberDetach(projectId, memberId) {
827
+ const fn = this.requireProjects(this.core.projectMemberDetach, 'Detaching a project member');
828
+ return JSON.parse(await fn.call(this.core, projectId, memberId));
829
+ }
830
+ /** The projects holding a live `(kind, ref)` membership — the run→project reverse read. */
831
+ async memberProjects(kind, ref) {
832
+ const fn = this.requireProjects(this.core.memberProjects, 'Resolving a member’s projects');
833
+ return JSON.parse(await fn.call(this.core, kind, ref));
834
+ }
835
+ /**
836
+ * Durable interaction requests (ADR §5.3), newest first — or `null` when this addon predates
837
+ * the binding (the `runEvents` convention: a missing capability must stay distinguishable from
838
+ * "no open prompts", or the caches' fallback chain misreports upgrades as empty inboxes).
839
+ */
840
+ async interactionRequests(sessionId, status) {
841
+ if (typeof this.core.interactionRequests !== 'function')
842
+ return null;
843
+ return JSON.parse(await this.core.interactionRequests(sessionId ?? null, status ?? null));
844
+ }
845
+ // ── The foundation record (ADR §3.2): charter writes + record probes ────────
846
+ /** Capture a memory at a STRICT `kind:id[/…]` scope (the engine refuses malformed segments). */
847
+ async captureMemory(content, scope) {
848
+ const fn = this.requireProjects(this.core.captureMemory, 'Capturing a memory');
849
+ await fn.call(this.core, content, scope);
850
+ }
851
+ /** Memories within `scope`'s subtree, newest first — the ADR's memory.coverage probe. */
852
+ async listMemories(scope, limit) {
853
+ const fn = this.requireProjects(this.core.listMemories, 'Listing memories');
854
+ return JSON.parse(await fn.call(this.core, scope, limit));
855
+ }
856
+ /** Ingest a document (title + chunks) into the knowledge store. Returns the chunk count. */
857
+ async ingestKnowledge(title, chunks) {
858
+ const fn = this.requireProjects(this.core.ingestKnowledge, 'Ingesting knowledge');
859
+ return JSON.parse(await fn.call(this.core, title, JSON.stringify(chunks)));
860
+ }
861
+ /** Recall up to `k` knowledge chunks relevant to `query`. */
862
+ async recallKnowledge(query, k) {
863
+ const fn = this.requireProjects(this.core.recallKnowledge, 'Recalling knowledge');
864
+ return JSON.parse(await fn.call(this.core, query, k));
865
+ }
591
866
  // ── Chat sessions (core#134 / crew#165) ────────────────────────────────────
592
867
  async chatOpen(chatId, clis, cwd) {
593
868
  const raw = await this.core.chatOpen(chatId, JSON.stringify(clis), cwd ?? null);
@@ -968,6 +1243,32 @@ export class CoreAdapter {
968
1243
  // regression FINDING-084's first attempted fix caused.
969
1244
  await writeFile(join(dir, `${def.id}.json`), JSON.stringify(overlayDef, null, 2), 'utf8');
970
1245
  }
1246
+ /**
1247
+ * Arm a PER-RUN composed workflow def (crew#293 deliver, crew#311 deliverable floor) with the
1248
+ * engine — hot registration
1249
+ * ONLY. Deliberately neither of the other two paths:
1250
+ * - not `registerWorkflow()`: the composed def must not enter `userWorkflows` or the overlay
1251
+ * dir — it is launch input for one run, and persisting it would grow the catalog and the
1252
+ * overlay dir by one entry per delivered run;
1253
+ * - not `_writeBuiltinOverlay()`: same reason, no file.
1254
+ * The engine's `registerWorkflow` binding validates the def server-side and makes it visible
1255
+ * to the next `launchRun` with no restart — exactly the lifetime a per-run def needs. (The
1256
+ * def is consumed at PLANNING time; a later daemon restart resumes the run from its persisted
1257
+ * units, so the in-memory registration expiring with the process is fine.)
1258
+ */
1259
+ async _armPerRunWorkflow(def) {
1260
+ const core = this.core;
1261
+ const register = core['registerWorkflow'];
1262
+ if (typeof register !== 'function') {
1263
+ // Same doctrine as registerWorkflow(): no validator ⇒ refuse loudly. Silently launching
1264
+ // the BASE workflow instead would drop the delivery the caller explicitly asked for —
1265
+ // and, for crew#311, would drop the deliverable floor, launching a run that can once
1266
+ // again report done over an artifact that was never written.
1267
+ throw new Error('deliver: "pr" / requireDeliverables need a wicked-core build with the registerWorkflow ' +
1268
+ 'binding — the per-run workflow cannot be armed');
1269
+ }
1270
+ await register.call(this.core, JSON.stringify(def));
1271
+ }
971
1272
  /**
972
1273
  * Register a user-authored workflow: persist to the Rust workflow overlay dir
973
1274
  * (`~/.config/wicked-core/workflows/<id>.json` or `$WICKED_WORKFLOWS_DIR`),
@@ -1070,6 +1371,24 @@ export class CoreAdapter {
1070
1371
  if (typeof v !== 'number' || !Number.isInteger(v) || v < 1 || v > 10000)
1071
1372
  delete parsed.graphNodeLimit;
1072
1373
  }
1374
+ // workerStallMinutes (crew#287): positive minutes; a hand-edited zero/negative/NaN would
1375
+ // make the stall watchdog fire on every sweep, so drop it and fall back to the default.
1376
+ if ('workerStallMinutes' in parsed) {
1377
+ // Same bounds the PUT /settings route enforces (integer, 1..1440) — a hand-edited
1378
+ // settings.json must not enable values the API rejects (Copilot on #301).
1379
+ const v = parsed.workerStallMinutes;
1380
+ if (typeof v !== 'number' || !Number.isInteger(v) || v < 1 || v > 1440) {
1381
+ delete parsed.workerStallMinutes;
1382
+ }
1383
+ }
1384
+ // worker_config_root (seat sign-in): absolute path or "" (= engine default). A hand-edited
1385
+ // relative path is dropped rather than exported as WICKED_WORKER_HOME, where the engine
1386
+ // would resolve it against an arbitrary spawn cwd.
1387
+ if ('worker_config_root' in parsed) {
1388
+ const r = parsed.worker_config_root;
1389
+ if (typeof r !== 'string' || (r !== '' && !isAbsolute(r)))
1390
+ delete parsed.worker_config_root;
1391
+ }
1073
1392
  return { ...DEFAULT_SETTINGS, ...parsed };
1074
1393
  }
1075
1394
  catch {