wicked-crew 0.6.0 → 0.7.1
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/README.md +103 -0
- package/dist/api/audit.d.ts +13 -0
- package/dist/api/audit.d.ts.map +1 -1
- package/dist/api/audit.js +18 -2
- package/dist/api/audit.js.map +1 -1
- package/dist/api/endpoint-manifest-live.d.ts +29 -0
- package/dist/api/endpoint-manifest-live.d.ts.map +1 -0
- package/dist/api/endpoint-manifest-live.js +72 -0
- package/dist/api/endpoint-manifest-live.js.map +1 -0
- package/dist/api/endpoint-manifest.d.ts +107 -0
- package/dist/api/endpoint-manifest.d.ts.map +1 -0
- package/dist/api/endpoint-manifest.js +108 -0
- package/dist/api/endpoint-manifest.js.map +1 -0
- package/dist/api/guidance-index.d.ts +39 -0
- package/dist/api/guidance-index.d.ts.map +1 -0
- package/dist/api/guidance-index.js +67 -0
- package/dist/api/guidance-index.js.map +1 -0
- package/dist/api/open-path.d.ts +16 -0
- package/dist/api/open-path.d.ts.map +1 -1
- package/dist/api/open-path.js +22 -0
- package/dist/api/open-path.js.map +1 -1
- package/dist/api/requirements.d.ts +7 -0
- package/dist/api/requirements.d.ts.map +1 -1
- package/dist/api/requirements.js +23 -2
- package/dist/api/requirements.js.map +1 -1
- package/dist/api/retry-index.d.ts +30 -0
- package/dist/api/retry-index.d.ts.map +1 -0
- package/dist/api/retry-index.js +45 -0
- package/dist/api/retry-index.js.map +1 -0
- package/dist/api/routes.d.ts +53 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +399 -30
- package/dist/api/routes.js.map +1 -1
- package/dist/api/run-files.d.ts +63 -0
- package/dist/api/run-files.d.ts.map +1 -0
- package/dist/api/run-files.js +271 -0
- package/dist/api/run-files.js.map +1 -0
- package/dist/api/server.d.ts +79 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +183 -8
- package/dist/api/server.js.map +1 -1
- package/dist/api/stall-watchdog.d.ts +62 -0
- package/dist/api/stall-watchdog.d.ts.map +1 -0
- package/dist/api/stall-watchdog.js +138 -0
- package/dist/api/stall-watchdog.js.map +1 -0
- package/dist/cli/index.js +89 -15
- package/dist/cli/index.js.map +1 -1
- package/dist/core/adapter.d.ts +36 -10
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +245 -31
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/bridge-reaper.d.ts +134 -0
- package/dist/core/bridge-reaper.d.ts.map +1 -0
- package/dist/core/bridge-reaper.js +286 -0
- package/dist/core/bridge-reaper.js.map +1 -0
- package/dist/core/deliver.d.ts +118 -0
- package/dist/core/deliver.d.ts.map +1 -0
- package/dist/core/deliver.js +241 -0
- package/dist/core/deliver.js.map +1 -0
- package/dist/core/deliverable-floor.d.ts +155 -0
- package/dist/core/deliverable-floor.d.ts.map +1 -0
- package/dist/core/deliverable-floor.js +248 -0
- package/dist/core/deliverable-floor.js.map +1 -0
- package/dist/core/exec.d.ts +2 -0
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js.map +1 -1
- package/dist/core/types.d.ts +79 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -1
- package/dist/interactive/bridge-pool.d.ts +52 -0
- package/dist/interactive/bridge-pool.d.ts.map +1 -1
- package/dist/interactive/bridge-pool.js +93 -12
- package/dist/interactive/bridge-pool.js.map +1 -1
- package/dist/interactive/chat-events.d.ts +207 -0
- package/dist/interactive/chat-events.d.ts.map +1 -0
- package/dist/interactive/chat-events.js +769 -0
- package/dist/interactive/chat-events.js.map +1 -0
- package/dist/interactive/demo-events.d.ts +283 -0
- package/dist/interactive/demo-events.d.ts.map +1 -0
- package/dist/interactive/demo-events.js +889 -0
- package/dist/interactive/demo-events.js.map +1 -0
- package/dist/interactive/draft-events.d.ts +87 -7
- package/dist/interactive/draft-events.d.ts.map +1 -1
- package/dist/interactive/draft-events.js +352 -49
- package/dist/interactive/draft-events.js.map +1 -1
- package/dist/interactive/edit-events.d.ts +25 -2
- package/dist/interactive/edit-events.d.ts.map +1 -1
- package/dist/interactive/edit-events.js +88 -9
- package/dist/interactive/edit-events.js.map +1 -1
- package/dist/interactive/repo-snapshot.d.ts +100 -0
- package/dist/interactive/repo-snapshot.d.ts.map +1 -0
- package/dist/interactive/repo-snapshot.js +289 -0
- package/dist/interactive/repo-snapshot.js.map +1 -0
- package/dist/projects/graph-paths.d.ts +122 -0
- package/dist/projects/graph-paths.d.ts.map +1 -0
- package/dist/projects/graph-paths.js +175 -0
- package/dist/projects/graph-paths.js.map +1 -0
- package/dist/projects/graph.d.ts +191 -0
- package/dist/projects/graph.d.ts.map +1 -0
- package/dist/projects/graph.js +829 -0
- package/dist/projects/graph.js.map +1 -0
- package/dist/projects/routes.d.ts +17 -0
- package/dist/projects/routes.d.ts.map +1 -1
- package/dist/projects/routes.js +139 -0
- package/dist/projects/routes.js.map +1 -1
- package/dist/qe/ledger.d.ts +3 -2
- package/dist/qe/ledger.d.ts.map +1 -1
- package/dist/qe/ledger.js +5 -4
- package/dist/qe/ledger.js.map +1 -1
- package/dist/studio/assets/index-D-BFUYnY.js +530 -0
- package/dist/studio/assets/index-D6S9zUtO.css +32 -0
- package/dist/studio/index.html +5 -3
- package/endpoint-manifest.json +624 -0
- package/package.json +8 -5
- package/dist/studio/assets/index-CCwXa1cn.js +0 -428
- package/dist/studio/assets/index-HWxo0h41.css +0 -32
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE DELIVERABLE FLOOR (crew#311) — "done" is re-derived from the artifact, never asserted.
|
|
3
|
+
*
|
|
4
|
+
* ## The defect
|
|
5
|
+
*
|
|
6
|
+
* A governed unit that produced NOTHING still passed its execution gate on the strength of a
|
|
7
|
+
* prose reply. The engine's only universal floor is the SUBSTANCE floor
|
|
8
|
+
* (wicked-core `actor.rs`, "phase produced no reviewable substance"): a governed
|
|
9
|
+
* Creator/Neutral unit is rejected only when its output carries BOTH under 200 trimmed chars
|
|
10
|
+
* of prose AND no worktree change. So ~200 characters of narration — "I'll read the design
|
|
11
|
+
* spec, let me check the API types…" — clears it, with no file anywhere. Prose length stood in
|
|
12
|
+
* for evidence.
|
|
13
|
+
*
|
|
14
|
+
* The engine has two real evidence instruments, and neither reaches a run of this shape:
|
|
15
|
+
*
|
|
16
|
+
* 1. `validator_pin` → the built-in EVIDENCE floor (`builtin_floors::EVIDENCE_FLOOR_PIN`,
|
|
17
|
+
* criterion "the run left a change in its worktree (done is re-derived from the diff, never
|
|
18
|
+
* asserted)"). It re-verifies a git DIFF, and is fail-closed on a run with no worktree — so
|
|
19
|
+
* an UNBOUND run (`repoRef` omitted, `workdir: null`) can never satisfy it. Every crew
|
|
20
|
+
* interactive seam launches unbound on purpose (wicked-core#293/#294), and their deliverable
|
|
21
|
+
* is a FILE in a declared write root, invisible to a diff.
|
|
22
|
+
* 2. `required_deliverables` → enforced by wicked-core as of FINDING-101, but only in the
|
|
23
|
+
* WRAPPED-CLI runner (`execute_wrapped.rs`), only against the unit's own cwd, and any
|
|
24
|
+
* absolute or `..`-escaping path is counted MISSING by construction. The ACP runner — the
|
|
25
|
+
* path a real seat takes — never consults it. So for a run whose deliverable is an absolute
|
|
26
|
+
* path under `extraWriteRoots`, declaring it there would fail every run on one runner and
|
|
27
|
+
* check nothing on the other. That gap is wicked-core's to close
|
|
28
|
+
* (wicked-core#297, items 1 and 3); once it is, this phase can retire in favour of the
|
|
29
|
+
* engine field. Crew must not paper over it meanwhile by declaring something unverifiable.
|
|
30
|
+
*
|
|
31
|
+
* ## What this module does
|
|
32
|
+
*
|
|
33
|
+
* It gives crew the instrument it can honestly own: a deterministic TOOL phase, appended to a
|
|
34
|
+
* PER-RUN copy of the workflow def (the exact mechanism `core/deliver.ts` proved for
|
|
35
|
+
* `deliver: "pr"` — the shared def is never mutated, nothing lands in the overlay dir, the
|
|
36
|
+
* catalog never grows a per-run entry), which asserts that the artifacts the launcher declared
|
|
37
|
+
* EXIST, are non-empty, and were written by THIS run (crew#320 — existence alone re-derives
|
|
38
|
+
* nothing at a path a prior run already populated). A Tool phase's failure surface is its exit
|
|
39
|
+
* code: wicked-core's
|
|
40
|
+
* `run_tool_cmd` maps a non-zero exit to `StepStatus::Failed`, which fails the unit and the run
|
|
41
|
+
* through the unchanged completion path. No LLM, no worktree, no judgement about the content —
|
|
42
|
+
* only "the phase promised this file; is it there".
|
|
43
|
+
*
|
|
44
|
+
* That is deliberately a FLOOR and not a review, in exactly the sense `builtin_floors` documents:
|
|
45
|
+
* it proves an artifact exists. It says nothing about whether the artifact is correct, or even
|
|
46
|
+
* related to the task. What it removes is the failure mode crew#311 recorded — a unit whose
|
|
47
|
+
* `Write` was policy-denied, which therefore produced no file at all, reporting done.
|
|
48
|
+
*
|
|
49
|
+
* ## The report
|
|
50
|
+
*
|
|
51
|
+
* The script prints what was EXPECTED and what was FOUND before it exits, on both branches, so
|
|
52
|
+
* the failure is legible from the unit output alone: which paths were declared, which exist
|
|
53
|
+
* (with their size), and which are missing and why (absent / empty file / empty directory).
|
|
54
|
+
* An operator reading a failed run must never have to guess what the phase was supposed to
|
|
55
|
+
* produce.
|
|
56
|
+
*/
|
|
57
|
+
/** The id of the appended floor phase — also the collision probe when a def already carries one. */
|
|
58
|
+
export const DELIVERABLE_FLOOR_PHASE_ID = 'verify-deliverables';
|
|
59
|
+
/** The marker line the failing branch prints. Asserted by tests and greppable in a unit output. */
|
|
60
|
+
export const DELIVERABLE_FLOOR_FAILURE_MARKER = '[wicked-crew] DELIVERABLE FLOOR FAILED';
|
|
61
|
+
/**
|
|
62
|
+
* Slack allowed on the freshness comparison, in milliseconds (crew#320).
|
|
63
|
+
*
|
|
64
|
+
* The launch timestamp comes from the daemon's `Date.now()` (millisecond resolution); the
|
|
65
|
+
* artifact timestamp comes from the FILESYSTEM, and not every filesystem keeps milliseconds —
|
|
66
|
+
* HFS+ and ext3 truncate mtime to whole seconds, and some SMB/NFS mounts are coarser still. A
|
|
67
|
+
* file genuinely written 400 ms after a launch at `…:07.600Z` can therefore report an mtime of
|
|
68
|
+
* `…:07.000Z` and read as older than the launch that produced it.
|
|
69
|
+
*
|
|
70
|
+
* One second absorbs that truncation without weakening the check this exists to make: the stale
|
|
71
|
+
* artifact crew#320 describes is the PREVIOUS run's output, which predates this run's launch by
|
|
72
|
+
* that run's whole remaining lifetime plus the turnaround before the next one — orders of
|
|
73
|
+
* magnitude more than a timestamp-granularity rounding error.
|
|
74
|
+
*/
|
|
75
|
+
export const DELIVERABLE_FLOOR_MTIME_SLACK_MS = 1_000;
|
|
76
|
+
/**
|
|
77
|
+
* The floor program, run as `node -e <script> <launchedAtMs> <path...>`.
|
|
78
|
+
*
|
|
79
|
+
* Node rather than a shell: this must work identically on macOS, Linux, and Windows, and the
|
|
80
|
+
* daemon already IS node, so `process.execPath` is a guaranteed-present absolute interpreter
|
|
81
|
+
* (see {@link deliverableFloorPhase}). `-e` runs CommonJS, so `require` is available; the launch
|
|
82
|
+
* timestamp and the declared paths arrive as `process.argv.slice(1)`, passed as argv rather than
|
|
83
|
+
* interpolated into a shell string so a path containing a space, a quote, or a `$` cannot be
|
|
84
|
+
* re-parsed.
|
|
85
|
+
*
|
|
86
|
+
* A path counts as PRODUCED when it exists, carries bytes, and was written by THIS RUN:
|
|
87
|
+
*
|
|
88
|
+
* - a non-empty file whose `mtimeMs` is at or after the launch timestamp;
|
|
89
|
+
* - a directory holding at least one direct entry whose `mtimeMs` is at or after it (a phase may
|
|
90
|
+
* declare a directory of outputs; the directory's own mtime is not enough, because a directory
|
|
91
|
+
* left over from a prior run keeps its entries and its mtime).
|
|
92
|
+
*
|
|
93
|
+
* A zero-byte file is NOT evidence — it is the same "nothing was produced" this exists to catch,
|
|
94
|
+
* and the draft/chat seams already treated an empty deliverable as a failure at their own
|
|
95
|
+
* post-hoc check.
|
|
96
|
+
*
|
|
97
|
+
* ## Why the timestamp (crew#320)
|
|
98
|
+
*
|
|
99
|
+
* Existence alone re-derives nothing when the path can be pre-populated. The interactive seams
|
|
100
|
+
* copy their deliverable to `outPath` and never remove it, and demo/draft/edit run directories
|
|
101
|
+
* are keyed by DOCUMENT ID (edit by `doc:version`) — so a second run over the same key starts
|
|
102
|
+
* with the previous run's file already sitting at the declared path. The floor would find it,
|
|
103
|
+
* find bytes, and pass: the artifact exists, but this run did not produce it. "Done" would be
|
|
104
|
+
* asserted by a leftover rather than derived from this run's work — the exact failure shape one
|
|
105
|
+
* level in from the one crew#311 closed.
|
|
106
|
+
*
|
|
107
|
+
* The comparison is the stronger of the two available fixes. Clearing `outPath` at launch would
|
|
108
|
+
* also work, but only for as long as every seam remembers to clear it; the mtime check survives
|
|
109
|
+
* a seam that forgets, and it survives a seam that has not been written yet.
|
|
110
|
+
*
|
|
111
|
+
* A launch timestamp that does not parse FAILS CLOSED. An armed floor that cannot judge freshness
|
|
112
|
+
* is not a floor, and passing on the grounds that the check itself is broken is precisely the
|
|
113
|
+
* "assert done" move this module exists to refuse.
|
|
114
|
+
*/
|
|
115
|
+
export function deliverableFloorScript() {
|
|
116
|
+
return [
|
|
117
|
+
'const fs=require("node:fs");',
|
|
118
|
+
'const pathmod=require("node:path");',
|
|
119
|
+
'const launch=Number(process.argv[1]);',
|
|
120
|
+
'const want=process.argv.slice(2);',
|
|
121
|
+
'const iso=(t)=>{try{return new Date(t).toISOString()}catch(e){return String(t)}};',
|
|
122
|
+
'if(!Number.isFinite(launch)){',
|
|
123
|
+
`console.log(${JSON.stringify(DELIVERABLE_FLOOR_FAILURE_MARKER)}+" — the floor was armed without a parseable launch timestamp ("+JSON.stringify(process.argv[1])+"), so it cannot tell this run's artifacts from a prior run's leftovers. Failing closed (crew#320).");`,
|
|
124
|
+
'process.exit(1)}',
|
|
125
|
+
`const floorAt=launch-${DELIVERABLE_FLOOR_MTIME_SLACK_MS};`,
|
|
126
|
+
'const fresh=(t)=>t>=floorAt;',
|
|
127
|
+
'const found=[],missing=[];',
|
|
128
|
+
'for(const p of want){',
|
|
129
|
+
'let s=null;',
|
|
130
|
+
'try{s=fs.statSync(p)}catch(e){s=null}',
|
|
131
|
+
'if(s===null){missing.push(p+" (does not exist)");continue}',
|
|
132
|
+
'if(s.isDirectory()){',
|
|
133
|
+
'let names=[];try{names=fs.readdirSync(p)}catch(e){names=[]}',
|
|
134
|
+
'if(names.length===0){missing.push(p+" (directory is empty)");continue}',
|
|
135
|
+
'let n=0;',
|
|
136
|
+
'for(const e of names){let es=null;try{es=fs.statSync(pathmod.join(p,e))}catch(err){es=null}if(es!==null&&fresh(es.mtimeMs)){n++}}',
|
|
137
|
+
'if(n===0){missing.push(p+" (directory, "+names.length+" entries, but NONE written by this run — every entry predates the launch at "+iso(launch)+", so they are a PRIOR run\'s output)")}',
|
|
138
|
+
'else{found.push(p+" (directory, "+names.length+" entries, "+n+" written by this run)")}',
|
|
139
|
+
'continue}',
|
|
140
|
+
'if(s.size===0){missing.push(p+" (file is empty, 0 bytes)");continue}',
|
|
141
|
+
'if(!fresh(s.mtimeMs)){missing.push(p+" (STALE: last modified "+iso(s.mtimeMs)+", before this run launched at "+iso(launch)+" — it is a PRIOR run\'s artifact, not this run\'s)");continue}',
|
|
142
|
+
'found.push(p+" ("+s.size+" bytes)");',
|
|
143
|
+
'}',
|
|
144
|
+
'console.log("[wicked-crew] deliverable floor: this phase declared "+want.length+" artifact(s); this run launched at "+iso(launch)+".");',
|
|
145
|
+
'console.log("[wicked-crew] EXPECTED: "+(want.length?want.join(", "):"(none)"));',
|
|
146
|
+
'console.log("[wicked-crew] FOUND: "+(found.length?found.join(", "):"(nothing)"));',
|
|
147
|
+
'if(missing.length===0){',
|
|
148
|
+
'console.log("[wicked-crew] every declared deliverable exists, carries bytes, and was written by THIS run — done is re-derived from the artifact, not asserted.");',
|
|
149
|
+
'process.exit(0)}',
|
|
150
|
+
'console.log("[wicked-crew] MISSING: "+missing.join(", "));',
|
|
151
|
+
`console.log(${JSON.stringify(DELIVERABLE_FLOOR_FAILURE_MARKER)}+" — the run reported done without producing the artifact(s) it was launched to produce. A prose reply is not a deliverable (crew#311), and a prior run's leftover file is not this run's evidence (crew#320).");`,
|
|
152
|
+
'process.exit(1);',
|
|
153
|
+
].join('');
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The floor PhaseDef for `paths`, judged against `launchedAtMs`.
|
|
157
|
+
*
|
|
158
|
+
* `launchedAtMs` is positional and required rather than defaulted (crew#320): a default would let
|
|
159
|
+
* a new caller arm a floor that checks existence only, and the resulting run would report done on
|
|
160
|
+
* a leftover with nothing in its output saying the freshness half was never applied. The compiler
|
|
161
|
+
* asking the question is the point.
|
|
162
|
+
*
|
|
163
|
+
* `cmd[0]` is `process.execPath` — the ABSOLUTE path of the node binary already running the
|
|
164
|
+
* daemon — not the bare name `node`. wicked-core preflights every Tool phase at launch
|
|
165
|
+
* (`workflow::preflight_tool_phases`) and REFUSES to start a run whose tool binary does not
|
|
166
|
+
* resolve; a bare `node` resolves only if the engine's PATH happens to carry one, which is
|
|
167
|
+
* exactly the kind of environment-dependent gate that fails in production and passes in tests.
|
|
168
|
+
* The composed def is per-run and hot-registered only, so baking a machine-local path into it
|
|
169
|
+
* costs nothing: it never reaches the catalog or the overlay dir.
|
|
170
|
+
*
|
|
171
|
+
* `role: 'neutral'` + `executes_code: false` + `governed`-free Tool dispatch: this phase is
|
|
172
|
+
* deterministic tooling, so the substance floor, the agent judge, and the input-governance fold
|
|
173
|
+
* do not apply to it — its verdict IS its exit code.
|
|
174
|
+
*/
|
|
175
|
+
export function deliverableFloorPhase(paths, launchedAtMs, dependsOn = []) {
|
|
176
|
+
return {
|
|
177
|
+
id: DELIVERABLE_FLOOR_PHASE_ID,
|
|
178
|
+
kind: 'test',
|
|
179
|
+
executor: {
|
|
180
|
+
type: 'tool',
|
|
181
|
+
// The timestamp rides ahead of the paths as its own argv slot: a path is arbitrary user
|
|
182
|
+
// text and could itself look like a number, so position — not parsing — separates them.
|
|
183
|
+
cmd: [process.execPath, '-e', deliverableFloorScript(), String(launchedAtMs), ...paths],
|
|
184
|
+
},
|
|
185
|
+
gate_type: 'execution',
|
|
186
|
+
gate: 'auto',
|
|
187
|
+
executes_code: false,
|
|
188
|
+
// NOT `verified_evidence: true`: wicked-core arms any `verified_evidence` phase that names no
|
|
189
|
+
// pin of its own with the worktree-DIFF floor (FINDING-055), which is fail-closed on the
|
|
190
|
+
// repo-less runs this phase exists to serve — it would deny every one of them for the wrong
|
|
191
|
+
// reason. This phase carries its own re-verification in its exit code.
|
|
192
|
+
verified_evidence: false,
|
|
193
|
+
// NOT the declared paths either: core checks `required_deliverables` relative to the unit's
|
|
194
|
+
// cwd and counts every absolute path as missing (see the module doc; wicked-core#297).
|
|
195
|
+
// Declaring them here would fail the run on the wrapped-CLI path for the wrong reason.
|
|
196
|
+
required_deliverables: [],
|
|
197
|
+
depends_on: dependsOn,
|
|
198
|
+
role: 'neutral',
|
|
199
|
+
skill_ref: null,
|
|
200
|
+
allowed_skills: [],
|
|
201
|
+
validator_pin: null,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Compose a PER-RUN def: `base`'s phases (untouched — the shared def is never mutated) plus the
|
|
206
|
+
* deliverable floor appended last, under a run-scoped id. Mirrors `composeDeliverWorkflow`'s
|
|
207
|
+
* contract exactly, including the id charset/length rules `registerWorkflow` enforces and the
|
|
208
|
+
* deliberate absence of `is_system` (core's register schema rejects unknown fields on this path,
|
|
209
|
+
* and a composed def is engine input, not catalog data).
|
|
210
|
+
*
|
|
211
|
+
* Throws on an empty `paths` list — a floor over nothing would pass vacuously, which is the
|
|
212
|
+
* defect, not the fix. Throws on a def that already carries the floor phase, for the same
|
|
213
|
+
* ambiguity reason `composeDeliverWorkflow` does.
|
|
214
|
+
*
|
|
215
|
+
* `launchedAtMs` defaults to composition time, which is the correct instant on every path that
|
|
216
|
+
* exists: composition is the last thing that happens before `core.launchRun`, so nothing this run
|
|
217
|
+
* produces can predate it, and a nonsense value is rejected here rather than being discovered by
|
|
218
|
+
* the tool phase minutes later (crew#320).
|
|
219
|
+
*/
|
|
220
|
+
export function composeDeliverableFloor(base, runId, paths, launchedAtMs = Date.now()) {
|
|
221
|
+
if (paths.length === 0) {
|
|
222
|
+
throw new Error('requireDeliverables: at least one path must be declared — a floor over nothing passes vacuously');
|
|
223
|
+
}
|
|
224
|
+
for (const p of paths) {
|
|
225
|
+
if (p.trim().length === 0) {
|
|
226
|
+
throw new Error('requireDeliverables: a declared deliverable path must not be blank');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (!Number.isFinite(launchedAtMs) || launchedAtMs <= 0) {
|
|
230
|
+
throw new Error(`requireDeliverables: launchedAtMs must be a positive epoch-millisecond timestamp, got ` +
|
|
231
|
+
`${String(launchedAtMs)} — the floor compares it against each artifact's mtime to prove ` +
|
|
232
|
+
`THIS run produced them`);
|
|
233
|
+
}
|
|
234
|
+
if (base.phases.some((p) => p.id === DELIVERABLE_FLOOR_PHASE_ID)) {
|
|
235
|
+
throw new Error(`workflow '${base.id}' already has a '${DELIVERABLE_FLOOR_PHASE_ID}' phase — launch it without requireDeliverables`);
|
|
236
|
+
}
|
|
237
|
+
const last = base.phases[base.phases.length - 1];
|
|
238
|
+
const safeRunId = runId.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
239
|
+
const composedId = `${base.id}-verified-${safeRunId}`.slice(0, 128);
|
|
240
|
+
return {
|
|
241
|
+
id: composedId,
|
|
242
|
+
phases: [
|
|
243
|
+
...base.phases,
|
|
244
|
+
deliverableFloorPhase(paths, launchedAtMs, last !== undefined ? [last.id] : []),
|
|
245
|
+
],
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
//# sourceMappingURL=deliverable-floor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deliverable-floor.js","sourceRoot":"","sources":["../../src/core/deliverable-floor.ts"],"names":[],"mappings":"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"}
|
package/dist/core/exec.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ type ExecOpts = {
|
|
|
18
18
|
timeout?: number;
|
|
19
19
|
cwd?: string;
|
|
20
20
|
env?: NodeJS.ProcessEnv;
|
|
21
|
+
/** Suppress the console window a child would open on Windows (git, etc.). */
|
|
22
|
+
windowsHide?: boolean;
|
|
21
23
|
/** `number | undefined` on purpose: `exactOptionalPropertyTypes` would otherwise reject the very
|
|
22
24
|
* shape this module defends against, making the guard below untypeable and untestable. */
|
|
23
25
|
maxBuffer?: number | undefined;
|
package/dist/core/exec.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../src/core/exec.ts"],"names":[],"mappings":"AA+BA;;sFAEsF;AACtF,eAAO,MAAM,qBAAqB,QAAmB,CAAC;AAUtD;;;;oDAIoD;AACpD,qBAAa,kBAAmB,SAAQ,KAAK;IAEzC,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,UAAU,EAAE,MAAM;gBADlB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM;CAS9B;AAED,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAMtD;AAED,KAAK,QAAQ,GAAG;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB;+FAC2F;IAC3F,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC,CAAC;AAEF;;kFAEkF;AAClF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAEvD;AAED;;kEAEkE;AAClE,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,GAAE,QAAa,GAClB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAoB7C"}
|
|
1
|
+
{"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../src/core/exec.ts"],"names":[],"mappings":"AA+BA;;sFAEsF;AACtF,eAAO,MAAM,qBAAqB,QAAmB,CAAC;AAUtD;;;;oDAIoD;AACpD,qBAAa,kBAAmB,SAAQ,KAAK;IAEzC,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,UAAU,EAAE,MAAM;gBADlB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM;CAS9B;AAED,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAMtD;AAED,KAAK,QAAQ,GAAG;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;+FAC2F;IAC3F,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC,CAAC;AAEF;;kFAEkF;AAClF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAEvD;AAED;;kEAEkE;AAClE,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,GAAE,QAAa,GAClB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAoB7C"}
|
package/dist/core/exec.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/core/exec.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,EAAE;AACF,eAAe;AACf,EAAE;AACF,oGAAoG;AACpG,iGAAiG;AACjG,gGAAgG;AAChG,2FAA2F;AAC3F,EAAE;AACF,kGAAkG;AAClG,kGAAkG;AAClG,qCAAqC;AACrC,EAAE;AACF,wDAAwD;AACxD,EAAE;AACF,gGAAgG;AAChG,8FAA8F;AAC9F,mGAAmG;AACnG,0EAA0E;AAC1E,EAAE;AACF,0BAA0B;AAC1B,EAAE;AACF,8FAA8F;AAC9F,6FAA6F;AAC7F,0BAA0B;AAE1B,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAE1C;;sFAEsF;AACtF,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAEtD;kFACkF;AAClF,SAAS,UAAU,CAAC,CAAS;IAC3B,IAAI,CAAC,IAAI,IAAI,GAAG,IAAI;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,MAAM,CAAC;IACpE,IAAI,CAAC,IAAI,IAAI;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;IACpD,OAAO,GAAG,CAAC,QAAQ,CAAC;AACtB,CAAC;AAED;;;;oDAIoD;AACpD,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAEhC;IACA;IAFX,YACW,IAAY,EACZ,UAAkB;QAE3B,KAAK,CACH,GAAG,IAAI,uBAAuB,UAAU,CAAC,UAAU,CAAC,wCAAwC;YAC1F,0CAA0C,IAAI,8CAA8C;YAC5F,wDAAwD,CAC3D,CAAC;QAPO,SAAI,GAAJ,IAAI,CAAQ;QACZ,eAAU,GAAV,UAAU,CAAQ;QAO3B,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED,iEAAiE;AACjE,MAAM,UAAU,gBAAgB,CAAC,GAAY;IAC3C,OAAO,CACL,OAAO,GAAG,KAAK,QAAQ;QACvB,GAAG,KAAK,IAAI;QACX,GAA0B,CAAC,IAAI,KAAK,mCAAmC,CACzE,CAAC;AACJ,CAAC;
|
|
1
|
+
{"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/core/exec.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,EAAE;AACF,eAAe;AACf,EAAE;AACF,oGAAoG;AACpG,iGAAiG;AACjG,gGAAgG;AAChG,2FAA2F;AAC3F,EAAE;AACF,kGAAkG;AAClG,kGAAkG;AAClG,qCAAqC;AACrC,EAAE;AACF,wDAAwD;AACxD,EAAE;AACF,gGAAgG;AAChG,8FAA8F;AAC9F,mGAAmG;AACnG,0EAA0E;AAC1E,EAAE;AACF,0BAA0B;AAC1B,EAAE;AACF,8FAA8F;AAC9F,6FAA6F;AAC7F,0BAA0B;AAE1B,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAE1C;;sFAEsF;AACtF,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAEtD;kFACkF;AAClF,SAAS,UAAU,CAAC,CAAS;IAC3B,IAAI,CAAC,IAAI,IAAI,GAAG,IAAI;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,MAAM,CAAC;IACpE,IAAI,CAAC,IAAI,IAAI;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;IACpD,OAAO,GAAG,CAAC,QAAQ,CAAC;AACtB,CAAC;AAED;;;;oDAIoD;AACpD,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAEhC;IACA;IAFX,YACW,IAAY,EACZ,UAAkB;QAE3B,KAAK,CACH,GAAG,IAAI,uBAAuB,UAAU,CAAC,UAAU,CAAC,wCAAwC;YAC1F,0CAA0C,IAAI,8CAA8C;YAC5F,wDAAwD,CAC3D,CAAC;QAPO,SAAI,GAAJ,IAAI,CAAQ;QACZ,eAAU,GAAV,UAAU,CAAQ;QAO3B,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED,iEAAiE;AACjE,MAAM,UAAU,gBAAgB,CAAC,GAAY;IAC3C,OAAO,CACL,OAAO,GAAG,KAAK,QAAQ;QACvB,GAAG,KAAK,IAAI;QACX,GAA0B,CAAC,IAAI,KAAK,mCAAmC,CACzE,CAAC;AACJ,CAAC;AAaD;;kFAEkF;AAClF,MAAM,UAAU,gBAAgB,CAAC,IAAc;IAC7C,OAAO,IAAI,CAAC,SAAS,IAAI,qBAAqB,CAAC;AACjD,CAAC;AAED;;kEAEkE;AAClE,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAY,EACZ,IAAc,EACd,OAAiB,EAAE;IAEnB,gGAAgG;IAChG,6FAA6F;IAC7F,2FAA2F;IAC3F,iCAAiC;IACjC,EAAE;IACF,sFAAsF;IACtF,6EAA6E;IAC7E,8DAA8D;IAC9D,EAAE;IACF,8FAA8F;IAC9F,uFAAuF;IACvF,4CAA4C;IAC5C,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACzC,IAAI,CAAC;QACH,OAAO,MAAM,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;IACjE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,gBAAgB,CAAC,GAAG,CAAC;YAAE,MAAM,IAAI,kBAAkB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QACzE,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -55,6 +55,84 @@ export interface LaunchRunInput {
|
|
|
55
55
|
* fails the launch with no session persisted. Omit for runs that deliver inside their workdir.
|
|
56
56
|
*/
|
|
57
57
|
extraWriteRoots?: string[];
|
|
58
|
+
/**
|
|
59
|
+
* The PROJECT's co-located code graph this run's governed workers should query instead of the
|
|
60
|
+
* run repo's own — resolved by `projects/graph.ts::resolveProjectGraphBinding`, which is the ONE
|
|
61
|
+
* thing that knows where a project graph lives and what each repo is labelled inside it.
|
|
62
|
+
*
|
|
63
|
+
* `repoLabel` is optional in the TYPE because a repo-LESS run legitimately has none — there is
|
|
64
|
+
* no own repo to name. It is not optional in MEANING for a repo-bound run: the engine uses it to
|
|
65
|
+
* confirm the co-located graph actually holds this run's repo.
|
|
66
|
+
*
|
|
67
|
+
* TypeScript cannot catch a missing label here — the invariant ties this field to `repoRef`, a
|
|
68
|
+
* sibling field — so `CoreAdapter.launchRun` enforces it at RUNTIME. Two failure modes, and they
|
|
69
|
+
* are NOT the same thing:
|
|
70
|
+
*
|
|
71
|
+
* - **crew refuses the launch outright.** Passing `projectGraph` without `projectId`, or with
|
|
72
|
+
* `repoRef` set and no `repoLabel`, THROWS: no session is created and nothing runs. These are
|
|
73
|
+
* caller bugs with no sensible degraded reading, so they fail before anything is persisted.
|
|
74
|
+
* - **the ENGINE declines the binding.** A well-formed binding whose graph turns out not to hold
|
|
75
|
+
* the named label is refused engine-side and the run proceeds against its own repo graph — a
|
|
76
|
+
* real launch, quietly narrower. That is the fallback; it is not what a missing `repoLabel`
|
|
77
|
+
* gets you.
|
|
78
|
+
*
|
|
79
|
+
* Omit the whole field for the per-repo behaviour.
|
|
80
|
+
*/
|
|
81
|
+
projectGraph?: {
|
|
82
|
+
dbPath: string;
|
|
83
|
+
repoLabel?: string;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Delivery mode (crew#293). `"pr"` ⇒ the adapter appends the hardened deliver Tool phase
|
|
87
|
+
* (push the run branch + `gh pr create`, see `core/deliver.ts`) to a PER-RUN copy of the
|
|
88
|
+
* selected workflow def and launches that copy — the shared def is never mutated. Requires
|
|
89
|
+
* `workflow`; a free-text run has no def to append to. Omit ⇒ no delivery phase (default).
|
|
90
|
+
*/
|
|
91
|
+
deliver?: 'pr';
|
|
92
|
+
/**
|
|
93
|
+
* THE DELIVERABLE FLOOR (crew#311) — absolute paths this run MUST have produced by the time
|
|
94
|
+
* it finishes. When non-empty (and `workflow` is set), the adapter appends a deterministic
|
|
95
|
+
* verification Tool phase to a PER-RUN copy of the def (see `core/deliverable-floor.ts`)
|
|
96
|
+
* which re-derives "done" from the artifacts: every declared path must exist and carry bytes,
|
|
97
|
+
* or the phase exits non-zero and the RUN FAILS, naming what was expected and what was found.
|
|
98
|
+
*
|
|
99
|
+
* This is the instrument for runs whose deliverable is a FILE in a declared write root rather
|
|
100
|
+
* than a worktree diff — every crew interactive seam. The engine's own two floors do not
|
|
101
|
+
* reach them: the built-in evidence floor re-verifies a git diff and is fail-closed on a
|
|
102
|
+
* repo-less run, and `required_deliverables` is checked only by the wrapped-CLI runner and
|
|
103
|
+
* only against the unit's cwd (an absolute path counts as missing there by construction).
|
|
104
|
+
*
|
|
105
|
+
* Omit for runs whose evidence is the worktree diff — those are the engine's to floor.
|
|
106
|
+
*/
|
|
107
|
+
requireDeliverables?: string[];
|
|
108
|
+
}
|
|
109
|
+
/** Default `workerStallMinutes` (crew#287): silent minutes before the stall watchdog fires. */
|
|
110
|
+
export declare const DEFAULT_WORKER_STALL_MINUTES = 15;
|
|
111
|
+
/**
|
|
112
|
+
* LOCAL extension of the published `SystemSettings` (wicked-crew-api-types 0.6.0): the crew#287
|
|
113
|
+
* stall-watchdog knob is daemon-owned until the contract picks it up. NOTE for the next
|
|
114
|
+
* api-types release: fold `workerStallMinutes` into `SystemSettings` (and the synthetic
|
|
115
|
+
* `workerStalled` /ws frame — see `api/stall-watchdog.ts` `WorkerStalledFrame` — into the
|
|
116
|
+
* event documentation). Extra fields are forward-additive on the wire (DES-STUDIO-001 §5.1),
|
|
117
|
+
* so shipping it daemon-side first breaks no consumer.
|
|
118
|
+
*/
|
|
119
|
+
export interface CrewSystemSettings extends SystemSettings {
|
|
120
|
+
/**
|
|
121
|
+
* Minutes a run in `executing` may go without ANY engine event on the daemon's relay before
|
|
122
|
+
* a synthetic `workerStalled` frame is broadcast on /ws (detection only; default 15).
|
|
123
|
+
*/
|
|
124
|
+
workerStallMinutes?: number;
|
|
125
|
+
/**
|
|
126
|
+
* Skin-owned preference blobs, round-tripped verbatim (crew#323). RESTATED here rather than
|
|
127
|
+
* merely inherited from `SystemSettings` so the daemon's own type SAYS the settings store is
|
|
128
|
+
* shared with the experience plane — the fact that used to live only in a comment in the
|
|
129
|
+
* studio's client, which is how `studio.appearance` / `studio.notifications` could be
|
|
130
|
+
* silently dropped by `PUT /settings` without either side's types objecting.
|
|
131
|
+
*
|
|
132
|
+
* The daemon never interprets a value here; it enforces only "JSON-serializable, under the
|
|
133
|
+
* per-key byte cap" at the PUT boundary (`api/routes.ts` `STUDIO_SETTINGS_MAX_BYTES`).
|
|
134
|
+
*/
|
|
135
|
+
[key: `studio.${string}`]: unknown;
|
|
58
136
|
}
|
|
59
|
-
export declare const DEFAULT_SETTINGS:
|
|
137
|
+
export declare const DEFAULT_SETTINGS: CrewSystemSettings;
|
|
60
138
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/core/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,mBAAmB,uBAAuB,CAAC;AAE3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,2EAA2E;AAC3E,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,8FAA8F;AAC9F,MAAM,WAAW,cAAc;IAC7B,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;IAChB,qBAAqB;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,mBAAmB,uBAAuB,CAAC;AAE3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,2EAA2E;AAC3E,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,8FAA8F;AAC9F,MAAM,WAAW,cAAc;IAC7B,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;IAChB,qBAAqB;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,CAAC;IACjB,uCAAuC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,YAAY,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACtD;;;;;OAKG;IACH,OAAO,CAAC,EAAE,IAAI,CAAC;IACf;;;;;;;;;;;;;;OAcG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC;AAED,+FAA+F;AAC/F,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAE/C;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAmB,SAAQ,cAAc;IACxD;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;;;OASG;IACH,CAAC,GAAG,EAAE,UAAU,MAAM,EAAE,GAAG,OAAO,CAAC;CACpC;AAED,eAAO,MAAM,gBAAgB,EAAE,kBAG9B,CAAC"}
|
package/dist/core/types.js
CHANGED
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
* daemon speaks these daemon-owned shapes, which is what quarantines the
|
|
19
19
|
* FINALIZING core-ts `subscribe` seam to a single file (DES-STUDIO-001 §5.2).
|
|
20
20
|
*/
|
|
21
|
+
/** Default `workerStallMinutes` (crew#287): silent minutes before the stall watchdog fires. */
|
|
22
|
+
export const DEFAULT_WORKER_STALL_MINUTES = 15;
|
|
21
23
|
export const DEFAULT_SETTINGS = {
|
|
22
24
|
graphNodeLimit: 150,
|
|
25
|
+
workerStallMinutes: DEFAULT_WORKER_STALL_MINUTES,
|
|
23
26
|
};
|
|
24
27
|
//# sourceMappingURL=types.js.map
|
package/dist/core/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AA2FH,+FAA+F;AAC/F,MAAM,CAAC,MAAM,4BAA4B,GAAG,EAAE,CAAC;AA6B/C,MAAM,CAAC,MAAM,gBAAgB,GAAuB;IAClD,cAAc,EAAE,GAAG;IACnB,kBAAkB,EAAE,4BAA4B;CACjD,CAAC"}
|
|
@@ -19,9 +19,39 @@
|
|
|
19
19
|
* live pid earns three 1.5 s attempts before the bridge is declared dead. Identity is checked
|
|
20
20
|
* too: `/api/health` must report THIS root, or a recycled port belonging to some other service
|
|
21
21
|
* would be proxied as if it were ours.
|
|
22
|
+
*
|
|
23
|
+
* On start/adopt the pool also records the daemon's own origin with the bridge (crew#298,
|
|
24
|
+
* `POST /api/studio-origin`, interactive ≥ 0.8.0) so the bridge's `GET /` redirects a direct
|
|
25
|
+
* visitor into studio instead of its API-only fallback page. Fire-and-forget, once per pooled
|
|
26
|
+
* bridge: recording can never fail — or slow down — a proxied request.
|
|
22
27
|
*/
|
|
23
28
|
import { type ChildProcess } from 'node:child_process';
|
|
29
|
+
import type { AddressInfo } from 'node:net';
|
|
24
30
|
export declare const LOCK_NAME = ".wi-serve.json";
|
|
31
|
+
/**
|
|
32
|
+
* The wicked-interactive range crew will start, as an npm spec.
|
|
33
|
+
*
|
|
34
|
+
* The spawn used to be a bare `npx --yes wicked-interactive`, which resolves whatever the public
|
|
35
|
+
* registry calls `latest` AT RUNTIME — no lockfile entry, no integrity hash, no floor. Three things
|
|
36
|
+
* follow from that, and none of them are theoretical:
|
|
37
|
+
*
|
|
38
|
+
* - a breaking wicked-interactive release changes crew's behaviour with no crew change, no review,
|
|
39
|
+
* and nothing in crew's history to point at during the incident;
|
|
40
|
+
* - crew has no way to REQUIRE a route it depends on. The learned-theme readback
|
|
41
|
+
* (`GET /d/:docId/api/theme/learned`, interactive#181) shipped in 0.8.1; against 0.8.0 the
|
|
42
|
+
* brand-learn surface polls forever and degrades silently. A floor turns that into a resolvable
|
|
43
|
+
* version, not a mystery;
|
|
44
|
+
* - the resolution is a network fetch from a public registry into a long-lived daemon.
|
|
45
|
+
*
|
|
46
|
+
* A caret floor is the smallest fix that closes the first two: it pins the MAJOR-compatible range so
|
|
47
|
+
* a 0.9.0 cannot arrive unannounced, and it states the minimum crew actually needs. It does NOT make
|
|
48
|
+
* the fetch reproducible — that needs a real dependency with a lockfile entry, which is the
|
|
49
|
+
* follow-up this comment exists to keep visible.
|
|
50
|
+
*
|
|
51
|
+
* Bump this when crew starts depending on a newer interactive route, and say which route in the
|
|
52
|
+
* commit.
|
|
53
|
+
*/
|
|
54
|
+
export declare const INTERACTIVE_SPEC = "wicked-interactive@^0.8.1";
|
|
25
55
|
/** ADR-0025: 1.5 s × 3 while the pid lives. */
|
|
26
56
|
export declare const HEALTH_TIMEOUT_MS = 1500;
|
|
27
57
|
export declare const HEALTH_ATTEMPTS = 3;
|
|
@@ -45,7 +75,21 @@ export interface BridgePoolIo {
|
|
|
45
75
|
startTimeoutMs?: number;
|
|
46
76
|
healthTimeoutMs?: number;
|
|
47
77
|
log?: (msg: string) => void;
|
|
78
|
+
/** Lower-severity channel for EXPECTED skips (a pre-0.8.0 bridge without the endpoint, #298). */
|
|
79
|
+
debug?: (msg: string) => void;
|
|
80
|
+
/**
|
|
81
|
+
* The daemon's own origin (`http://<bound host>:<bound port>`), resolved LAZILY — the pool is
|
|
82
|
+
* built before `listen`, but only consulted while serving a request, i.e. after the address is
|
|
83
|
+
* bound. Null (or absent) means "nothing to record" and the pool never POSTs (#298).
|
|
84
|
+
*/
|
|
85
|
+
studioOrigin?: () => string | null;
|
|
48
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* The daemon's own http origin from its bound server address, or null before `listen`. Wildcard
|
|
89
|
+
* binds (`0.0.0.0` / `::`) are not dialable from a browser, so they normalize to loopback — the
|
|
90
|
+
* bridge is local-only anyway, so whoever hits its port can reach the daemon at 127.0.0.1 too.
|
|
91
|
+
*/
|
|
92
|
+
export declare function boundOrigin(addr: AddressInfo | string | null): string | null;
|
|
49
93
|
/** `<root>/.wi-serve.json`, or null when absent/unparseable/incomplete. */
|
|
50
94
|
export declare function readLock(root: string): LiveBridge | null;
|
|
51
95
|
/** Signal 0 probes existence without delivering: EPERM means alive-but-not-ours. */
|
|
@@ -64,6 +108,14 @@ export declare class InteractiveBridgePool {
|
|
|
64
108
|
/** Live bridges, for tests and future operator introspection. */
|
|
65
109
|
keys(): string[];
|
|
66
110
|
private resolveOrStart;
|
|
111
|
+
/**
|
|
112
|
+
* POST the daemon's own origin to the bridge's `/api/studio-origin` (#298), so the bridge's
|
|
113
|
+
* `GET /` redirects a direct visitor into studio instead of the API-only fallback page.
|
|
114
|
+
* At most one attempt per pooled bridge per process; 404/405 means the bridge predates the
|
|
115
|
+
* endpoint (interactive < 0.8.0) and is an expected skip, any other failure is a warn — never
|
|
116
|
+
* an error, because origin recording must never fail a proxy request.
|
|
117
|
+
*/
|
|
118
|
+
private recordStudioOrigin;
|
|
67
119
|
/** The lockfile points at a bridge that is alive, answering, and serving THIS root. */
|
|
68
120
|
private healthy;
|
|
69
121
|
private start;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge-pool.d.ts","sourceRoot":"","sources":["../../src/interactive/bridge-pool.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"bridge-pool.d.ts","sourceRoot":"","sources":["../../src/interactive/bridge-pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE3E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAG5C,eAAO,MAAM,SAAS,mBAAmB,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,gBAAgB,8BAA8B,CAAC;AAC5D,+CAA+C;AAC/C,eAAO,MAAM,iBAAiB,OAAO,CAAC;AACtC,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,6FAA6F;AAC7F,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAEvC,yDAAyD;AACzD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,sFAAsF;AACtF,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,qFAAqF;IACrF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBACV,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAK1C;AAOD,gGAAgG;AAChG,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,YAAY,CAAC;IACvC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,iGAAiG;IACjG,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CACpC;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAS5E;AAED,2EAA2E;AAC3E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAQxD;AAED,oFAAoF;AACpF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQ7C;AAyBD,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAe;IAClC,oGAAoG;IACpG,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAmC;IACxD,6FAA6F;IAC7F,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA0C;gBAEvD,EAAE,GAAE,YAAiB;IAIjC,qGAAqG;IAC/F,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAc/C,uFAAuF;IACvF,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAI9B,iEAAiE;IACjE,IAAI,IAAI,MAAM,EAAE;YAIF,cAAc;IAW5B;;;;;;OAMG;IACH,OAAO,CAAC,kBAAkB;IA+B1B,uFAAuF;YACzE,OAAO;YAeP,KAAK;CAsCpB"}
|
|
@@ -19,11 +19,40 @@
|
|
|
19
19
|
* live pid earns three 1.5 s attempts before the bridge is declared dead. Identity is checked
|
|
20
20
|
* too: `/api/health` must report THIS root, or a recycled port belonging to some other service
|
|
21
21
|
* would be proxied as if it were ours.
|
|
22
|
+
*
|
|
23
|
+
* On start/adopt the pool also records the daemon's own origin with the bridge (crew#298,
|
|
24
|
+
* `POST /api/studio-origin`, interactive ≥ 0.8.0) so the bridge's `GET /` redirects a direct
|
|
25
|
+
* visitor into studio instead of its API-only fallback page. Fire-and-forget, once per pooled
|
|
26
|
+
* bridge: recording can never fail — or slow down — a proxied request.
|
|
22
27
|
*/
|
|
23
28
|
import { spawn as nodeSpawn } from 'node:child_process';
|
|
24
29
|
import { mkdirSync, readFileSync } from 'node:fs';
|
|
25
30
|
import { join, resolve } from 'node:path';
|
|
26
31
|
export const LOCK_NAME = '.wi-serve.json';
|
|
32
|
+
/**
|
|
33
|
+
* The wicked-interactive range crew will start, as an npm spec.
|
|
34
|
+
*
|
|
35
|
+
* The spawn used to be a bare `npx --yes wicked-interactive`, which resolves whatever the public
|
|
36
|
+
* registry calls `latest` AT RUNTIME — no lockfile entry, no integrity hash, no floor. Three things
|
|
37
|
+
* follow from that, and none of them are theoretical:
|
|
38
|
+
*
|
|
39
|
+
* - a breaking wicked-interactive release changes crew's behaviour with no crew change, no review,
|
|
40
|
+
* and nothing in crew's history to point at during the incident;
|
|
41
|
+
* - crew has no way to REQUIRE a route it depends on. The learned-theme readback
|
|
42
|
+
* (`GET /d/:docId/api/theme/learned`, interactive#181) shipped in 0.8.1; against 0.8.0 the
|
|
43
|
+
* brand-learn surface polls forever and degrades silently. A floor turns that into a resolvable
|
|
44
|
+
* version, not a mystery;
|
|
45
|
+
* - the resolution is a network fetch from a public registry into a long-lived daemon.
|
|
46
|
+
*
|
|
47
|
+
* A caret floor is the smallest fix that closes the first two: it pins the MAJOR-compatible range so
|
|
48
|
+
* a 0.9.0 cannot arrive unannounced, and it states the minimum crew actually needs. It does NOT make
|
|
49
|
+
* the fetch reproducible — that needs a real dependency with a lockfile entry, which is the
|
|
50
|
+
* follow-up this comment exists to keep visible.
|
|
51
|
+
*
|
|
52
|
+
* Bump this when crew starts depending on a newer interactive route, and say which route in the
|
|
53
|
+
* commit.
|
|
54
|
+
*/
|
|
55
|
+
export const INTERACTIVE_SPEC = 'wicked-interactive@^0.8.1';
|
|
27
56
|
/** ADR-0025: 1.5 s × 3 while the pid lives. */
|
|
28
57
|
export const HEALTH_TIMEOUT_MS = 1500;
|
|
29
58
|
export const HEALTH_ATTEMPTS = 3;
|
|
@@ -41,7 +70,22 @@ export class BridgeUnavailableError extends Error {
|
|
|
41
70
|
}
|
|
42
71
|
/** The one command that reproduces a failed start in a terminal, where its output is visible. */
|
|
43
72
|
function serveCommand(root) {
|
|
44
|
-
return `npx
|
|
73
|
+
return `npx ${INTERACTIVE_SPEC} serve --root ${root}`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The daemon's own http origin from its bound server address, or null before `listen`. Wildcard
|
|
77
|
+
* binds (`0.0.0.0` / `::`) are not dialable from a browser, so they normalize to loopback — the
|
|
78
|
+
* bridge is local-only anyway, so whoever hits its port can reach the daemon at 127.0.0.1 too.
|
|
79
|
+
*/
|
|
80
|
+
export function boundOrigin(addr) {
|
|
81
|
+
if (addr === null || typeof addr === 'string')
|
|
82
|
+
return null;
|
|
83
|
+
const host = addr.address === '' || addr.address === '0.0.0.0' || addr.address === '::'
|
|
84
|
+
? '127.0.0.1'
|
|
85
|
+
: addr.address.includes(':')
|
|
86
|
+
? `[${addr.address}]`
|
|
87
|
+
: addr.address;
|
|
88
|
+
return `http://${host}:${addr.port}`;
|
|
45
89
|
}
|
|
46
90
|
/** `<root>/.wi-serve.json`, or null when absent/unparseable/incomplete. */
|
|
47
91
|
export function readLock(root) {
|
|
@@ -97,8 +141,8 @@ export class InteractiveBridgePool {
|
|
|
97
141
|
// Fast path: we started/adopted it and its pid is still alive. A full health round-trip on
|
|
98
142
|
// every proxied request would put a 1.5 s timeout budget in front of every asset fetch.
|
|
99
143
|
const cached = this.live.get(root);
|
|
100
|
-
if (cached && pidAlive(cached.pid))
|
|
101
|
-
return cached;
|
|
144
|
+
if (cached && pidAlive(cached.bridge.pid))
|
|
145
|
+
return cached.bridge;
|
|
102
146
|
this.live.delete(root);
|
|
103
147
|
const pending = this.inflight.get(root);
|
|
104
148
|
if (pending)
|
|
@@ -116,14 +160,51 @@ export class InteractiveBridgePool {
|
|
|
116
160
|
return [...this.live.keys()];
|
|
117
161
|
}
|
|
118
162
|
async resolveOrStart(root) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
this.
|
|
126
|
-
return
|
|
163
|
+
// Adopt-or-start; either way the bridge just answered `/api/health` for this root, which is
|
|
164
|
+
// exactly the moment #298 wants the studio origin recorded — fire-and-forget, so recording
|
|
165
|
+
// can never delay (let alone fail) the proxied request that triggered the resolution.
|
|
166
|
+
const bridge = (await this.healthy(root)) ?? (await this.start(root));
|
|
167
|
+
const entry = { bridge, originRecorded: false };
|
|
168
|
+
this.live.set(root, entry);
|
|
169
|
+
this.recordStudioOrigin(entry);
|
|
170
|
+
return bridge;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* POST the daemon's own origin to the bridge's `/api/studio-origin` (#298), so the bridge's
|
|
174
|
+
* `GET /` redirects a direct visitor into studio instead of the API-only fallback page.
|
|
175
|
+
* At most one attempt per pooled bridge per process; 404/405 means the bridge predates the
|
|
176
|
+
* endpoint (interactive < 0.8.0) and is an expected skip, any other failure is a warn — never
|
|
177
|
+
* an error, because origin recording must never fail a proxy request.
|
|
178
|
+
*/
|
|
179
|
+
recordStudioOrigin(entry) {
|
|
180
|
+
if (entry.originRecorded)
|
|
181
|
+
return;
|
|
182
|
+
entry.originRecorded = true;
|
|
183
|
+
const origin = this.io.studioOrigin?.() ?? null;
|
|
184
|
+
if (origin === null)
|
|
185
|
+
return; // no origin to record (pool not bootstrapped from a listening daemon)
|
|
186
|
+
const { host, port } = entry.bridge;
|
|
187
|
+
const timeout = this.io.healthTimeoutMs ?? HEALTH_TIMEOUT_MS;
|
|
188
|
+
void (async () => {
|
|
189
|
+
try {
|
|
190
|
+
const res = await fetch(`http://${host}:${port}/api/studio-origin`, {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
headers: { 'content-type': 'application/json' },
|
|
193
|
+
body: JSON.stringify({ origin }),
|
|
194
|
+
signal: AbortSignal.timeout(timeout),
|
|
195
|
+
});
|
|
196
|
+
await res.arrayBuffer().catch(() => undefined); // drain, so the connection is released
|
|
197
|
+
if (res.status === 404 || res.status === 405) {
|
|
198
|
+
this.io.debug?.(`bridge at ${host}:${port} has no /api/studio-origin (interactive < 0.8.0) — skipping origin record`);
|
|
199
|
+
}
|
|
200
|
+
else if (!res.ok) {
|
|
201
|
+
this.io.log?.(`recording studio origin ${origin} with the bridge at ${host}:${port} failed: HTTP ${res.status}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
this.io.log?.(`recording studio origin ${origin} with the bridge at ${host}:${port} failed: ${err.message}`);
|
|
206
|
+
}
|
|
207
|
+
})();
|
|
127
208
|
}
|
|
128
209
|
/** The lockfile points at a bridge that is alive, answering, and serving THIS root. */
|
|
129
210
|
async healthy(root) {
|
|
@@ -178,7 +259,7 @@ export class InteractiveBridgePool {
|
|
|
178
259
|
function defaultSpawn(root) {
|
|
179
260
|
// `--yes` is load-bearing: without it npx PROMPTS when the package is not installed, and a
|
|
180
261
|
// daemon has no tty to answer with — the request would hang instead of failing to a 503.
|
|
181
|
-
return nodeSpawn('npx', ['--yes',
|
|
262
|
+
return nodeSpawn('npx', ['--yes', INTERACTIVE_SPEC, 'serve', '--root', root], {
|
|
182
263
|
cwd: root,
|
|
183
264
|
detached: true,
|
|
184
265
|
stdio: 'ignore',
|