wicked-crew 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/elicitation-cache.d.ts +93 -0
- package/dist/api/elicitation-cache.d.ts.map +1 -0
- package/dist/api/elicitation-cache.js +128 -0
- package/dist/api/elicitation-cache.js.map +1 -0
- package/dist/api/evidence.d.ts +37 -38
- package/dist/api/evidence.d.ts.map +1 -1
- package/dist/api/evidence.js +40 -36
- package/dist/api/evidence.js.map +1 -1
- package/dist/api/gate-cache.d.ts +29 -13
- package/dist/api/gate-cache.d.ts.map +1 -1
- package/dist/api/gate-cache.js +82 -36
- package/dist/api/gate-cache.js.map +1 -1
- package/dist/api/requirements.d.ts +66 -0
- package/dist/api/requirements.d.ts.map +1 -0
- package/dist/api/requirements.js +328 -0
- package/dist/api/requirements.js.map +1 -0
- package/dist/api/routes.d.ts +2 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +494 -34
- package/dist/api/routes.js.map +1 -1
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +13 -6
- package/dist/api/server.js.map +1 -1
- package/dist/core/adapter.d.ts +94 -6
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +386 -86
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/exec.d.ts +37 -0
- package/dist/core/exec.d.ts.map +1 -0
- package/dist/core/exec.js +97 -0
- package/dist/core/exec.js.map +1 -0
- package/dist/core/repoPaths.d.ts +37 -0
- package/dist/core/repoPaths.d.ts.map +1 -0
- package/dist/core/repoPaths.js +48 -0
- package/dist/core/repoPaths.js.map +1 -0
- package/dist/core/types.d.ts +81 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/studio/assets/index-Ci_R6ARr.js +423 -0
- package/dist/studio/assets/index-Dio_c1Q3.css +32 -0
- package/dist/studio/index.html +2 -2
- package/package.json +14 -13
- package/dist/studio/assets/index-BCcaLGxk.js +0 -420
- package/dist/studio/assets/index-vsyAOtPq.css +0 -32
package/dist/core/adapter.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
-
import { execFile } from 'node:child_process';
|
|
3
2
|
import { mkdir, access, readFile, writeFile, chmod, rm } from 'node:fs/promises';
|
|
3
|
+
import { existsSync, readFileSync, renameSync } from 'node:fs';
|
|
4
4
|
import { join, dirname, resolve, isAbsolute, relative, sep } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
|
-
import { promisify } from 'node:util';
|
|
8
7
|
import { randomUUID } from 'node:crypto';
|
|
9
8
|
import { DEFAULT_SETTINGS } from './types.js';
|
|
10
|
-
|
|
9
|
+
import { execCapped } from './exec.js';
|
|
11
10
|
/** Resolved path under the user's home directory. */
|
|
12
11
|
function wickedDir(...parts) {
|
|
13
12
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? '/tmp';
|
|
@@ -61,10 +60,46 @@ const require = createRequire(import.meta.url);
|
|
|
61
60
|
const { Core } = require('wicked-core-ts');
|
|
62
61
|
// ── Built-in workflow definitions (crew#44) ──────────────────────────────────
|
|
63
62
|
// Static mirrors of wicked-core workflow defs: feature, bug, migration, survey-repo,
|
|
64
|
-
//
|
|
63
|
+
// domain-graph-slice, memories, collab, onboarding, chat, and domain-extraction.
|
|
65
64
|
// Swap for `this.core.listWorkflowsJson()` / `this.core.getWorkflowJson(id)` once
|
|
66
65
|
// the wicked-core-ts NAPI methods land.
|
|
67
|
-
|
|
66
|
+
/**
|
|
67
|
+
* The ids wicked-core seeds itself, in `WorkflowRegistry::with_defaults()`.
|
|
68
|
+
*
|
|
69
|
+
* `launchRun`'s generic drop-in overlay write SKIPS every id in this set (`onboarding` is written
|
|
70
|
+
* by the onboarding path instead — see the end of this comment, it is the one deliberate exception).
|
|
71
|
+
* A file in that dir shadows the compiled built-in
|
|
72
|
+
* *wholesale* — `register` overwrites by id and `load_dir` runs after `with_defaults` — so writing
|
|
73
|
+
* this hand-transcribed mirror over the real def silently replaces it with a copy missing whatever
|
|
74
|
+
* the def has grown since the mirror was transcribed. That is not hypothetical: the mirror predated
|
|
75
|
+
* the evidence floors, so the write took `validator_pin` back off `feature.adversarial-review`,
|
|
76
|
+
* `bug.verify` and `migration.verify` — the entire content of core's gate-floor change, undone by a
|
|
77
|
+
* file write, with no error and a workflow still reporting the right id and phases (FINDING-049).
|
|
78
|
+
*
|
|
79
|
+
* The write exists for the ids core does NOT seed (chat, survey-repo, domain-graph-slice,
|
|
80
|
+
* memories, domain-extraction): for those the overlay is the only reason they resolve at all, so
|
|
81
|
+
* it stays.
|
|
82
|
+
*
|
|
83
|
+
* The exception: `onboarding` is core-seeded AND still written, by the onboarding path rather than
|
|
84
|
+
* by the generic one. Deliberate — that def's executor cmds are baked with runtime `--db` paths, so
|
|
85
|
+
* it shadows core's copy with a real customization rather than a stale transcription. It is the one
|
|
86
|
+
* shadow that earns its keep, and the reason this set gates the generic write specifically.
|
|
87
|
+
*/
|
|
88
|
+
const CORE_SEEDED_WORKFLOWS = new Set(['feature', 'bug', 'migration', 'onboarding', 'collab']);
|
|
89
|
+
/**
|
|
90
|
+
* The content-address of core's built-in evidence floor (`builtin_floors::EVIDENCE_FLOOR_PIN`),
|
|
91
|
+
* carried on the Evaluator phase of feature/bug/migration.
|
|
92
|
+
*
|
|
93
|
+
* Duplicating a hash is a real cost, paid because the alternative is worse. What `listWorkflows()`
|
|
94
|
+
* serves IS what `GET /api/v1/workflows` and the work-mode selector show, and a `null` here reads
|
|
95
|
+
* as "this phase is ungated" — the opposite of the truth for the three phases core gates. Reporting
|
|
96
|
+
* a gate that exists is the honest failure direction; the drift guard in
|
|
97
|
+
* `tests/armed-workflow-served.test.ts` fails loudly on a developer machine the moment core's value
|
|
98
|
+
* moves. This is display only: as of FINDING-049 these defs are never written to core's overlay dir
|
|
99
|
+
* (see CORE_SEEDED_WORKFLOWS), so a stale value here cannot reach the engine.
|
|
100
|
+
*/
|
|
101
|
+
const EVIDENCE_FLOOR_PIN = '2fcde907d57f3ee2';
|
|
102
|
+
export const BUILTIN_WORKFLOWS = [
|
|
68
103
|
{
|
|
69
104
|
id: 'chat',
|
|
70
105
|
is_system: true,
|
|
@@ -76,9 +111,19 @@ const BUILTIN_WORKFLOWS = [
|
|
|
76
111
|
id: 'onboarding',
|
|
77
112
|
is_system: true,
|
|
78
113
|
phases: [
|
|
79
|
-
{ id: 'index', executor: { type: 'tool', cmd: ['wicked-estate', 'index'] }, 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 },
|
|
80
|
-
{ id: 'annotate', executor: { type: 'tool', cmd: ['wicked-estate', 'clusters', '--annotate'] }, kind: 'recon', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['index'], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
|
|
81
|
-
|
|
114
|
+
{ id: 'index', executor: { type: 'tool', cmd: ['wicked-estate', 'index', '{repo_root}', '--db', '{code_graph_db}'] }, 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 },
|
|
115
|
+
{ id: 'annotate', executor: { type: 'tool', cmd: ['wicked-estate', 'clusters', '--annotate', '--db', '{code_graph_db}'] }, kind: 'recon', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['index'], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
|
|
116
|
+
// index → annotate, and NOT a third `domain` phase running `wicked-core domain-graph`. That
|
|
117
|
+
// phase could never pass: domain-graph fails closed below 1.0 front-half coverage, and nothing
|
|
118
|
+
// in this workflow annotates a single symbol, so coverage was 0.0 on every repo — every
|
|
119
|
+
// registration ended sessionFailed after the two phases that matter had both succeeded
|
|
120
|
+
// (FINDING-068). domain-graph belongs to `domain-extraction`, downstream of the agentic
|
|
121
|
+
// extract+coverage phases that produce its precondition. Mirrors core's `onboarding_def()`.
|
|
122
|
+
//
|
|
123
|
+
// The `{repo_root}` / `{code_graph_db}` placeholders are core's, substituted per run from the
|
|
124
|
+
// launch's `repoRef` (wicked-core#179). This package used to bake absolute paths in here and
|
|
125
|
+
// write the result to one shared overlay file per launch — which concurrent registrations
|
|
126
|
+
// raced, indexing one repo's tree under another repo's name (FINDING-075, #196).
|
|
82
127
|
],
|
|
83
128
|
},
|
|
84
129
|
{
|
|
@@ -87,7 +132,7 @@ const BUILTIN_WORKFLOWS = [
|
|
|
87
132
|
{ 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 },
|
|
88
133
|
{ 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 },
|
|
89
134
|
{ 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 },
|
|
90
|
-
{ 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:
|
|
135
|
+
{ 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 },
|
|
91
136
|
{ 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 },
|
|
92
137
|
{ 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 },
|
|
93
138
|
],
|
|
@@ -98,7 +143,7 @@ const BUILTIN_WORKFLOWS = [
|
|
|
98
143
|
{ 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 },
|
|
99
144
|
{ 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 },
|
|
100
145
|
{ id: 'fix', kind: 'build', 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: null },
|
|
101
|
-
{ 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:
|
|
146
|
+
{ 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 },
|
|
102
147
|
],
|
|
103
148
|
},
|
|
104
149
|
{
|
|
@@ -107,18 +152,10 @@ const BUILTIN_WORKFLOWS = [
|
|
|
107
152
|
{ 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 },
|
|
108
153
|
{ 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: null },
|
|
109
154
|
{ 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 },
|
|
110
|
-
{ 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:
|
|
155
|
+
{ 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 },
|
|
111
156
|
{ 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 },
|
|
112
157
|
],
|
|
113
158
|
},
|
|
114
|
-
{
|
|
115
|
-
id: 'repo-graph',
|
|
116
|
-
is_system: true,
|
|
117
|
-
phases: [
|
|
118
|
-
{ id: 'index', executor: { type: 'tool', cmd: ['wicked-estate', 'index'] }, 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 },
|
|
119
|
-
{ id: 'annotate', executor: { type: 'tool', cmd: ['wicked-estate', 'clusters', '--annotate'] }, kind: 'recon', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['index'], role: 'neutral', skill_ref: null, allowed_skills: [], validator_pin: null },
|
|
120
|
-
],
|
|
121
|
-
},
|
|
122
159
|
{
|
|
123
160
|
id: 'survey-repo',
|
|
124
161
|
is_system: true,
|
|
@@ -155,7 +192,123 @@ const BUILTIN_WORKFLOWS = [
|
|
|
155
192
|
{ id: 'verdict', kind: 'review', gate_type: 'value', gate: { human_confirm: { unconditional: false } }, executes_code: false, verified_evidence: false, required_deliverables: [], depends_on: ['revise'], role: 'evaluator', skill_ref: null, allowed_skills: [], validator_pin: null },
|
|
156
193
|
],
|
|
157
194
|
},
|
|
195
|
+
// The one workflow that ARMS the dual-validator gate: `coverage` carries an approved
|
|
196
|
+
// `validator_pin`, so layer 1 is live here and inert in every entry above. Transcribed
|
|
197
|
+
// field-for-field from the source of truth, `wicked-core/workflows/domain-extraction.json`
|
|
198
|
+
// (core ships it as a *drop-in*, not a seeded built-in, and exposes no dump command — hence a
|
|
199
|
+
// hand-transcribed mirror, like every other entry in this array).
|
|
200
|
+
//
|
|
201
|
+
// The pin is a content hash over the validator's criterion + script + approved flag. Core
|
|
202
|
+
// re-derives it in `domain_extraction.rs` and fails its own test if it drifts; if that test ever
|
|
203
|
+
// forces core's constant to change, THIS literal must change with it or crew will write an
|
|
204
|
+
// overlay that fails closed at plan time.
|
|
205
|
+
//
|
|
206
|
+
// Running it needs a one-time, idempotent `wicked-core seed-domain-validators` to vault + approve
|
|
207
|
+
// that validator. That step is deliberately manual — approval is an audited act a human/council
|
|
208
|
+
// owns, not something a daemon does unattended — and until it is run, a launch fails CLOSED at
|
|
209
|
+
// plan time rather than running the phase ungated. Not `is_system`: this is an operator-selectable
|
|
210
|
+
// work mode, unlike the dedicated-entry-point workflows above.
|
|
211
|
+
{
|
|
212
|
+
id: 'domain-extraction',
|
|
213
|
+
phases: [
|
|
214
|
+
{ id: 'survey', kind: 'recon', gate_type: null, gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: ['legacy-graph.digest.txt'], depends_on: [], role: 'neutral', skill_ref: 'wicked-garden-domain', allowed_skills: [], validator_pin: null },
|
|
215
|
+
{ id: 'analyze', kind: 'recon', gate_type: null, gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: ['analysis-report.json'], depends_on: ['survey'], role: 'neutral', skill_ref: 'wicked-garden-domain', allowed_skills: [], validator_pin: null },
|
|
216
|
+
{ id: 'extract', kind: 'recon', gate_type: 'value', gate: 'auto', executes_code: false, verified_evidence: false, required_deliverables: ['annotations.jsonl'], depends_on: ['analyze'], role: 'creator', skill_ref: 'wicked-garden-domain-extractor', allowed_skills: [], validator_pin: null },
|
|
217
|
+
{ 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: 'adaf3e9b6d088f1a' },
|
|
218
|
+
{ id: 'domain-graph', kind: 'build', gate_type: 'strategy', gate: { human_confirm: { unconditional: false } }, executes_code: false, verified_evidence: false, required_deliverables: ['requirements_graph.json'], depends_on: ['coverage'], role: 'neutral', skill_ref: 'wicked-garden-domain-modeler', allowed_skills: [], validator_pin: null },
|
|
219
|
+
],
|
|
220
|
+
},
|
|
158
221
|
];
|
|
222
|
+
/**
|
|
223
|
+
* Chat is not available in this deployment at all — a capability gap, never a bad request.
|
|
224
|
+
*
|
|
225
|
+
* It arrives two ways and both mean the same thing to an operator: the addon predates the binding
|
|
226
|
+
* (no method to call), or the engine was spawned without the ACP runner and says so when called.
|
|
227
|
+
* Only the first is knowable before the call, which is why this is a thrown type rather than a
|
|
228
|
+
* capability flag.
|
|
229
|
+
*
|
|
230
|
+
* Typed rather than left to the caller to sniff out of the message text: the route used to regex
|
|
231
|
+
* the message for one of the two phrasings, so the other fell through to `400` and told an operator
|
|
232
|
+
* to fix a request that was already correct.
|
|
233
|
+
*/
|
|
234
|
+
export class ChatUnsupportedError extends Error {
|
|
235
|
+
constructor(message) {
|
|
236
|
+
super(message);
|
|
237
|
+
this.name = 'ChatUnsupportedError';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* `resolveElicitation` is not available in this deployment — the NAPI binding has not
|
|
242
|
+
* landed in the installed `wicked-core-ts` yet (DES-002 §4 P-1 stub).
|
|
243
|
+
*
|
|
244
|
+
* Routes map this to HTTP 501 so an operator knows to upgrade rather than to fix a
|
|
245
|
+
* call that was already correct.
|
|
246
|
+
*/
|
|
247
|
+
export class ElicitationUnsupportedError extends Error {
|
|
248
|
+
constructor(message) {
|
|
249
|
+
super(message);
|
|
250
|
+
this.name = 'ElicitationUnsupportedError';
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** The engine's own way of reporting a build that cannot do chat, raised at call time. */
|
|
254
|
+
const ENGINE_CHAT_UNSUPPORTED = /chat unsupported/i;
|
|
255
|
+
/**
|
|
256
|
+
* Quarantine a pre-#197 `onboarding.json` left in the overlay dir.
|
|
257
|
+
*
|
|
258
|
+
* This package used to write that file on every launch, baked with ONE repo's absolute paths. It no
|
|
259
|
+
* longer does — core declares `{repo_root}` / `{code_graph_db}` and binds them per run
|
|
260
|
+
* (wicked-core#179). But the overlay dir is PERSISTENT STATE, and the engine's `load_dir` registers
|
|
261
|
+
* whatever it finds there, replacing a compiled def by id, wholesale.
|
|
262
|
+
*
|
|
263
|
+
* So an upgraded deployment keeps running the last file the old code wrote. Not intermittently:
|
|
264
|
+
* EVERY onboarding run indexes whichever repo happened to be registered last before the upgrade.
|
|
265
|
+
* Observed exactly that on this host after #197 merged — three fresh registrations in three
|
|
266
|
+
* different orgs all indexed `agentic-products/eliza`, the last repo seeded before the fix.
|
|
267
|
+
*
|
|
268
|
+
* Renamed rather than deleted. The file is almost certainly machine-written, but the overlay dir is
|
|
269
|
+
* an operator-facing extension point and silently destroying something out of it is not this
|
|
270
|
+
* process's call. The rename is enough to stop the shadow, and leaves the evidence in place.
|
|
271
|
+
*/
|
|
272
|
+
function quarantineStaleOnboardingOverlay() {
|
|
273
|
+
const stale = join(workflowOverlayDir(), 'onboarding.json');
|
|
274
|
+
if (!existsSync(stale))
|
|
275
|
+
return; // the ordinary case on a clean install
|
|
276
|
+
// Only a PRE-#197 artifact, never an operator's override. `registerWorkflow()` writes user
|
|
277
|
+
// definitions into this same directory, and parking one on every boot would delete a deliberate
|
|
278
|
+
// customization each time it was re-registered.
|
|
279
|
+
//
|
|
280
|
+
// The signature is specific: old crew baked one repo's ABSOLUTE paths into the tool commands. A
|
|
281
|
+
// def carrying `{repo_root}` / `{code_graph_db}`, or agent phases, or relative commands, is not
|
|
282
|
+
// what this is looking for and is left alone. An operator who hand-writes absolute paths into a
|
|
283
|
+
// shared def has written the same bug, and gets the same treatment for the same reason.
|
|
284
|
+
let bakedPaths;
|
|
285
|
+
try {
|
|
286
|
+
const def = JSON.parse(readFileSync(stale, 'utf8'));
|
|
287
|
+
bakedPaths = (def.phases ?? [])
|
|
288
|
+
.flatMap((p) => (p.executor?.type === 'tool' ? (p.executor.cmd ?? []) : []))
|
|
289
|
+
.filter((arg) => arg.startsWith('/'));
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// Unparseable: not ours to judge. The engine reports its own load failure.
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (bakedPaths.length === 0)
|
|
296
|
+
return;
|
|
297
|
+
const parked = `${stale}.superseded-by-crew197`;
|
|
298
|
+
try {
|
|
299
|
+
renameSync(stale, parked);
|
|
300
|
+
console.warn(`[onboarding] removed a stale overlay that would have hijacked every onboarding run: ` +
|
|
301
|
+
`${stale} → ${parked}. It baked ${bakedPaths[0]} into a def shared by every repo, which is ` +
|
|
302
|
+
`what a pre-#197 crew wrote; the engine resolves it in preference to the built-in ` +
|
|
303
|
+
`(FINDING-075).`);
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
// Loud, and non-fatal: the daemon still starts, but every onboarding on this host is wrong
|
|
307
|
+
// until the file goes, so the operator has to be told rather than left to discover it.
|
|
308
|
+
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, ` +
|
|
309
|
+
`whatever repo the run names (FINDING-075).`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
159
312
|
/**
|
|
160
313
|
* The single isolation boundary over wicked-core-ts. It holds the ONE `Core`
|
|
161
314
|
* handle, makes the ONE `subscribe()` call for the whole process, parses each
|
|
@@ -181,6 +334,9 @@ export class CoreAdapter {
|
|
|
181
334
|
// it publishes `task.dispatched` / consumes `task.completed` around WHATEVER step runner the engine
|
|
182
335
|
// wires (the production wrapped-CLI runner OR the deterministic stub), so a stub engine can arm it
|
|
183
336
|
// for a fast, offline, deterministic proof of the event path.
|
|
337
|
+
// BEFORE the Core spawns: the actor reads the overlay dir at startup, so a stale
|
|
338
|
+
// `onboarding.json` has to be out of the way by then or it shadows the built-in def.
|
|
339
|
+
quarantineStaleOnboardingOverlay();
|
|
184
340
|
const armExec = opts.engineExec === true;
|
|
185
341
|
if (armExec) {
|
|
186
342
|
if (!opts.busDbPath || opts.busDbPath.length === 0) {
|
|
@@ -254,11 +410,17 @@ export class CoreAdapter {
|
|
|
254
410
|
opts.repoRef = input.repoRef;
|
|
255
411
|
if (input.workflow !== undefined) {
|
|
256
412
|
opts.workflow = input.workflow;
|
|
257
|
-
// Ensure
|
|
413
|
+
// Ensure DROP-IN workflow definitions are present in the Rust overlay dir on first use.
|
|
258
414
|
// Uses a dedicated helper (not registerWorkflow) to avoid adding built-ins to userWorkflows,
|
|
259
415
|
// which would duplicate them in listWorkflows(). The write is skipped after the first call
|
|
260
416
|
// per process lifetime.
|
|
261
|
-
|
|
417
|
+
//
|
|
418
|
+
// Ids core seeds itself are excluded: writing them shadows the real def with this stale
|
|
419
|
+
// mirror — see CORE_SEEDED_WORKFLOWS. Core resolves those from its own registry, so there is
|
|
420
|
+
// nothing to write and never was.
|
|
421
|
+
const builtinDef = CORE_SEEDED_WORKFLOWS.has(input.workflow)
|
|
422
|
+
? undefined
|
|
423
|
+
: BUILTIN_WORKFLOWS.find((w) => w.id === input.workflow);
|
|
262
424
|
if (builtinDef && !this._builtinOverlayWritten.has(input.workflow)) {
|
|
263
425
|
// Mark before await so concurrent launchRun() calls for the same builtin
|
|
264
426
|
// don't both pass the has() check and race to write the same file.
|
|
@@ -287,6 +449,23 @@ export class CoreAdapter {
|
|
|
287
449
|
}
|
|
288
450
|
return this.core.injectWorkerMessage(runId, message, target);
|
|
289
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* A run's recorded event history, oldest first — or `null` when this wicked-core build has no
|
|
454
|
+
* event-log read binding.
|
|
455
|
+
*
|
|
456
|
+
* `null` rather than `[]` on purpose. An empty history is a real, ordinary answer (a run that
|
|
457
|
+
* emitted nothing, or one predating the log), and collapsing "nothing happened" into "I cannot
|
|
458
|
+
* tell you what happened" is how a missing capability gets reported to an operator as an absent
|
|
459
|
+
* gate — the FINDING-050 shape, distinct causes wearing one message. Callers branch on it.
|
|
460
|
+
*/
|
|
461
|
+
async runEvents(runId) {
|
|
462
|
+
if (typeof this.core.runEvents !== 'function')
|
|
463
|
+
return null;
|
|
464
|
+
// `RecordedEvent`, not `CoreEvent`: the binding's contract is the `/ws` frame PLUS a capture-time
|
|
465
|
+
// `ts` and an ordering `seq`, and consumers (the evidence bundle) need both. Typing this as the
|
|
466
|
+
// bare frame made every caller widen or cast to get at fields the engine always sends.
|
|
467
|
+
return JSON.parse(await this.core.runEvents(runId));
|
|
468
|
+
}
|
|
290
469
|
/** Run ids on the store. */
|
|
291
470
|
async sessions() {
|
|
292
471
|
return JSON.parse(await this.core.sessions());
|
|
@@ -327,14 +506,84 @@ export class CoreAdapter {
|
|
|
327
506
|
async workOutput(unitId) {
|
|
328
507
|
return JSON.parse(await this.core.workOutput(unitId));
|
|
329
508
|
}
|
|
509
|
+
// ── Chat sessions (core#134 / crew#165) ────────────────────────────────────
|
|
510
|
+
async chatOpen(chatId, clis, cwd) {
|
|
511
|
+
const raw = await this.core.chatOpen(chatId, JSON.stringify(clis), cwd ?? null);
|
|
512
|
+
return JSON.parse(raw);
|
|
513
|
+
}
|
|
514
|
+
async chatSend(chatId, text, targets, cwd) {
|
|
515
|
+
const raw = await this.core.chatSend(chatId, text, targets === undefined ? null : JSON.stringify(targets), cwd ?? null);
|
|
516
|
+
return JSON.parse(raw);
|
|
517
|
+
}
|
|
518
|
+
async chatSeats(chatId) {
|
|
519
|
+
return JSON.parse(await this.core.chatSeats(chatId));
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Every live chat, so an operator can find the ones nothing is going to close (FINDING-027).
|
|
523
|
+
*
|
|
524
|
+
* Chat sessions are a warm pool that deliberately outlives the page, and the only client that
|
|
525
|
+
* knew a chat's id is the tab that minted it. Without this an orphaned seat is unreclaimable
|
|
526
|
+
* short of restarting the daemon — the leak is real but invisible, which is the worse half.
|
|
527
|
+
*
|
|
528
|
+
* `idleSecs` is `number | null`, not `number`. The Rust side uses `u64::MAX` for "no activity
|
|
529
|
+
* timestamp"; as an f64 that arrives as 18446744073709552000, which no caller can test for by
|
|
530
|
+
* equality and every caller can accidentally do arithmetic on. The binding maps it to `null`.
|
|
531
|
+
*/
|
|
532
|
+
async chatList() {
|
|
533
|
+
const list = this.core.chatList;
|
|
534
|
+
if (typeof list !== 'function') {
|
|
535
|
+
throw new ChatUnsupportedError('Listing chats is not yet supported by this wicked-core build');
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
return JSON.parse(await list.call(this.core));
|
|
539
|
+
}
|
|
540
|
+
catch (err) {
|
|
541
|
+
// A build without the ACP runner has the binding and refuses at call time, so the presence
|
|
542
|
+
// check above cannot catch it. Classified here rather than at the route because this file is
|
|
543
|
+
// the only one that touches the addon (DES-STUDIO-001 §5.2) — matching engine wording anywhere
|
|
544
|
+
// else would spread that coupling.
|
|
545
|
+
const text = err instanceof Error ? err.message : String(err);
|
|
546
|
+
if (ENGINE_CHAT_UNSUPPORTED.test(text))
|
|
547
|
+
throw new ChatUnsupportedError(text);
|
|
548
|
+
throw err;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async chatClose(chatId) {
|
|
552
|
+
await this.core.chatClose(chatId);
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Forward the operator's elicitation response to the actor (DES-002 §4 P-1).
|
|
556
|
+
*
|
|
557
|
+
* NAPI flat signature: `resolve_elicitation(run_id, elicitation_id, action, response)`.
|
|
558
|
+
* `response` is `null` for `decline` and `cancel` actions; a non-empty string for `accept`.
|
|
559
|
+
*
|
|
560
|
+
* Throws `ElicitationUnsupportedError` until the NAPI binding is present in the installed
|
|
561
|
+
* `wicked-core-ts`. Routes map that to HTTP 501.
|
|
562
|
+
*/
|
|
563
|
+
async resolveElicitation(_runId, _elicitationId, _action, _response) {
|
|
564
|
+
// Consume stub params to satisfy @typescript-eslint/no-unused-vars; the
|
|
565
|
+
// parameter names are part of the public interface and must not be dropped.
|
|
566
|
+
void _runId;
|
|
567
|
+
void _elicitationId;
|
|
568
|
+
void _action;
|
|
569
|
+
void _response;
|
|
570
|
+
// The NAPI binding (`this.core.resolveElicitation`) will land with the actor-side
|
|
571
|
+
// work in a follow-on. Until then, every call throws so the route surfaces 501 and
|
|
572
|
+
// an operator knows to upgrade rather than to keep retrying.
|
|
573
|
+
throw new ElicitationUnsupportedError('resolveElicitation is not yet bound in this wicked-core build; upgrade wicked-core-ts to enable it');
|
|
574
|
+
}
|
|
330
575
|
/** repo id → onboarding run id (in-memory; graph persists on disk across restarts). */
|
|
331
576
|
repoOnboardRunIds = new Map();
|
|
332
|
-
/**
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
|
|
577
|
+
/**
|
|
578
|
+
* repo id → the in-flight launch, so a concurrent caller joins it instead of starting a second.
|
|
579
|
+
*
|
|
580
|
+
* A `Set` of ids was not enough. The id was added here but the run id was only recorded in
|
|
581
|
+
* `repoOnboardRunIds` AFTER the launch resolved, so a second caller arriving mid-flight saw
|
|
582
|
+
* "in flight" with no run id to return, fell through, and launched a DUPLICATE run against the
|
|
583
|
+
* same repo. Holding the promise makes the second caller await the first and receive its run id —
|
|
584
|
+
* the dedup the `Set` was named for.
|
|
585
|
+
*/
|
|
586
|
+
onboardingInFlight = new Map();
|
|
338
587
|
/** Register a local git repo → the persisted `RepoEntry`. */
|
|
339
588
|
async registerRepo(name, rootPath) {
|
|
340
589
|
return JSON.parse(await this.core.registerRepo(name, rootPath));
|
|
@@ -393,7 +642,7 @@ export class CoreAdapter {
|
|
|
393
642
|
catch { /* not yet cloned */ }
|
|
394
643
|
if (needsClone) {
|
|
395
644
|
try {
|
|
396
|
-
await
|
|
645
|
+
await execCapped('git', ['clone', '--', gitUrl, cloneDir], {
|
|
397
646
|
timeout: 5 * 60 * 1000,
|
|
398
647
|
});
|
|
399
648
|
}
|
|
@@ -421,63 +670,52 @@ export class CoreAdapter {
|
|
|
421
670
|
* Returns the run id so the UI can navigate directly to it.
|
|
422
671
|
*/
|
|
423
672
|
async launchOnboardingRun(repoId, repoName) {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
this.onboardingInFlight.add(repoId);
|
|
673
|
+
// Join an in-flight launch for THIS repo rather than starting a second one. Concurrency across
|
|
674
|
+
// DIFFERENT repos is the point and is untouched; two launches for the SAME repo are a duplicate.
|
|
675
|
+
const inFlight = this.onboardingInFlight.get(repoId);
|
|
676
|
+
if (inFlight)
|
|
677
|
+
return inFlight;
|
|
430
678
|
const runId = randomUUID();
|
|
431
|
-
|
|
432
|
-
|
|
679
|
+
// Launches are NOT serialized. They used to be, through an `_onboardingChain` promise, because
|
|
680
|
+
// each rewrote the shared `onboarding` overlay before launching. That chain never worked: its
|
|
681
|
+
// own comment claimed "after launch the def is baked into the run's units", and the def is
|
|
682
|
+
// actually resolved at DISPATCH — after the launch call returns. So it serialized the writer and
|
|
683
|
+
// left the reader racing, which is how three concurrent registrations indexed one repo under
|
|
684
|
+
// three names (FINDING-075, #196).
|
|
685
|
+
//
|
|
686
|
+
// Nothing is shared now: core binds each run's repo into its own units from `repoRef`
|
|
687
|
+
// (wicked-core#179). Concurrent registration is the point — it is a requirement of the corpus
|
|
688
|
+
// this platform is tested against, not an optimisation.
|
|
689
|
+
const launch = this._doOnboardingLaunch(repoId, repoName, runId).then(() => runId);
|
|
690
|
+
this.onboardingInFlight.set(repoId, launch);
|
|
433
691
|
try {
|
|
434
|
-
await
|
|
435
|
-
return runId;
|
|
692
|
+
return await launch;
|
|
436
693
|
}
|
|
437
694
|
finally {
|
|
438
695
|
this.onboardingInFlight.delete(repoId);
|
|
439
696
|
}
|
|
440
697
|
}
|
|
441
698
|
async _doOnboardingLaunch(repoId, repoName, runId) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
};
|
|
463
|
-
const def = {
|
|
464
|
-
...base,
|
|
465
|
-
phases: base.phases.map((ph) => CMDS[ph.id] ? { ...ph, executor: { type: 'tool', cmd: CMDS[ph.id] } } : ph),
|
|
466
|
-
};
|
|
467
|
-
// _writeBuiltinOverlay persists the overlay AND hot-registers it in the actor.
|
|
468
|
-
await this._writeBuiltinOverlay(def);
|
|
469
|
-
// Mark the builtin as written BEFORE launchRun: its generic once-guard would otherwise
|
|
470
|
-
// see 'onboarding' as unwritten and clobber the baked def with the static mirror in the
|
|
471
|
-
// window between this write and the engine resolving the workflow (observed live).
|
|
472
|
-
this._builtinOverlayWritten.add('onboarding');
|
|
473
|
-
await this.launchRun({
|
|
474
|
-
problem: `Onboard repository: ${repoName}`,
|
|
475
|
-
sessionId: runId,
|
|
476
|
-
clisJson: JSON.stringify(CoreAdapter.roster()),
|
|
477
|
-
workflow: 'onboarding',
|
|
478
|
-
repoRef: repoId,
|
|
479
|
-
});
|
|
480
|
-
}
|
|
699
|
+
// No overlay write. This used to rewrite core's `onboarding` def with THIS repo's absolute paths
|
|
700
|
+
// and persist it to one shared file (`~/.config/wicked-core/workflows/onboarding.json`), then
|
|
701
|
+
// hot-register it — the one place a core-seeded id was deliberately shadowed.
|
|
702
|
+
//
|
|
703
|
+
// That shadow was the defect. The engine resolves a workflow at DISPATCH time, after this call
|
|
704
|
+
// returns, so concurrent launches raced on the single file and the last writer won: two repos in
|
|
705
|
+
// two different orgs had a third org's tree indexed into a third org's database, each reported
|
|
706
|
+
// under its own name (FINDING-075, #196). Serializing the writes does not fix it — the chain
|
|
707
|
+
// serializes the producer and leaves the consumer racing.
|
|
708
|
+
//
|
|
709
|
+
// Core now declares `{repo_root}` / `{code_graph_db}` on the phases and binds them per run from
|
|
710
|
+
// `repoRef`, which this call already passes (wicked-core#179). Nothing is shared, so nothing can
|
|
711
|
+
// be raced, and onboarding launches may run concurrently.
|
|
712
|
+
await this.launchRun({
|
|
713
|
+
problem: `Onboard repository: ${repoName}`,
|
|
714
|
+
sessionId: runId,
|
|
715
|
+
clisJson: JSON.stringify(CoreAdapter.roster()),
|
|
716
|
+
workflow: 'onboarding',
|
|
717
|
+
repoRef: repoId,
|
|
718
|
+
});
|
|
481
719
|
this.repoOnboardRunIds.set(repoId, runId);
|
|
482
720
|
}
|
|
483
721
|
/** Return the onboarding run id for a repo (undefined if not launched this session). */
|
|
@@ -514,6 +752,28 @@ export class CoreAdapter {
|
|
|
514
752
|
async upsertConformanceRule(rule) {
|
|
515
753
|
await this.core.upsertConformanceRule(JSON.stringify(rule));
|
|
516
754
|
}
|
|
755
|
+
/**
|
|
756
|
+
* Withdraw a policy from enforcement. Resolves `true` if a policy with that id existed.
|
|
757
|
+
*
|
|
758
|
+
* Retire, not delete (FINDING-038): the node stays readable so a past decision citing this id is
|
|
759
|
+
* still explicable, but governance stops selecting it. The boolean is what lets the route answer
|
|
760
|
+
* 404 instead of reporting a success that removed nothing.
|
|
761
|
+
*/
|
|
762
|
+
async retirePolicy(id) {
|
|
763
|
+
const retire = this.core.retirePolicy;
|
|
764
|
+
if (typeof retire !== 'function') {
|
|
765
|
+
throw new Error('Retiring a policy is not yet supported by this wicked-core build');
|
|
766
|
+
}
|
|
767
|
+
return JSON.parse(await retire.call(this.core, id));
|
|
768
|
+
}
|
|
769
|
+
/** Withdraw a conformance rule from recall. Same contract as {@link retirePolicy}. */
|
|
770
|
+
async retireConformanceRule(id) {
|
|
771
|
+
const retire = this.core.retireConformanceRule;
|
|
772
|
+
if (typeof retire !== 'function') {
|
|
773
|
+
throw new Error('Retiring a conformance rule is not yet supported by this wicked-core build');
|
|
774
|
+
}
|
|
775
|
+
return JSON.parse(await retire.call(this.core, id));
|
|
776
|
+
}
|
|
517
777
|
/** Recall conformance rules matching a facet query (read-only, does not block actor). */
|
|
518
778
|
async recallRulesPreview(query) {
|
|
519
779
|
const cleanQuery = {};
|
|
@@ -561,11 +821,29 @@ export class CoreAdapter {
|
|
|
561
821
|
await mkdir(dir, { recursive: true });
|
|
562
822
|
const overlayDef = { ...def };
|
|
563
823
|
delete overlayDef.is_system;
|
|
564
|
-
|
|
824
|
+
const json = JSON.stringify(overlayDef);
|
|
565
825
|
const core = this.core;
|
|
566
|
-
|
|
567
|
-
|
|
826
|
+
const register = core['registerWorkflow'];
|
|
827
|
+
// Same validate-before-persist ordering as registerWorkflow (FINDING-002). This path had the
|
|
828
|
+
// identical defect — write first, validate last — which is the P3 shape this campaign keeps
|
|
829
|
+
// finding: N paths, one hardened. A mirror that drifted far enough for core to reject it would
|
|
830
|
+
// otherwise leave an unparseable *.json in the dispatch overlay dir, and core would skip it at
|
|
831
|
+
// the next load. Letting the rejection propagate instead fails the launch with core's own
|
|
832
|
+
// reason, which beats dispatching against a workflow core will silently drop.
|
|
833
|
+
if (typeof register === 'function') {
|
|
834
|
+
await register.call(this.core, json);
|
|
568
835
|
}
|
|
836
|
+
// Deliberately NOT the refusal registerWorkflow makes when the binding is absent, and the
|
|
837
|
+
// difference is the input, not the caller:
|
|
838
|
+
// - a user def is arbitrary runtime input no test has ever seen, so unvalidatable means
|
|
839
|
+
// unsafe to persist;
|
|
840
|
+
// - a built-in mirror is asserted field-for-field against wicked-core's own
|
|
841
|
+
// workflows/<id>.json by tests/builtin-overlay-shadow.test.ts, so its parseability is
|
|
842
|
+
// established at build time rather than needing a runtime check.
|
|
843
|
+
// Refusing here would also break DELIVERY: this write is the only way core resolves a drop-in
|
|
844
|
+
// id, so a refusal turns a silent ungating into a hard "unknown workflow" — exactly the
|
|
845
|
+
// regression FINDING-084's first attempted fix caused.
|
|
846
|
+
await writeFile(join(dir, `${def.id}.json`), JSON.stringify(overlayDef, null, 2), 'utf8');
|
|
569
847
|
}
|
|
570
848
|
/**
|
|
571
849
|
* Register a user-authored workflow: persist to the Rust workflow overlay dir
|
|
@@ -585,14 +863,36 @@ export class CoreAdapter {
|
|
|
585
863
|
// field and silently drops any workflow whose JSON it cannot fully deserialise.
|
|
586
864
|
const overlayDef = { ...def };
|
|
587
865
|
delete overlayDef.is_system;
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
//
|
|
591
|
-
//
|
|
866
|
+
const json = JSON.stringify(overlayDef);
|
|
867
|
+
// VALIDATE BEFORE PERSISTING (FINDING-002). This ordering is the whole fix.
|
|
868
|
+
//
|
|
869
|
+
// The write used to come first and `registerWorkflow` last, so core's parser — the only thing
|
|
870
|
+
// that actually knows the overlay schema — ran AFTER the state was already mutated. Observed
|
|
871
|
+
// end to end: POST /api/v1/workflows answered
|
|
872
|
+
// 400 invalid workflow JSON: unknown field `name`, expected `id` or `phases`
|
|
873
|
+
// and the file was on disk anyway, `name` included, and served from `userWorkflows` as though
|
|
874
|
+
// registered. On the next daemon start core could not deserialise its own overlay file:
|
|
875
|
+
// wicked-core: skipping workflow file .../probe-002-persist.json
|
|
876
|
+
// and the workflow VANISHED while its file remained. That is FINDING-002's root cause: not
|
|
877
|
+
// "registration is not durable" but "a rejected request persisted a def core cannot read".
|
|
878
|
+
//
|
|
879
|
+
// Core's parser is the authority, so it is what we ask. Enumerating the accepted fields in TS
|
|
880
|
+
// instead would be a second copy of core's schema — the exact drift this codebase keeps paying
|
|
881
|
+
// for, and `is_system` above is already one hand-maintained instance of it.
|
|
592
882
|
const core = this.core;
|
|
593
|
-
|
|
594
|
-
|
|
883
|
+
const register = core['registerWorkflow'];
|
|
884
|
+
if (typeof register !== 'function') {
|
|
885
|
+
// No validator, so no safe way to persist: an unvalidated def written here is a file core
|
|
886
|
+
// may silently skip at load. Refusing is the honest outcome — and it is loud, unlike the
|
|
887
|
+
// vanishing act it replaces. `registerWorkflow` has been declared (non-optional) in
|
|
888
|
+
// wicked-core-ts since 0.4.0, so this is a real floor, not a routine path.
|
|
889
|
+
throw new Error('this wicked-core build exposes no registerWorkflow binding, so a workflow cannot be ' +
|
|
890
|
+
'validated before it is written; refusing to persist an unvalidated definition');
|
|
595
891
|
}
|
|
892
|
+
// Throws on a def core rejects — before anything is written or registered.
|
|
893
|
+
await register.call(this.core, json);
|
|
894
|
+
await writeFile(path, JSON.stringify(overlayDef, null, 2), 'utf8');
|
|
895
|
+
this.userWorkflows.set(def.id, def);
|
|
596
896
|
return def.id;
|
|
597
897
|
}
|
|
598
898
|
/**
|