wicked-crew 0.7.7 → 0.7.9
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/audit.d.ts +9 -1
- package/dist/api/audit.d.ts.map +1 -1
- package/dist/api/audit.js +11 -3
- package/dist/api/audit.js.map +1 -1
- package/dist/api/delivery-index.d.ts +94 -7
- package/dist/api/delivery-index.d.ts.map +1 -1
- package/dist/api/delivery-index.js +182 -4
- package/dist/api/delivery-index.js.map +1 -1
- package/dist/api/elicitation-cache.d.ts +10 -0
- package/dist/api/elicitation-cache.d.ts.map +1 -1
- package/dist/api/elicitation-cache.js +18 -0
- package/dist/api/elicitation-cache.js.map +1 -1
- package/dist/api/governance-steering.d.ts +8 -0
- package/dist/api/governance-steering.d.ts.map +1 -1
- package/dist/api/governance-steering.js +36 -12
- package/dist/api/governance-steering.js.map +1 -1
- package/dist/api/post-hoc-deliver.d.ts +38 -0
- package/dist/api/post-hoc-deliver.d.ts.map +1 -0
- package/dist/api/post-hoc-deliver.js +57 -0
- package/dist/api/post-hoc-deliver.js.map +1 -0
- package/dist/api/routes.d.ts +34 -7
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +314 -35
- package/dist/api/routes.js.map +1 -1
- package/dist/api/seat-signin.d.ts +18 -2
- package/dist/api/seat-signin.d.ts.map +1 -1
- package/dist/api/seat-signin.js +21 -2
- package/dist/api/seat-signin.js.map +1 -1
- package/dist/api/server.d.ts +22 -7
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +131 -22
- package/dist/api/server.js.map +1 -1
- package/dist/api/stall-watchdog.d.ts +46 -21
- package/dist/api/stall-watchdog.d.ts.map +1 -1
- package/dist/api/stall-watchdog.js +193 -19
- package/dist/api/stall-watchdog.js.map +1 -1
- package/dist/api/steering-landing.d.ts +130 -0
- package/dist/api/steering-landing.d.ts.map +1 -0
- package/dist/api/steering-landing.js +367 -0
- package/dist/api/steering-landing.js.map +1 -0
- package/dist/api/testing.d.ts +3 -0
- package/dist/api/testing.d.ts.map +1 -1
- package/dist/api/testing.js +146 -18
- package/dist/api/testing.js.map +1 -1
- package/dist/api/unit-output.d.ts.map +1 -1
- package/dist/api/unit-output.js +7 -1
- package/dist/api/unit-output.js.map +1 -1
- package/dist/campaigns/plan.d.ts +46 -0
- package/dist/campaigns/plan.d.ts.map +1 -1
- package/dist/campaigns/plan.js +80 -0
- package/dist/campaigns/plan.js.map +1 -1
- package/dist/campaigns/routes.d.ts +2 -2
- package/dist/campaigns/worktrees.d.ts +39 -0
- package/dist/campaigns/worktrees.d.ts.map +1 -0
- package/dist/campaigns/worktrees.js +95 -0
- package/dist/campaigns/worktrees.js.map +1 -0
- package/dist/cli/index.js +46 -13
- package/dist/cli/index.js.map +1 -1
- package/dist/core/adapter.d.ts +65 -13
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +148 -31
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/bridge-reaper.d.ts +54 -9
- package/dist/core/bridge-reaper.d.ts.map +1 -1
- package/dist/core/bridge-reaper.js +116 -11
- package/dist/core/bridge-reaper.js.map +1 -1
- package/dist/core/types.d.ts +25 -12
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +14 -0
- package/dist/core/types.js.map +1 -1
- package/dist/interactive/chat-events.d.ts.map +1 -1
- package/dist/interactive/chat-events.js +5 -2
- package/dist/interactive/chat-events.js.map +1 -1
- package/dist/interactive/demo-events.d.ts.map +1 -1
- package/dist/interactive/demo-events.js +5 -2
- package/dist/interactive/demo-events.js.map +1 -1
- package/dist/interactive/doc-delete-routes.d.ts +69 -0
- package/dist/interactive/doc-delete-routes.d.ts.map +1 -0
- package/dist/interactive/doc-delete-routes.js +240 -0
- package/dist/interactive/doc-delete-routes.js.map +1 -0
- package/dist/interactive/doc-ledger-sweep.d.ts +53 -0
- package/dist/interactive/doc-ledger-sweep.d.ts.map +1 -0
- package/dist/interactive/doc-ledger-sweep.js +45 -0
- package/dist/interactive/doc-ledger-sweep.js.map +1 -0
- package/dist/interactive/draft-events.d.ts.map +1 -1
- package/dist/interactive/draft-events.js +5 -2
- package/dist/interactive/draft-events.js.map +1 -1
- package/dist/interactive/edit-events.d.ts.map +1 -1
- package/dist/interactive/edit-events.js +5 -2
- package/dist/interactive/edit-events.js.map +1 -1
- package/dist/interactive/ledger.d.ts +20 -0
- package/dist/interactive/ledger.d.ts.map +1 -1
- package/dist/interactive/ledger.js +32 -0
- package/dist/interactive/ledger.js.map +1 -1
- package/dist/interactive/ws-relay.d.ts +10 -0
- package/dist/interactive/ws-relay.d.ts.map +1 -1
- package/dist/interactive/ws-relay.js +16 -0
- package/dist/interactive/ws-relay.js.map +1 -1
- package/dist/projects/graph-paths.d.ts +7 -22
- package/dist/projects/graph-paths.d.ts.map +1 -1
- package/dist/projects/graph-paths.js +10 -37
- package/dist/projects/graph-paths.js.map +1 -1
- package/dist/projects/settings.d.ts +32 -2
- package/dist/projects/settings.d.ts.map +1 -1
- package/dist/projects/settings.js +48 -6
- package/dist/projects/settings.js.map +1 -1
- package/dist/projects/state-home.d.ts +42 -0
- package/dist/projects/state-home.d.ts.map +1 -0
- package/dist/projects/state-home.js +55 -0
- package/dist/projects/state-home.js.map +1 -0
- package/dist/qe/acceptance.d.ts +9 -0
- package/dist/qe/acceptance.d.ts.map +1 -1
- package/dist/qe/acceptance.js +21 -1
- package/dist/qe/acceptance.js.map +1 -1
- package/dist/studio/assets/index-gtE6scX0.js +537 -0
- package/dist/studio/index.html +1 -1
- package/dist/studio/testid-inventory.json +154 -153
- package/endpoint-manifest.json +28 -2
- package/package.json +4 -4
- package/dist/studio/assets/index-BjCZdfES.js +0 -537
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE STEERING-AUTHOR LANDING (crew#388) — the post-approval write that closes TH-12.
|
|
3
|
+
*
|
|
4
|
+
* ## The defect
|
|
5
|
+
*
|
|
6
|
+
* The `steering-author` workflow's doctrine says "approved rules land via the rules CRUD with
|
|
7
|
+
* `provenance.source: "chat"` — the run itself writes nothing", and the propose phase tells the
|
|
8
|
+
* worker "do not write them yourself". Correct on both counts — evaluator≠creator — but NO
|
|
9
|
+
* component performed the write: the generic `POST /runs/:id/gate` only confirmed the gate, core
|
|
10
|
+
* has no steering-author hook, and the studio's `onRunResolved` only reloads rules. Every
|
|
11
|
+
* chat-authored rule ever approved was silently lost, orphaned as prose in the run's transcript
|
|
12
|
+
* (campaign 2026-09-01, scenario C5, run f3db4335).
|
|
13
|
+
*
|
|
14
|
+
* ## The design
|
|
15
|
+
*
|
|
16
|
+
* The landing is CREW-side, on APPROVE of the propose gate — the run keeps writing nothing to the
|
|
17
|
+
* store. The proposal is read machine-readably first: the propose phase now ALSO writes the
|
|
18
|
+
* proposed-rules JSON to a declared per-run artifact (`<steering inbox>/proposed-rules.json`,
|
|
19
|
+
* inside the run's own `extraWriteRoots`) — an ARTIFACT of the proposal, not a store write, so
|
|
20
|
+
* evaluator≠creator holds. Parsing the propose unit's stored transcript is the FALLBACK (runs
|
|
21
|
+
* launched before this fix, or a worker that ignored the file contract), and the cached gate
|
|
22
|
+
* prompt is the last resort (the transcript is written only on approval, so a store hiccup there
|
|
23
|
+
* still has the prompt core composed FROM that output).
|
|
24
|
+
*
|
|
25
|
+
* Invariants:
|
|
26
|
+
* - AUDITABLE — every landed rule records `governance.rule.upserted` with the chat provenance
|
|
27
|
+
* and the run id, exactly as the doctrine documents; a landing that could not happen records
|
|
28
|
+
* `governance.steering.landing_failed` with the reason.
|
|
29
|
+
* - IDEMPOTENT — a durable marker (`landed.json` beside the proposal) is written after a full
|
|
30
|
+
* landing; a replayed approve (daemon restart mid-request, double-driven gate) finds it and
|
|
31
|
+
* re-lands nothing. The rule write itself is an UPSERT keyed by rule id, so even a lost
|
|
32
|
+
* marker degrades to a same-content overwrite, never a duplicate rule.
|
|
33
|
+
* - FAIL-LOUD — when no source yields a parseable proposal the approve still succeeds for the
|
|
34
|
+
* RUN (the gate decision is the operator's and already happened), but the response and the
|
|
35
|
+
* audit trail carry an explicit `landing.outcome: "failed"` with the reason. A silent no-op
|
|
36
|
+
* is the bug this module exists to end.
|
|
37
|
+
* - AMEND-APPROVE — an approve carrying an amend note lands the proposal UNCHANGED: the amend
|
|
38
|
+
* steers the RUN (core injects it into the run's continuation), not the rule text. An
|
|
39
|
+
* operator who wants different rules rejects and re-authors; editing rule text inside a gate
|
|
40
|
+
* note would land words nobody reviewed as JSON nobody saw.
|
|
41
|
+
*/
|
|
42
|
+
import { promises as fsp } from 'node:fs';
|
|
43
|
+
import { join } from 'node:path';
|
|
44
|
+
import { resolveRunWorkflow } from '../qe/acceptance.js';
|
|
45
|
+
import { coreUnitId } from './evidence.js';
|
|
46
|
+
import { STEERING_PROPOSAL_FILENAME, STEERING_TYPES, steeringInboxDir } from './governance-steering.js';
|
|
47
|
+
export { STEERING_PROPOSAL_FILENAME } from './governance-steering.js';
|
|
48
|
+
/** The workflow whose propose gate this landing serves. */
|
|
49
|
+
export const STEERING_AUTHOR_WORKFLOW = 'steering-author';
|
|
50
|
+
/** Durable idempotency marker written beside the proposal after a full landing. */
|
|
51
|
+
export const STEERING_LANDED_MARKER_FILENAME = 'landed.json';
|
|
52
|
+
/** Where the propose phase is told to write its machine-readable proposal. */
|
|
53
|
+
export function steeringProposalPath(runId) {
|
|
54
|
+
return join(steeringInboxDir(runId), STEERING_PROPOSAL_FILENAME);
|
|
55
|
+
}
|
|
56
|
+
/** Where a completed landing records what it landed (replay dedupe). */
|
|
57
|
+
export function steeringLandedMarkerPath(runId) {
|
|
58
|
+
return join(steeringInboxDir(runId), STEERING_LANDED_MARKER_FILENAME);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The steering fields a rule write may carry (the merged model's additions) — mirrored by
|
|
62
|
+
* `POST /governance/rules`, which refuses them on a pre-steering engine because the engine
|
|
63
|
+
* would silently DROP them and persist a rule that enforces differently than written. The
|
|
64
|
+
* landing applies the same guard: authored-then-downgraded is a loud landing failure, never
|
|
65
|
+
* a quiet field amputation.
|
|
66
|
+
*/
|
|
67
|
+
export const STEERING_RULE_FIELDS = [
|
|
68
|
+
'steering_type',
|
|
69
|
+
'applies_to',
|
|
70
|
+
'excludes',
|
|
71
|
+
'weight',
|
|
72
|
+
'effect',
|
|
73
|
+
'trigger',
|
|
74
|
+
'obligations',
|
|
75
|
+
'criteria',
|
|
76
|
+
];
|
|
77
|
+
/** True when this run IS a steering-author run (by workflow id, or — for runs whose stored
|
|
78
|
+
* `workflow_id` is core's `wf-<uuid>` instance id — by exact phase sequence). Defensive on a
|
|
79
|
+
* view without a `workflow_id` (partial-stub adapters in tests; a free-text run has no def):
|
|
80
|
+
* "not steering-author" is the honest answer there, and the legacy gate path must not 500. */
|
|
81
|
+
export function isSteeringAuthorRun(run, workflows) {
|
|
82
|
+
if (typeof run.session.workflow_id !== 'string')
|
|
83
|
+
return false;
|
|
84
|
+
return resolveRunWorkflow(run, workflows)?.id === STEERING_AUTHOR_WORKFLOW;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Recover the default steering type from the author route's own problem-statement preamble
|
|
88
|
+
* (`Author steering rules for the '<type>' steering type …`) so a proposed rule that omitted
|
|
89
|
+
* `steering_type` still lands on the sub-page the operator authored it for. `undefined` when
|
|
90
|
+
* the problem does not carry the preamble (a hand-launched run) — the engine's serde default
|
|
91
|
+
* (`architecture`) then applies, same as any other write.
|
|
92
|
+
*/
|
|
93
|
+
export function steeringTypeFromProblem(problem) {
|
|
94
|
+
const m = /^Author steering rules for the '([a-z-]+)' steering type/.exec(problem);
|
|
95
|
+
const type = m?.[1];
|
|
96
|
+
return type !== undefined && STEERING_TYPES.has(type) ? type : undefined;
|
|
97
|
+
}
|
|
98
|
+
function isRawRule(v) {
|
|
99
|
+
return (typeof v === 'object' &&
|
|
100
|
+
v !== null &&
|
|
101
|
+
!Array.isArray(v) &&
|
|
102
|
+
typeof v['id'] === 'string' &&
|
|
103
|
+
v['id'].trim().length > 0 &&
|
|
104
|
+
typeof v['statement'] === 'string' &&
|
|
105
|
+
v['statement'].trim().length > 0);
|
|
106
|
+
}
|
|
107
|
+
/** Accept a bare array or a `{ rules: [...] }` wrapper; every entry must look like a rule. */
|
|
108
|
+
function rulesFromJson(parsed) {
|
|
109
|
+
const arr = Array.isArray(parsed)
|
|
110
|
+
? parsed
|
|
111
|
+
: typeof parsed === 'object' && parsed !== null && Array.isArray(parsed['rules'])
|
|
112
|
+
? parsed['rules']
|
|
113
|
+
: null;
|
|
114
|
+
if (arr === null || arr.length === 0)
|
|
115
|
+
return null;
|
|
116
|
+
return arr.every(isRawRule) ? arr : null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Extract the proposed rules from free text — the whole text as JSON first (the deliverable
|
|
120
|
+
* file's shape), then each fenced code block, then every balanced top-level `[...]` in the
|
|
121
|
+
* text (the transcript / gate-prompt shape, where the JSON array is embedded in prose).
|
|
122
|
+
* `null` when nothing parses to a non-empty array of `{id, statement}` objects.
|
|
123
|
+
*/
|
|
124
|
+
export function extractProposedRules(text) {
|
|
125
|
+
const trimmed = text.trim();
|
|
126
|
+
if (trimmed.length === 0)
|
|
127
|
+
return null;
|
|
128
|
+
// 1. The whole text is the JSON (the deliverable-file contract).
|
|
129
|
+
try {
|
|
130
|
+
const whole = rulesFromJson(JSON.parse(trimmed));
|
|
131
|
+
if (whole !== null)
|
|
132
|
+
return whole;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
/* fall through to embedded extraction */
|
|
136
|
+
}
|
|
137
|
+
// 2. Fenced code blocks — workers habitually wrap JSON in ```json fences.
|
|
138
|
+
for (const m of trimmed.matchAll(/```(?:json)?\s*\n([\s\S]*?)```/g)) {
|
|
139
|
+
const block = m[1];
|
|
140
|
+
if (block === undefined)
|
|
141
|
+
continue;
|
|
142
|
+
try {
|
|
143
|
+
const rules = rulesFromJson(JSON.parse(block.trim()));
|
|
144
|
+
if (rules !== null)
|
|
145
|
+
return rules;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
/* try the next block */
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// 3. Balanced top-level arrays embedded in prose. String-aware bracket walk, so a `]` inside
|
|
152
|
+
// a statement never closes the array early.
|
|
153
|
+
for (let i = trimmed.indexOf('['); i !== -1; i = trimmed.indexOf('[', i + 1)) {
|
|
154
|
+
let depth = 0;
|
|
155
|
+
let inString = false;
|
|
156
|
+
let escaped = false;
|
|
157
|
+
for (let j = i; j < trimmed.length; j++) {
|
|
158
|
+
const ch = trimmed[j];
|
|
159
|
+
if (escaped) {
|
|
160
|
+
escaped = false;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (inString) {
|
|
164
|
+
if (ch === '\\')
|
|
165
|
+
escaped = true;
|
|
166
|
+
else if (ch === '"')
|
|
167
|
+
inString = false;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (ch === '"')
|
|
171
|
+
inString = true;
|
|
172
|
+
else if (ch === '[')
|
|
173
|
+
depth++;
|
|
174
|
+
else if (ch === ']') {
|
|
175
|
+
depth--;
|
|
176
|
+
if (depth === 0) {
|
|
177
|
+
try {
|
|
178
|
+
const rules = rulesFromJson(JSON.parse(trimmed.slice(i, j + 1)));
|
|
179
|
+
if (rules !== null)
|
|
180
|
+
return rules;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
/* not JSON — keep scanning from the next '[' */
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Stamp the chat provenance and the recovered default steering type onto one proposed rule.
|
|
194
|
+
* `source: "chat"` is FORCED, not defaulted — the landing is the doctrine's one write path for
|
|
195
|
+
* chat-authored rules, and a worker that wrote some other source into its proposal must not be
|
|
196
|
+
* able to disguise where the rule came from. Everything else passes through untouched: the
|
|
197
|
+
* ENGINE's upsert path is the one spelling of what a valid rule is.
|
|
198
|
+
*/
|
|
199
|
+
export function normalizeProposedRule(raw, defaultType) {
|
|
200
|
+
const prior = typeof raw['provenance'] === 'object' && raw['provenance'] !== null && !Array.isArray(raw['provenance'])
|
|
201
|
+
? raw['provenance']
|
|
202
|
+
: {};
|
|
203
|
+
const sourceKinds = Array.isArray(prior['source_kinds']) ? prior['source_kinds'] : [];
|
|
204
|
+
const out = {
|
|
205
|
+
...raw,
|
|
206
|
+
provenance: { ...prior, source: 'chat', source_kinds: sourceKinds },
|
|
207
|
+
};
|
|
208
|
+
const declaredType = raw['steering_type'];
|
|
209
|
+
if ((typeof declaredType !== 'string' || !STEERING_TYPES.has(declaredType)) &&
|
|
210
|
+
defaultType !== undefined) {
|
|
211
|
+
out['steering_type'] = defaultType;
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
/** Read a file, mapping ONLY a missing file to null — any other error (EACCES, EIO) throws. */
|
|
216
|
+
async function readIfExists(path) {
|
|
217
|
+
try {
|
|
218
|
+
return await fsp.readFile(path, 'utf8');
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
if (err.code === 'ENOENT')
|
|
222
|
+
return null;
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Perform the landing for an APPROVED steering-author propose gate. Called AFTER
|
|
228
|
+
* `confirmGate` resolved — the gate decision is already durable; this function's only job is
|
|
229
|
+
* the write the doctrine promised. Never throws: every failure is folded into a loud
|
|
230
|
+
* `outcome: "failed"` result (and the audit trail), because a landing error must not turn an
|
|
231
|
+
* already-successful gate approval into an HTTP 500.
|
|
232
|
+
*
|
|
233
|
+
* `gatePrompt` is the cached gate prompt captured BEFORE the confirm (terminal events prune
|
|
234
|
+
* the cache) — the last-resort proposal source.
|
|
235
|
+
*/
|
|
236
|
+
export async function landSteeringProposal(deps, run, gatePrompt) {
|
|
237
|
+
const runId = run.session.id;
|
|
238
|
+
const { adapter, audit, actor } = deps;
|
|
239
|
+
const failed = (error, landedIds = [], source) => {
|
|
240
|
+
audit.record('governance.steering.landing_failed', actor, {
|
|
241
|
+
runId,
|
|
242
|
+
detail: { error, ...(landedIds.length > 0 ? { landedRuleIds: landedIds } : {}) },
|
|
243
|
+
});
|
|
244
|
+
return {
|
|
245
|
+
outcome: 'failed',
|
|
246
|
+
ruleIds: landedIds,
|
|
247
|
+
...(source !== undefined ? { source } : {}),
|
|
248
|
+
error,
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
// ── Idempotency: a replayed approve re-lands nothing ─────────────────────────
|
|
252
|
+
let markerText = null;
|
|
253
|
+
try {
|
|
254
|
+
markerText = await readIfExists(steeringLandedMarkerPath(runId));
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
/* an unreadable marker (not just missing) is treated as absent — the upsert is
|
|
258
|
+
idempotent by rule id, so the worst case is a same-content overwrite */
|
|
259
|
+
}
|
|
260
|
+
if (markerText !== null) {
|
|
261
|
+
try {
|
|
262
|
+
const marker = JSON.parse(markerText);
|
|
263
|
+
if (Array.isArray(marker.ruleIds)) {
|
|
264
|
+
return {
|
|
265
|
+
outcome: 'landed',
|
|
266
|
+
ruleIds: marker.ruleIds,
|
|
267
|
+
source: marker.source,
|
|
268
|
+
alreadyLanded: true,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
/* a corrupt marker is treated as absent — the upsert is idempotent anyway */
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// ── Read the proposal: deliverable file → stored transcript → cached gate prompt ──
|
|
277
|
+
let rawRules = null;
|
|
278
|
+
let source;
|
|
279
|
+
let fileText = null;
|
|
280
|
+
try {
|
|
281
|
+
fileText = await readIfExists(steeringProposalPath(runId));
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
// A present-but-unreadable deliverable (EACCES, EIO) must NOT silently fall back to the
|
|
285
|
+
// transcript — that would mask an operational failure. Fail the landing loudly instead.
|
|
286
|
+
return failed(`the run's proposal deliverable exists but could not be read: ${err instanceof Error ? err.message : String(err)}`);
|
|
287
|
+
}
|
|
288
|
+
if (fileText !== null) {
|
|
289
|
+
rawRules = extractProposedRules(fileText);
|
|
290
|
+
if (rawRules !== null)
|
|
291
|
+
source = 'deliverable';
|
|
292
|
+
}
|
|
293
|
+
if (rawRules === null) {
|
|
294
|
+
// The transcript is stored on approval (deny-dominates writes no output for a rejected
|
|
295
|
+
// unit), so post-confirm it is readable. Find the propose unit by its phase-suffixed id.
|
|
296
|
+
const proposeUnit = run.units.find((u) => u.id === `${runId}:propose` || u.id.endsWith(':propose'));
|
|
297
|
+
if (proposeUnit !== undefined) {
|
|
298
|
+
try {
|
|
299
|
+
const transcript = await adapter.workOutput(coreUnitId(runId, proposeUnit));
|
|
300
|
+
if (transcript !== null) {
|
|
301
|
+
rawRules = extractProposedRules(transcript);
|
|
302
|
+
if (rawRules !== null)
|
|
303
|
+
source = 'transcript';
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
/* an unreadable transcript falls through to the gate prompt */
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (rawRules === null && gatePrompt !== undefined) {
|
|
312
|
+
rawRules = extractProposedRules(gatePrompt);
|
|
313
|
+
if (rawRules !== null)
|
|
314
|
+
source = 'gate-prompt';
|
|
315
|
+
}
|
|
316
|
+
if (rawRules === null || source === undefined) {
|
|
317
|
+
return failed(`the approved steering proposal could not be parsed from the run record: no ` +
|
|
318
|
+
`${STEERING_PROPOSAL_FILENAME} in the run's steering inbox, and no JSON array of ` +
|
|
319
|
+
`{id, statement} rule objects in the propose unit's stored transcript or gate prompt. ` +
|
|
320
|
+
`The gate approval stands, but NO rules were landed — re-author via ` +
|
|
321
|
+
`POST /governance/steering/author, or land the transcript's rules by hand through ` +
|
|
322
|
+
`POST /governance/rules.`);
|
|
323
|
+
}
|
|
324
|
+
const defaultType = steeringTypeFromProblem(run.session.problem);
|
|
325
|
+
const rules = rawRules.map((r) => normalizeProposedRule(r, defaultType));
|
|
326
|
+
// ── The same pre-steering-engine guard as POST /governance/rules ─────────────
|
|
327
|
+
if (!adapter.steeringSupported()) {
|
|
328
|
+
const carrying = rules.filter((r) => STEERING_RULE_FIELDS.some((f) => r[f] !== undefined));
|
|
329
|
+
if (carrying.length > 0) {
|
|
330
|
+
return failed(`the installed engine predates the steering model and would silently drop the steering ` +
|
|
331
|
+
`fields on ${carrying.length} proposed rule(s) — landing refused so the rules are not ` +
|
|
332
|
+
`persisted enforcing differently than authored. Upgrade wicked-core-ts (>= 0.7.5) and ` +
|
|
333
|
+
`re-author.`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// ── Land, auditing each write exactly as the doctrine documents ──────────────
|
|
337
|
+
const landedIds = [];
|
|
338
|
+
const failures = [];
|
|
339
|
+
for (const rule of rules) {
|
|
340
|
+
try {
|
|
341
|
+
await adapter.upsertConformanceRule(rule);
|
|
342
|
+
landedIds.push(rule.id);
|
|
343
|
+
audit.record('governance.rule.upserted', actor, {
|
|
344
|
+
runId,
|
|
345
|
+
detail: { id: rule.id, source: 'chat', via: STEERING_AUTHOR_WORKFLOW },
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
failures.push(`${rule.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (failures.length > 0) {
|
|
353
|
+
return failed(`landing was partial: ${landedIds.length} of ${rules.length} proposed rule(s) landed; ` +
|
|
354
|
+
`the store refused ${failures.join('; ')}`, landedIds, source);
|
|
355
|
+
}
|
|
356
|
+
// ── Durable marker (best-effort — the upsert is idempotent even without it) ──
|
|
357
|
+
try {
|
|
358
|
+
await fsp.mkdir(steeringInboxDir(runId), { recursive: true });
|
|
359
|
+
const marker = { ruleIds: landedIds, source, at: new Date().toISOString() };
|
|
360
|
+
await fsp.writeFile(steeringLandedMarkerPath(runId), JSON.stringify(marker, null, 2), 'utf8');
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
/* a missing marker degrades to a re-upsert of identical content, never a duplicate */
|
|
364
|
+
}
|
|
365
|
+
return { outcome: 'landed', ruleIds: landedIds, source };
|
|
366
|
+
}
|
|
367
|
+
//# sourceMappingURL=steering-landing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"steering-landing.js","sourceRoot":"","sources":["../../src/api/steering-landing.ts"],"names":[],"mappings":"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"}
|
package/dist/api/testing.d.ts
CHANGED
|
@@ -115,14 +115,17 @@ export declare const TestingReconSchema: z.ZodObject<{
|
|
|
115
115
|
problem: z.ZodString;
|
|
116
116
|
projectId: z.ZodOptional<z.ZodString>;
|
|
117
117
|
repoRefs: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
118
|
+
ungated: z.ZodOptional<z.ZodBoolean>;
|
|
118
119
|
}, "strict", z.ZodTypeAny, {
|
|
119
120
|
problem: string;
|
|
120
121
|
projectId?: string | undefined;
|
|
121
122
|
repoRefs?: string[] | undefined;
|
|
123
|
+
ungated?: boolean | undefined;
|
|
122
124
|
}, {
|
|
123
125
|
problem: string;
|
|
124
126
|
projectId?: string | undefined;
|
|
125
127
|
repoRefs?: string[] | undefined;
|
|
128
|
+
ungated?: boolean | undefined;
|
|
126
129
|
}>;
|
|
127
130
|
export interface TestingRoutesDeps {
|
|
128
131
|
audit: AuditLog;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../src/api/testing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,KAAK,EAAwC,MAAM,kBAAkB,CAAC;AACpF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../src/api/testing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,KAAK,EAAwC,MAAM,kBAAkB,CAAC;AACpF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAO3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AA+CvE,eAAO,MAAM,wBAAwB;;;;;;;;;EAK1B,CAAC;AAEZ,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKxB,CAAC;AAQZ,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;EAUpB,CAAC;AAEZ,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,QAAQ,CAAC;IAChB,OAAO,EAAE,CAAC,GAAG,EAAE,cAAc,GAAG;QAAE,KAAK,CAAC,EAAE,KAAK,CAAA;KAAE,KAAK,KAAK,CAAC;IAC5D,kGAAkG;IAClG,MAAM,EAAE,MAAM,OAAO,EAAE,CAAC;IACxB;+FAC2F;IAC3F,QAAQ,CAAC,EAAE;QAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;QAAC,KAAK,EAAE,eAAe,CAAA;KAAE,CAAC;CAC/D;AAED,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,eAAe,EACpB,OAAO,EAAE,WAAW,EACpB,IAAI,EAAE,iBAAiB,GACtB,IAAI,CAuVN"}
|
package/dist/api/testing.js
CHANGED
|
@@ -25,6 +25,7 @@ import { GovernanceEvalsUnsupportedError, ProjectsUnsupportedError, } from '../c
|
|
|
25
25
|
import { API_PREFIX } from './api-prefix.js';
|
|
26
26
|
import { STEERING_TYPE_VALUES } from './governance-steering.js';
|
|
27
27
|
import { resolveScopeRepos } from './multiscope.js';
|
|
28
|
+
import { buildReconCampaign, RECON_INTAKE_GATE_TOKEN } from '../campaigns/plan.js';
|
|
28
29
|
import { resolveProjectGraphBinding } from '../projects/graph.js';
|
|
29
30
|
import { MEMBERSHIP_ATTACHED, membershipAttachedKey } from '../projects/events.js';
|
|
30
31
|
const V = API_PREFIX;
|
|
@@ -78,8 +79,10 @@ export const ImportEvalCorpusSchema = z
|
|
|
78
79
|
.strict();
|
|
79
80
|
// The recon trigger's body (the pinned multiscope wire — see api/multiscope.ts): the operator's
|
|
80
81
|
// brief verbatim (`problem` — studio composes its own recon framing, this route never rewrites
|
|
81
|
-
// caller text) plus the two optional scope fields
|
|
82
|
-
//
|
|
82
|
+
// caller text) plus the two optional scope fields, plus `ungated` (crew#391): every launched
|
|
83
|
+
// sibling pauses at its intake gate BY DEFAULT — the posture the launch banner promises — and
|
|
84
|
+
// an unattended fan is an EXPLICIT request, never a silent default. Strict: a misspelled field
|
|
85
|
+
// must 400 by name, never launch a recon the caller believed was different (FINDING-031).
|
|
83
86
|
export const TestingReconSchema = z
|
|
84
87
|
.object({
|
|
85
88
|
problem: z.string().min(1),
|
|
@@ -88,6 +91,7 @@ export const TestingReconSchema = z
|
|
|
88
91
|
.array(z.string().min(1))
|
|
89
92
|
.min(1, 'repoRefs must name at least one registered repo — omit the field to scope by project alone')
|
|
90
93
|
.optional(),
|
|
94
|
+
ungated: z.boolean().optional(),
|
|
91
95
|
})
|
|
92
96
|
.strict();
|
|
93
97
|
export function registerTestingRoutes(app, adapter, deps) {
|
|
@@ -174,8 +178,11 @@ export function registerTestingRoutes(app, adapter, deps) {
|
|
|
174
178
|
// standard /ws frames. What THIS route adds is the pinned multiscope wire: `repoRefs` and/or
|
|
175
179
|
// `projectId` resolve (fail-closed, api/multiscope.ts) to registered repos, and because one
|
|
176
180
|
// engine run carries ONE repo (`LaunchOptions.repoRef` — wicked-core#179), a multi-repo recon
|
|
177
|
-
// FANS one run per repo
|
|
178
|
-
//
|
|
181
|
+
// FANS one run per repo. A real fan (≥ 2 repos) registers an ENGINE campaign and files the
|
|
182
|
+
// siblings under it (crew#390 — `GET /campaigns` serves it with real stats); every sibling
|
|
183
|
+
// pauses at its intake gate unless the caller EXPLICITLY sent `ungated: true` (crew#391 — the
|
|
184
|
+
// launch banner's promise is the default, never silently 'none'). Neither scope field ⇒ one
|
|
185
|
+
// unscoped run — the launch the studio ReconPanel sends today, kept backward compatible.
|
|
179
186
|
app.post(`${V}/testing/recon`, {
|
|
180
187
|
config: {
|
|
181
188
|
manifest: {
|
|
@@ -209,9 +216,130 @@ export function registerTestingRoutes(app, adapter, deps) {
|
|
|
209
216
|
if (!scope.ok) {
|
|
210
217
|
return reply.code(scope.status).send({ error: scope.error });
|
|
211
218
|
}
|
|
212
|
-
// The shared campaign label every fanned run files under (audit detail + response) —
|
|
213
|
-
//
|
|
219
|
+
// The shared campaign label every fanned run files under (audit detail + response) — and,
|
|
220
|
+
// for a real fan (≥ 2 repos), the id of the ENGINE campaign it registers (crew#390).
|
|
214
221
|
const campaign = `recon-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
|
|
222
|
+
// The gate posture (crew#391): the intake gate per sibling — the exact promise the launch
|
|
223
|
+
// banner makes ("stops at its intake gate; nothing runs until you approve") — unless the
|
|
224
|
+
// caller EXPLICITLY sent `ungated: true`. Never a silent default the banner contradicts.
|
|
225
|
+
const gate = b.ungated === true ? 'none' : RECON_INTAKE_GATE_TOKEN;
|
|
226
|
+
/** The audit-detail fields every launch spelling shares. */
|
|
227
|
+
const gateDetail = { gate, ...(b.ungated === true ? { ungated: true } : {}) };
|
|
228
|
+
/** The post-commit half of the §2.2 filing (the POST /runs idiom): tag future /ws frames
|
|
229
|
+
* and emit the membership event with the AUTHENTICATED actor id. */
|
|
230
|
+
const fileIntoProject = (runId, attachedAt) => {
|
|
231
|
+
if (b.projectId === undefined || deps.projects === undefined)
|
|
232
|
+
return;
|
|
233
|
+
deps.projects.index.set(runId, b.projectId);
|
|
234
|
+
deps.projects.bus?.emit(MEMBERSHIP_ATTACHED, {
|
|
235
|
+
project_id: b.projectId,
|
|
236
|
+
member: { kind: 'crew.run', ref: runId },
|
|
237
|
+
actor: actorOf(req).id,
|
|
238
|
+
}, membershipAttachedKey(b.projectId, 'crew.run', runId, attachedAt));
|
|
239
|
+
};
|
|
240
|
+
// ── A real fan (≥ 2 repos): register the ENGINE campaign and file the runs under it ────
|
|
241
|
+
// (crew#390, option (a)): the fan converges with the campaign-launch path — one durable
|
|
242
|
+
// `CampaignDef` (one agent node per repo, no edges, fan-width concurrency), scheduled and
|
|
243
|
+
// persisted by the engine, so `GET /campaigns` serves it with real per-node stats instead
|
|
244
|
+
// of a label only the audit trail could see. The per-run loop below stays for the
|
|
245
|
+
// single/unscoped recon (today's shape, unchanged) and as the honest fallback on an
|
|
246
|
+
// engine addon without the campaign bindings — and on win32, where the engine's
|
|
247
|
+
// `wicked-worktrees/<run_id>` path (':' in the run id) cannot exist on the filesystem.
|
|
248
|
+
if (scope.repos.length >= 2 && adapter.campaignsSupported() && process.platform !== 'win32') {
|
|
249
|
+
const built = buildReconCampaign({
|
|
250
|
+
id: campaign,
|
|
251
|
+
problem: b.problem,
|
|
252
|
+
repos: scope.repos,
|
|
253
|
+
clis: deps.roster(),
|
|
254
|
+
ungated: b.ungated === true,
|
|
255
|
+
});
|
|
256
|
+
// Pre-provision every node's worktree BEFORE the launch (the engine names worktree
|
|
257
|
+
// branches `wicked/{run_id}` verbatim, and a node run id is not a legal git ref — see
|
|
258
|
+
// campaigns/worktrees.ts). Fail-closed: a git refusal aborts with nothing scheduled.
|
|
259
|
+
const rootOf = new Map((await adapter.listRepos()).map((r) => [r.id, r.root_path]));
|
|
260
|
+
let rollbackWorktrees;
|
|
261
|
+
try {
|
|
262
|
+
rollbackWorktrees = await adapter.provisionCampaignWorktrees(scope.repos.map((r, i) => {
|
|
263
|
+
const repoRoot = rootOf.get(r.id);
|
|
264
|
+
if (repoRoot === undefined) {
|
|
265
|
+
// Unreachable (resolveScopeRepos just admitted these ids) — but a missing root
|
|
266
|
+
// must abort, never provision a worktree in a guessed location.
|
|
267
|
+
throw new Error(`repo '${r.id}' vanished from the registry mid-launch`);
|
|
268
|
+
}
|
|
269
|
+
return { runId: built.runIds[i], repoRoot };
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
return reply.code(500).send({ error: message(err) });
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
await adapter.launchCampaign(built.def);
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
// Nothing launched (the engine validates-then-persists the def as one unit): reclaim
|
|
280
|
+
// the pre-provisioned worktrees, then answer with the campaign-route posture — 409
|
|
281
|
+
// for a state conflict, else 500 (the def is DAEMON-built from already-validated
|
|
282
|
+
// inputs, so a reject here is ours, not "fix your request").
|
|
283
|
+
await rollbackWorktrees().catch(() => { });
|
|
284
|
+
const msg = message(err);
|
|
285
|
+
const busy = /already exists|already launched|busy|in flight/i.test(msg);
|
|
286
|
+
return reply.code(busy ? 409 : 500).send({ error: msg });
|
|
287
|
+
}
|
|
288
|
+
audit.record('campaign.launched', actorOf(req), {
|
|
289
|
+
detail: {
|
|
290
|
+
campaignId: campaign,
|
|
291
|
+
nodes: built.def.nodes.length,
|
|
292
|
+
edges: 0,
|
|
293
|
+
policy: built.def.policy,
|
|
294
|
+
maxConcurrency: built.def.max_concurrency,
|
|
295
|
+
recon: true,
|
|
296
|
+
...gateDetail,
|
|
297
|
+
...(b.projectId !== undefined ? { projectId: b.projectId } : {}),
|
|
298
|
+
repoRefs: scope.repos.map((r) => r.id),
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
// Campaign nodes are UNFILED at the engine seam (`RunSpec::to_launch_spec` pins
|
|
302
|
+
// `project_id: None` — filing rides the daemon, per the engine's own doc), so a
|
|
303
|
+
// projectId recon files each sibling here: the membership row via the projects surface,
|
|
304
|
+
// then the same post-commit index/event half `POST /runs` performs. The node run ids
|
|
305
|
+
// are deterministic (`{campaign}:{node}:a0`), so the rows land before any run finishes.
|
|
306
|
+
let projectAttachError;
|
|
307
|
+
for (const [i, runId] of built.runIds.entries()) {
|
|
308
|
+
if (b.projectId !== undefined) {
|
|
309
|
+
try {
|
|
310
|
+
const { member } = await adapter.projectMemberAttach(b.projectId, 'crew.run', runId, { campaign, recon: true }, actorOf(req).id);
|
|
311
|
+
fileIntoProject(runId, member.attached_at);
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
// The campaign is LIVE — failing the request now would report a launch that
|
|
315
|
+
// happened as one that did not. Name the filing gap instead (the chats-route
|
|
316
|
+
// idiom): the operator can re-attach via POST /projects/:id/members.
|
|
317
|
+
projectAttachError ??= message(err);
|
|
318
|
+
req.log.warn({ runId, projectId: b.projectId }, `recon run filing failed: ${message(err)}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// The same trail entry POST /runs writes — each node IS a run launch this route
|
|
322
|
+
// caused, findable by the same `?action=run.launched` query, grouped by the label.
|
|
323
|
+
audit.record('run.launched', actorOf(req), {
|
|
324
|
+
runId,
|
|
325
|
+
detail: {
|
|
326
|
+
campaign,
|
|
327
|
+
recon: true,
|
|
328
|
+
...gateDetail,
|
|
329
|
+
repoRef: scope.repos[i].id,
|
|
330
|
+
...(b.projectId !== undefined ? { projectId: b.projectId } : {}),
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
return reply.code(201).send({
|
|
335
|
+
runId: built.runIds[0],
|
|
336
|
+
runIds: built.runIds,
|
|
337
|
+
campaign,
|
|
338
|
+
campaignRegistered: true,
|
|
339
|
+
...(projectAttachError !== undefined ? { projectAttachError } : {}),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
// ── The per-run path: single repo / unscoped (today's shape), or no campaign bindings ──
|
|
215
343
|
// No scope fields ⇒ one unscoped run (today's recon), spelled as a single null target.
|
|
216
344
|
const targets = scope.repos.length > 0 ? scope.repos : [null];
|
|
217
345
|
const runIds = [];
|
|
@@ -221,6 +349,9 @@ export function registerTestingRoutes(app, adapter, deps) {
|
|
|
221
349
|
problem: b.problem,
|
|
222
350
|
sessionId: runId,
|
|
223
351
|
clisJson: JSON.stringify(deps.roster()),
|
|
352
|
+
// crew#391: the intake gate rides EVERY spelling of the recon launch — the fallback
|
|
353
|
+
// fan and the single run hold the same posture the banner promises.
|
|
354
|
+
humanConfirm: gate,
|
|
224
355
|
};
|
|
225
356
|
if (target !== null)
|
|
226
357
|
input.repoRef = target.id;
|
|
@@ -247,6 +378,7 @@ export function registerTestingRoutes(app, adapter, deps) {
|
|
|
247
378
|
`${runIds.length} run(s) launched: ${msg}`,
|
|
248
379
|
runIds,
|
|
249
380
|
campaign,
|
|
381
|
+
campaignRegistered: false,
|
|
250
382
|
});
|
|
251
383
|
}
|
|
252
384
|
if (err instanceof ProjectsUnsupportedError) {
|
|
@@ -262,26 +394,22 @@ export function registerTestingRoutes(app, adapter, deps) {
|
|
|
262
394
|
detail: {
|
|
263
395
|
campaign,
|
|
264
396
|
recon: true,
|
|
397
|
+
...gateDetail,
|
|
265
398
|
...(target !== null ? { repoRef: target.id } : {}),
|
|
266
399
|
...(b.projectId !== undefined ? { projectId: b.projectId } : {}),
|
|
267
400
|
},
|
|
268
401
|
});
|
|
269
|
-
|
|
270
|
-
// The post-commit half of the §2.2 filing (the POST /runs idiom): tag future /ws
|
|
271
|
-
// frames and emit the membership event with the AUTHENTICATED actor id.
|
|
272
|
-
deps.projects.index.set(runId, b.projectId);
|
|
273
|
-
deps.projects.bus?.emit(MEMBERSHIP_ATTACHED, {
|
|
274
|
-
project_id: b.projectId,
|
|
275
|
-
member: { kind: 'crew.run', ref: runId },
|
|
276
|
-
actor: actorOf(req).id,
|
|
277
|
-
}, membershipAttachedKey(b.projectId, 'crew.run', runId, Date.now()));
|
|
278
|
-
}
|
|
402
|
+
fileIntoProject(runId, Date.now());
|
|
279
403
|
runIds.push(runId);
|
|
280
404
|
}
|
|
281
405
|
// `runId` (first) is the single-run spelling the ReconPanel's launch already reads;
|
|
282
406
|
// `runIds` is the source of truth (length ≥ 1 always — one entry per fanned repo, in the
|
|
283
|
-
// caller's resolved order; one entry for an unscoped recon).
|
|
284
|
-
|
|
407
|
+
// caller's resolved order; one entry for an unscoped recon). `campaignRegistered: false`
|
|
408
|
+
// says honestly that THIS launch will not appear on GET /campaigns (a single/unscoped
|
|
409
|
+
// recon, or an engine addon without the campaign bindings).
|
|
410
|
+
return reply
|
|
411
|
+
.code(201)
|
|
412
|
+
.send({ runId: runIds[0], runIds, campaign, campaignRegistered: false });
|
|
285
413
|
});
|
|
286
414
|
}
|
|
287
415
|
//# sourceMappingURL=testing.js.map
|