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.
- package/dist/api/delivery-assurance.d.ts.map +1 -1
- package/dist/api/delivery-assurance.js +14 -3
- package/dist/api/delivery-assurance.js.map +1 -1
- package/dist/api/discovery-source.d.ts +4 -4
- package/dist/api/discovery-source.d.ts.map +1 -1
- package/dist/api/discovery-source.js +14 -8
- package/dist/api/discovery-source.js.map +1 -1
- package/dist/api/eval-sample.d.ts +12 -12
- package/dist/api/governance-steering.d.ts +2 -2
- package/dist/api/governance-steering.js +2 -2
- package/dist/api/governance-steering.js.map +1 -1
- package/dist/api/mcp.d.ts +4 -4
- package/dist/api/onboarding-capture.d.ts +1 -1
- package/dist/api/onboarding-capture.d.ts.map +1 -1
- package/dist/api/onboarding-capture.js +1 -1
- package/dist/api/onboarding-capture.js.map +1 -1
- package/dist/api/routes.d.ts +133 -131
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +37 -34
- package/dist/api/routes.js.map +1 -1
- package/dist/api/server.d.ts +6 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +36 -1
- package/dist/api/server.js.map +1 -1
- package/dist/api/testing.js +1 -1
- package/dist/api/testing.js.map +1 -1
- package/dist/campaigns/plan.d.ts +8 -1
- package/dist/campaigns/plan.d.ts.map +1 -1
- package/dist/campaigns/plan.js +15 -3
- package/dist/campaigns/plan.js.map +1 -1
- package/dist/campaigns/routes.d.ts +8 -8
- package/dist/core/adapter.d.ts +29 -89
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +86 -609
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/builtin-catalog.d.ts +24 -0
- package/dist/core/builtin-catalog.d.ts.map +1 -0
- package/dist/core/builtin-catalog.js +54 -0
- package/dist/core/builtin-catalog.js.map +1 -0
- package/dist/core/council-settings.d.ts +18 -0
- package/dist/core/council-settings.d.ts.map +1 -0
- package/dist/core/council-settings.js +37 -0
- package/dist/core/council-settings.js.map +1 -0
- package/dist/core/deliver-text.d.ts +1 -1
- package/dist/core/deliver-text.d.ts.map +1 -1
- package/dist/core/deliver-text.js +3 -2
- package/dist/core/deliver-text.js.map +1 -1
- package/dist/core/deliver.d.ts +0 -6
- package/dist/core/deliver.d.ts.map +1 -1
- package/dist/core/deliver.js +0 -6
- package/dist/core/deliver.js.map +1 -1
- package/dist/core/run-identity.d.ts +1 -1
- package/dist/core/run-identity.d.ts.map +1 -1
- package/dist/core/run-identity.js +7 -3
- package/dist/core/run-identity.js.map +1 -1
- package/dist/editors/csp.js +2 -2
- package/dist/editors/csp.js.map +1 -1
- package/dist/editors/pack-install.d.ts +35 -14
- package/dist/editors/pack-install.d.ts.map +1 -1
- package/dist/editors/pack-install.js +171 -24
- package/dist/editors/pack-install.js.map +1 -1
- package/dist/editors/registry.d.ts +151 -13
- package/dist/editors/registry.d.ts.map +1 -1
- package/dist/editors/registry.js +257 -20
- package/dist/editors/registry.js.map +1 -1
- package/dist/editors/routes.d.ts +10 -5
- package/dist/editors/routes.d.ts.map +1 -1
- package/dist/editors/routes.js +30 -20
- package/dist/editors/routes.js.map +1 -1
- package/dist/learn/routes.d.ts +42 -0
- package/dist/learn/routes.d.ts.map +1 -0
- package/dist/learn/routes.js +87 -0
- package/dist/learn/routes.js.map +1 -0
- package/dist/learn/schedule-store.d.ts +41 -0
- package/dist/learn/schedule-store.d.ts.map +1 -0
- package/dist/learn/schedule-store.js +137 -0
- package/dist/learn/schedule-store.js.map +1 -0
- package/dist/learn/scheduler.d.ts +71 -0
- package/dist/learn/scheduler.d.ts.map +1 -0
- package/dist/learn/scheduler.js +222 -0
- package/dist/learn/scheduler.js.map +1 -0
- package/dist/learn/topics.d.ts +14 -0
- package/dist/learn/topics.d.ts.map +1 -0
- package/dist/learn/topics.js +95 -0
- package/dist/learn/topics.js.map +1 -0
- package/dist/projects/graph.d.ts +14 -0
- package/dist/projects/graph.d.ts.map +1 -1
- package/dist/projects/graph.js +41 -2
- package/dist/projects/graph.js.map +1 -1
- package/dist/projects/routes.d.ts +2 -2
- package/dist/projects/state-home-registry.d.ts.map +1 -1
- package/dist/projects/state-home-registry.js +2 -0
- package/dist/projects/state-home-registry.js.map +1 -1
- package/dist/qe/acceptance.d.ts +5 -0
- package/dist/qe/acceptance.d.ts.map +1 -1
- package/dist/qe/acceptance.js +4 -2
- package/dist/qe/acceptance.js.map +1 -1
- package/dist/qe/author-workflow.d.ts +6 -83
- package/dist/qe/author-workflow.d.ts.map +1 -1
- package/dist/qe/author-workflow.js +6 -300
- package/dist/qe/author-workflow.js.map +1 -1
- package/dist/qe/ledger.d.ts +20 -0
- package/dist/qe/ledger.d.ts.map +1 -1
- package/dist/qe/ledger.js +28 -2
- package/dist/qe/ledger.js.map +1 -1
- package/dist/skills/core-closure.d.ts +3 -5
- package/dist/skills/core-closure.d.ts.map +1 -1
- package/dist/skills/core-closure.js +3 -5
- package/dist/skills/core-closure.js.map +1 -1
- package/dist/standing-orders/routes.d.ts +4 -4
- package/dist/standing-orders/store.d.ts +2 -2
- package/dist/studio/assets/index-C0ZMi_8d.js +583 -0
- package/dist/studio/assets/index-DYLo25MW.css +32 -0
- package/dist/studio/editors/wicked-page/editor.json +1 -1
- package/dist/studio/editors/wicked-page/index.html +1 -1
- package/dist/studio/index.html +2 -2
- package/dist/studio/testid-inventory.json +566 -36
- package/endpoint-manifest.json +59 -2
- package/package.json +4 -4
- package/dist/studio/assets/index-BGfFmWw4.css +0 -32
- package/dist/studio/assets/index-BMRUMU4X.js +0 -567
package/dist/core/adapter.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
|
436
|
-
//
|
|
437
|
-
//
|
|
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 (
|
|
1843
|
-
*
|
|
1844
|
-
*
|
|
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
|
|
1871
|
-
*
|
|
1872
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
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
|
|
2819
|
-
//
|
|
2820
|
-
//
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
/**
|
|
2825
|
-
*
|
|
2826
|
-
*
|
|
2827
|
-
*
|
|
2828
|
-
*
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
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
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
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
|
-
|
|
2870
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2885
|
-
//
|
|
2886
|
-
//
|
|
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
|
|
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(
|
|
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
|
-
|
|
2906
|
-
const def = this.
|
|
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
|
|
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
|
|
2968
|
-
* (
|
|
2969
|
-
*
|
|
2970
|
-
*
|
|
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
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
//
|
|
2982
|
-
const
|
|
2983
|
-
delete
|
|
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
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
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)))
|