wicked-crew 0.7.30 → 0.7.31
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/chat-scope.d.ts +15 -2
- package/dist/api/chat-scope.d.ts.map +1 -1
- package/dist/api/chat-scope.js +7 -2
- package/dist/api/chat-scope.js.map +1 -1
- package/dist/api/eval-sample.d.ts +12 -12
- package/dist/api/routes.d.ts +10 -0
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +231 -37
- package/dist/api/routes.js.map +1 -1
- package/dist/api/run-files.d.ts +26 -0
- package/dist/api/run-files.d.ts.map +1 -1
- package/dist/api/run-files.js +78 -0
- package/dist/api/run-files.js.map +1 -1
- package/dist/api/seat-health.d.ts +33 -0
- package/dist/api/seat-health.d.ts.map +1 -1
- package/dist/api/seat-health.js +86 -2
- package/dist/api/seat-health.js.map +1 -1
- package/dist/api/seat-signin.d.ts +8 -0
- package/dist/api/seat-signin.d.ts.map +1 -1
- package/dist/api/seat-signin.js +48 -3
- package/dist/api/seat-signin.js.map +1 -1
- package/dist/api/seat-standing.d.ts +14 -2
- package/dist/api/seat-standing.d.ts.map +1 -1
- package/dist/api/seat-standing.js +14 -4
- package/dist/api/seat-standing.js.map +1 -1
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +41 -4
- package/dist/api/server.js.map +1 -1
- package/dist/api/skills.d.ts +2 -2
- package/dist/api/testing.d.ts +28 -1
- package/dist/api/testing.d.ts.map +1 -1
- package/dist/api/testing.js +190 -0
- package/dist/api/testing.js.map +1 -1
- package/dist/campaigns/rollup.d.ts +4 -0
- package/dist/campaigns/rollup.d.ts.map +1 -1
- package/dist/campaigns/rollup.js.map +1 -1
- package/dist/campaigns/routes.d.ts +3 -1
- package/dist/campaigns/routes.d.ts.map +1 -1
- package/dist/campaigns/routes.js +3 -0
- package/dist/campaigns/routes.js.map +1 -1
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +23 -2
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/deliver-text.d.ts +7 -0
- package/dist/core/deliver-text.d.ts.map +1 -1
- package/dist/core/deliver-text.js +41 -2
- package/dist/core/deliver-text.js.map +1 -1
- package/dist/core/engine-roster.d.ts +68 -0
- package/dist/core/engine-roster.d.ts.map +1 -0
- package/dist/core/engine-roster.js +156 -0
- package/dist/core/engine-roster.js.map +1 -0
- package/dist/interactive/chat-events.d.ts.map +1 -1
- package/dist/interactive/chat-events.js +26 -4
- package/dist/interactive/chat-events.js.map +1 -1
- package/dist/interactive/council-outcome.d.ts +28 -1
- package/dist/interactive/council-outcome.d.ts.map +1 -1
- package/dist/interactive/council-outcome.js +51 -1
- package/dist/interactive/council-outcome.js.map +1 -1
- package/dist/interactive/demo-events.d.ts.map +1 -1
- package/dist/interactive/demo-events.js +26 -4
- package/dist/interactive/demo-events.js.map +1 -1
- package/dist/interactive/doc-run-index.d.ts +56 -0
- package/dist/interactive/doc-run-index.d.ts.map +1 -0
- package/dist/interactive/doc-run-index.js +100 -0
- package/dist/interactive/doc-run-index.js.map +1 -0
- package/dist/interactive/docs-index.d.ts +68 -0
- package/dist/interactive/docs-index.d.ts.map +1 -0
- package/dist/interactive/docs-index.js +114 -0
- package/dist/interactive/docs-index.js.map +1 -0
- package/dist/interactive/draft-events.d.ts +18 -0
- package/dist/interactive/draft-events.d.ts.map +1 -1
- package/dist/interactive/draft-events.js +36 -3
- 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 +26 -4
- package/dist/interactive/edit-events.js.map +1 -1
- package/dist/interactive/ledger.d.ts +2 -0
- package/dist/interactive/ledger.d.ts.map +1 -1
- package/dist/interactive/ledger.js +4 -0
- package/dist/interactive/ledger.js.map +1 -1
- package/dist/qe/author-workflow.d.ts +165 -0
- package/dist/qe/author-workflow.d.ts.map +1 -0
- package/dist/qe/author-workflow.js +377 -0
- package/dist/qe/author-workflow.js.map +1 -0
- package/dist/qe/test-sets.d.ts +112 -0
- package/dist/qe/test-sets.d.ts.map +1 -0
- package/dist/qe/test-sets.js +170 -0
- package/dist/qe/test-sets.js.map +1 -0
- package/dist/skills/refs.d.ts +37 -0
- package/dist/skills/refs.d.ts.map +1 -1
- package/dist/skills/refs.js +22 -0
- package/dist/skills/refs.js.map +1 -1
- package/dist/skills/runtime.d.ts +13 -1
- package/dist/skills/runtime.d.ts.map +1 -1
- package/dist/skills/runtime.js +88 -1
- package/dist/skills/runtime.js.map +1 -1
- package/dist/skills/store.d.ts +82 -5
- package/dist/skills/store.d.ts.map +1 -1
- package/dist/skills/store.js +109 -21
- package/dist/skills/store.js.map +1 -1
- package/dist/studio/assets/{index-_ydd-jKd.css → index-C0UZ2BxT.css} +1 -1
- package/dist/studio/assets/index-Clm0MXDF.js +544 -0
- package/dist/studio/index.html +2 -2
- package/dist/studio/testid-inventory.json +251 -5
- package/endpoint-manifest.json +24 -1
- package/package.json +3 -3
- package/dist/studio/assets/index-CWUS9Ump.js +0 -542
package/dist/api/routes.js
CHANGED
|
@@ -20,7 +20,10 @@ import { applyWorkerConfigRoot, signedInHeuristic } from './seat-signin.js';
|
|
|
20
20
|
import { chatSeatAdmission, seatStanding } from './seat-standing.js';
|
|
21
21
|
import { ChatScopeIndex, chatScopeDeps, prepareChatScratch, removeChatScratch, resolveChatScope, } from './chat-scope.js';
|
|
22
22
|
import { allowedRootsFor, isInsideRoot, openWithSystemDefault } from './open-path.js';
|
|
23
|
-
import { InvalidDiffBaseError, NotARegularFileError, UnresolvableDiffBaseError, readFileCapped, worktreeDiff, } from './run-files.js';
|
|
23
|
+
import { InvalidDiffBaseError, MERGE_BASE_LITERAL, NotARegularFileError, UnresolvableDiffBaseError, branchDiff, isPlainRef, readFileCapped, worktreeDiff, } from './run-files.js';
|
|
24
|
+
import { DocRunIndex } from '../interactive/doc-run-index.js';
|
|
25
|
+
import { listInteractiveDocs } from '../interactive/docs-index.js';
|
|
26
|
+
import { TestSetIndex } from '../qe/test-sets.js';
|
|
24
27
|
import { resolveProjectGraphBinding } from '../projects/graph.js';
|
|
25
28
|
import { registerProjectRoutes } from '../projects/routes.js';
|
|
26
29
|
import { registerCampaignRoutes } from '../campaigns/routes.js';
|
|
@@ -469,6 +472,10 @@ runtime = {}) {
|
|
|
469
472
|
const guidanceIndex = runtime.guidanceIndex ?? new GuidanceIndex();
|
|
470
473
|
const chatScopes = runtime.chatScopes ?? new ChatScopeIndex();
|
|
471
474
|
const deliveryIndex = runtime.deliveryIndex ?? new DeliveryIndex();
|
|
475
|
+
// Wave 6: the doc↔run binding and the registered test sets — `createServer` injects the real
|
|
476
|
+
// ones; a directly-driven route set gets an empty index (every run `document_id: null`, no sets).
|
|
477
|
+
const docRuns = runtime.docRuns ?? new DocRunIndex(() => []);
|
|
478
|
+
const testSets = runtime.testSets ?? new TestSetIndex();
|
|
472
479
|
const worktreeExists = runtime.worktreeExists ?? ((p) => existsSync(p));
|
|
473
480
|
const vacuityProbes = {
|
|
474
481
|
worktreeExists,
|
|
@@ -557,6 +564,9 @@ runtime = {}) {
|
|
|
557
564
|
const decorateRun = (view) => {
|
|
558
565
|
const conflictStrand = normalizeStranded(view);
|
|
559
566
|
view.session.project_id = projects.index.projectOf(view.session.id) ?? null;
|
|
567
|
+
// Wave 6 (F-4R2-006): the interactive document this run answered, from the seams' handoff
|
|
568
|
+
// ledgers — `null` = genuinely not a document run, so the field is ALWAYS present on served runs.
|
|
569
|
+
view.session.document_id = docRuns.documentOf(view.session.id) ?? null;
|
|
560
570
|
const retryOf = retryIndex.retryOfFor(view.session.id);
|
|
561
571
|
if (retryOf !== undefined)
|
|
562
572
|
view.session.retry_of = retryOf;
|
|
@@ -719,7 +729,11 @@ runtime = {}) {
|
|
|
719
729
|
signed_in: signed,
|
|
720
730
|
// The bench is THIS daemon's council evidence (councilSeatFailed, bounded window) — the
|
|
721
731
|
// prediction learns from what the engine actually did with the seat (#533 review, F-1).
|
|
722
|
-
...seatStanding(seat, signed, health, seatHealth.councilBenchFor(key)
|
|
732
|
+
...seatStanding(seat, signed, health, seatHealth.councilBenchFor(key),
|
|
733
|
+
// F-A45-006: the seat's OWN "no credential" report (a ballot's "No API key found", a
|
|
734
|
+
// worker's 401, an auth ACP fallback) overrides the file probe — `auth` flips, not only
|
|
735
|
+
// `council_eligible`, and the evidence rides on the wire.
|
|
736
|
+
seatHealth.authFailureFor(key)),
|
|
723
737
|
};
|
|
724
738
|
});
|
|
725
739
|
};
|
|
@@ -868,26 +882,8 @@ runtime = {}) {
|
|
|
868
882
|
if (resolved === null)
|
|
869
883
|
return reply;
|
|
870
884
|
const workdir = resolved.session.workdir;
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
}
|
|
874
|
-
if (!existsSync(workdir)) {
|
|
875
|
-
return reply.code(409).send({ error: `run ${id}'s workdir no longer exists: ${workdir}` });
|
|
876
|
-
}
|
|
877
|
-
// Narrowing is WORKTREE-scoped: a contained-but-outside-the-worktree path (extra write
|
|
878
|
-
// root / repo root) is a valid FILE read but has no meaning as a diff pathspec — rejected
|
|
879
|
-
// explicitly here rather than handing git a `../`-prefixed pathspec and surfacing its
|
|
880
|
-
// "outside repository" error as a 500 (Copilot, #305).
|
|
881
|
-
if (resolved.target !== undefined && !isInsideRoot(workdir, resolved.target)) {
|
|
882
|
-
return reply.code(400).send({
|
|
883
|
-
error: `\`path\` must be inside the run's worktree to diff: ${workdir}`,
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
const rel = resolved.target === undefined ? undefined : relative(workdir, resolved.target);
|
|
887
|
-
try {
|
|
888
|
-
return await worktreeDiff(workdir, rel, rawBase);
|
|
889
|
-
}
|
|
890
|
-
catch (err) {
|
|
885
|
+
/** The route's one error mapping, shared by the worktree and the branch reads. */
|
|
886
|
+
const diffError = (err) => {
|
|
891
887
|
// Named 400s (§8.1): malformed base (not a plain ref) and well-formed-but-unresolvable
|
|
892
888
|
// base are both client errors, each with its error name in the body — never a git 500.
|
|
893
889
|
if (err instanceof InvalidDiffBaseError || err instanceof UnresolvableDiffBaseError) {
|
|
@@ -905,6 +901,75 @@ runtime = {}) {
|
|
|
905
901
|
});
|
|
906
902
|
}
|
|
907
903
|
return reply.code(500).send({ error: message(err) });
|
|
904
|
+
};
|
|
905
|
+
const worktreeLive = typeof workdir === 'string' && workdir.length > 0 && existsSync(workdir);
|
|
906
|
+
if (!worktreeLive) {
|
|
907
|
+
// Wave 6 (F-7R2-013): the engine reaps a completed run's worktree, and the run page went dark
|
|
908
|
+
// (`409 workdir no longer exists`). The run's WORK is not gone — it lives on the `wicked/<id>`
|
|
909
|
+
// branch of the registered repo, and the wave-6 engine records `run_branch` + `base_commit`
|
|
910
|
+
// on the session. Serve the diff from the BRANCH (`source: 'branch'`): the recorded branch
|
|
911
|
+
// (else `wicked/<id>`) against the recorded base (else its merge-base with the default
|
|
912
|
+
// branch), `?base=<ref>` overriding the base with a plain in-repo ref. Only when there is
|
|
913
|
+
// neither a worktree nor a branch does the 409 stand — and then it says so.
|
|
914
|
+
const repoRef = resolved.session.repo_ref;
|
|
915
|
+
const root = repoRef !== null ? await repoRootOf(repoRef) : undefined;
|
|
916
|
+
if (root !== undefined) {
|
|
917
|
+
const session = resolved.session;
|
|
918
|
+
const branch = typeof session.run_branch === 'string' && session.run_branch !== '' ? session.run_branch : `wicked/${id}`;
|
|
919
|
+
let base = typeof session.base_commit === 'string' && session.base_commit !== '' ? session.base_commit : null;
|
|
920
|
+
if (rawBase !== undefined && rawBase !== '' && rawBase !== MERGE_BASE_LITERAL) {
|
|
921
|
+
if (!isPlainRef(rawBase)) {
|
|
922
|
+
return reply.code(400).send({
|
|
923
|
+
error: `InvalidDiffBaseError: \`base\` must be the literal '${MERGE_BASE_LITERAL}' or a plain git ref`,
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
base = rawBase;
|
|
927
|
+
}
|
|
928
|
+
// Narrowing: a path under the (gone) worktree or under the repo root, made repo-relative.
|
|
929
|
+
let rel;
|
|
930
|
+
if (resolved.target !== undefined) {
|
|
931
|
+
if (isInsideRoot(root, resolved.target))
|
|
932
|
+
rel = relative(root, resolved.target);
|
|
933
|
+
else if (typeof workdir === 'string' && workdir.length > 0 && isInsideRoot(workdir, resolved.target)) {
|
|
934
|
+
rel = relative(workdir, resolved.target);
|
|
935
|
+
}
|
|
936
|
+
else {
|
|
937
|
+
return reply.code(400).send({
|
|
938
|
+
error: `\`path\` must be inside the run's worktree or its repository to diff: ${root}`,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
try {
|
|
943
|
+
const fromBranch = await branchDiff(root, branch, base, rel);
|
|
944
|
+
if (fromBranch !== null)
|
|
945
|
+
return fromBranch;
|
|
946
|
+
}
|
|
947
|
+
catch (err) {
|
|
948
|
+
return diffError(err);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
if (typeof workdir !== 'string' || workdir.length === 0) {
|
|
952
|
+
return reply.code(409).send({ error: `run ${id} has no workdir and no run branch — nothing to diff` });
|
|
953
|
+
}
|
|
954
|
+
return reply.code(409).send({
|
|
955
|
+
error: `run ${id}'s workdir no longer exists (${workdir}) and its run branch holds no commits — nothing to diff`,
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
// Narrowing is WORKTREE-scoped: a contained-but-outside-the-worktree path (extra write
|
|
959
|
+
// root / repo root) is a valid FILE read but has no meaning as a diff pathspec — rejected
|
|
960
|
+
// explicitly here rather than handing git a `../`-prefixed pathspec and surfacing its
|
|
961
|
+
// "outside repository" error as a 500 (Copilot, #305).
|
|
962
|
+
if (resolved.target !== undefined && !isInsideRoot(workdir, resolved.target)) {
|
|
963
|
+
return reply.code(400).send({
|
|
964
|
+
error: `\`path\` must be inside the run's worktree to diff: ${workdir}`,
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
const rel = resolved.target === undefined ? undefined : relative(workdir, resolved.target);
|
|
968
|
+
try {
|
|
969
|
+
return { ...(await worktreeDiff(workdir, rel, rawBase)), source: 'worktree' };
|
|
970
|
+
}
|
|
971
|
+
catch (err) {
|
|
972
|
+
return diffError(err);
|
|
908
973
|
}
|
|
909
974
|
});
|
|
910
975
|
// Registered repos → target-repo picker.
|
|
@@ -981,7 +1046,10 @@ runtime = {}) {
|
|
|
981
1046
|
const input = {
|
|
982
1047
|
problem: b.problem,
|
|
983
1048
|
sessionId: b.sessionId ?? randomUUID(),
|
|
984
|
-
|
|
1049
|
+
// The default roster carries crew's STANDING (wave 6, the crew half of F-7R2-006): the adapter
|
|
1050
|
+
// translates `council_eligible: false` into the engine's per-seat bench verdict at launch
|
|
1051
|
+
// (`core/engine-roster.ts`), so a signed-out seat is never convened, never a judge.
|
|
1052
|
+
clisJson: b.clisJson ?? JSON.stringify(rosterWithStanding()),
|
|
985
1053
|
};
|
|
986
1054
|
if (b.entityMode !== undefined)
|
|
987
1055
|
input.entityMode = b.entityMode;
|
|
@@ -1152,8 +1220,17 @@ runtime = {}) {
|
|
|
1152
1220
|
// resolve, not leak.
|
|
1153
1221
|
// Fastify parses a REPEATED query param as string[] — normalize so `?include=archived`
|
|
1154
1222
|
// and `?include=archived&include=archived` behave identically (Copilot).
|
|
1155
|
-
const { include, limit } = req.query;
|
|
1223
|
+
const { include, limit, doc } = req.query;
|
|
1156
1224
|
const includeArchived = (Array.isArray(include) ? include : [include]).includes('archived');
|
|
1225
|
+
// `?doc=<document id>` (wave 6, F-4R2-006): only the runs the interactive seams launched for
|
|
1226
|
+
// that document — the doc↔run binding as a direct read (the run DTO carries `document_id`).
|
|
1227
|
+
// A repeated `?doc` is an ambiguity, not a repetition — refused like a repeated `?limit`.
|
|
1228
|
+
if (Array.isArray(doc)) {
|
|
1229
|
+
return reply.code(400).send({ error: '`doc` may be given at most once' });
|
|
1230
|
+
}
|
|
1231
|
+
if (doc !== undefined && doc.trim() === '') {
|
|
1232
|
+
return reply.code(400).send({ error: '`doc` must name a document id' });
|
|
1233
|
+
}
|
|
1157
1234
|
// `?limit=N` — the top N AFTER the actionable-first sort below, so a capped poll still sees
|
|
1158
1235
|
// the runs that need a human before the terminal sediment. The default stays UNBOUNDED: the
|
|
1159
1236
|
// param used to be read by nobody (silently ignored — the full payload regardless), so an
|
|
@@ -1176,9 +1253,10 @@ runtime = {}) {
|
|
|
1176
1253
|
}
|
|
1177
1254
|
cap = Number(limit);
|
|
1178
1255
|
}
|
|
1179
|
-
const
|
|
1256
|
+
const unarchived = includeArchived
|
|
1180
1257
|
? views
|
|
1181
1258
|
: views.filter((v) => v.session.archived_at == null);
|
|
1259
|
+
const visible = doc === undefined ? unarchived : unarchived.filter((v) => docRuns.documentOf(v.session.id) === doc.trim());
|
|
1182
1260
|
const ordered = sortActionableFirst(visible);
|
|
1183
1261
|
return { runs: (cap !== undefined ? ordered.slice(0, cap) : ordered).map(decorateRun) };
|
|
1184
1262
|
});
|
|
@@ -1577,19 +1655,23 @@ runtime = {}) {
|
|
|
1577
1655
|
// refuses per seat with its reason, which is copied into `refused` too.
|
|
1578
1656
|
const scoped = scope.kind !== 'none';
|
|
1579
1657
|
const refused = [];
|
|
1658
|
+
// The standing roster, read ONCE: the default admission below and the engine-drop
|
|
1659
|
+
// attribution after `chatOpen` both consult it (F-A45-011).
|
|
1660
|
+
const standing = rosterWithStanding();
|
|
1661
|
+
const standingOf = (key) => standing.find((s) => String(s.key) === key);
|
|
1580
1662
|
let clis;
|
|
1581
1663
|
if (b.clis !== undefined) {
|
|
1582
1664
|
clis = b.clis;
|
|
1583
1665
|
}
|
|
1584
1666
|
else {
|
|
1585
1667
|
clis = [];
|
|
1586
|
-
for (const seat of
|
|
1668
|
+
for (const seat of standing) {
|
|
1587
1669
|
const key = String(seat.key);
|
|
1588
1670
|
const admission = chatSeatAdmission(seat, seat.auth ?? 'unknown', scoped);
|
|
1589
1671
|
if (admission.ok)
|
|
1590
1672
|
clis.push(key);
|
|
1591
1673
|
else
|
|
1592
|
-
refused.push({ cliKey: key, reason: admission.reason });
|
|
1674
|
+
refused.push({ cliKey: key, reason: admission.reason, source: admission.source });
|
|
1593
1675
|
}
|
|
1594
1676
|
}
|
|
1595
1677
|
if (clis.length === 0) {
|
|
@@ -1613,7 +1695,44 @@ runtime = {}) {
|
|
|
1613
1695
|
// A REQUESTED seat the engine refused joins the same list, with the engine's reason.
|
|
1614
1696
|
for (const s of seats) {
|
|
1615
1697
|
if (!s.ok)
|
|
1616
|
-
refused.push({ cliKey: s.cliKey, reason: s.error ?? 'the engine refused the seat' });
|
|
1698
|
+
refused.push({ cliKey: s.cliKey, reason: s.error ?? 'the engine refused the seat', source: 'engine' });
|
|
1699
|
+
}
|
|
1700
|
+
// F-A45-011 (F-2R2-007 still open): a seat the engine DROPPED — absent from `seats`
|
|
1701
|
+
// altogether, neither ok nor refused — used to vanish without a trace (the fresh rig's pi:
|
|
1702
|
+
// default chips claude·pi·opencode, 201 `seats:[claude, opencode]`, `refused: []`). It was
|
|
1703
|
+
// taken out by the council-bench / dispatch-timeout path, not by admission, so admission's
|
|
1704
|
+
// list never named it. Every requested-or-defaulted seat that is not in `seats` is named
|
|
1705
|
+
// here with the most specific cause the daemon knows: its own "no credential" report
|
|
1706
|
+
// (`auth`), this daemon's council bench (`bench`), else the dispatch budget (`budget`).
|
|
1707
|
+
for (const key of clis) {
|
|
1708
|
+
if (seats.some((s) => s.cliKey === key) || refused.some((r) => r.cliKey === key))
|
|
1709
|
+
continue;
|
|
1710
|
+
const st = standingOf(key);
|
|
1711
|
+
const authFailure = seatHealth.authFailureFor(key);
|
|
1712
|
+
if (authFailure !== null || st?.auth === 'signed_out') {
|
|
1713
|
+
refused.push({
|
|
1714
|
+
cliKey: key,
|
|
1715
|
+
reason: authFailure !== null
|
|
1716
|
+
? `not seated — the seat itself reported no credential (${authFailure.source}: ${authFailure.detail}); sign it in from the System page`
|
|
1717
|
+
: 'not seated — signed out; sign it in from the System page',
|
|
1718
|
+
source: 'auth',
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
else if (st?.council_bench !== undefined) {
|
|
1722
|
+
refused.push({
|
|
1723
|
+
cliKey: key,
|
|
1724
|
+
reason: `not seated — ${st.council_ineligible_reason ?? 'benched by this daemon’s recent councils'}`,
|
|
1725
|
+
source: 'bench',
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
else {
|
|
1729
|
+
refused.push({
|
|
1730
|
+
cliKey: key,
|
|
1731
|
+
reason: 'not seated — the engine did not warm it within its dispatch budget (the seat timed out or ' +
|
|
1732
|
+
'was dropped at dispatch); check GET /roster and try again, or name seats with `clis`',
|
|
1733
|
+
source: 'budget',
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1617
1736
|
}
|
|
1618
1737
|
// Nothing warmed (independent review, W1): the engine holds no pool row and has dropped the
|
|
1619
1738
|
// scope itself (no `chatClosed` will come) — report the PER-SEAT reasons, never an
|
|
@@ -1679,7 +1798,7 @@ runtime = {}) {
|
|
|
1679
1798
|
projectAttachError = message(err);
|
|
1680
1799
|
}
|
|
1681
1800
|
}
|
|
1682
|
-
if (!chatScopes.set(chatId, scope, token)) {
|
|
1801
|
+
if (!chatScopes.set(chatId, scope, token, refused)) {
|
|
1683
1802
|
// The reservation was cancelled while the open was in flight — an engine `chatClosed` or a
|
|
1684
1803
|
// `DELETE` for this id (Copilot, #518). Nothing was recorded; tear the chat down (engine
|
|
1685
1804
|
// session, scratch root, and the filing just made) instead of returning a stale 201.
|
|
@@ -1702,6 +1821,7 @@ runtime = {}) {
|
|
|
1702
1821
|
chat: chatId,
|
|
1703
1822
|
cliKey: r.cliKey,
|
|
1704
1823
|
reason: r.reason,
|
|
1824
|
+
source: r.source,
|
|
1705
1825
|
...(b.projectId !== undefined ? { project_id: b.projectId } : {}),
|
|
1706
1826
|
});
|
|
1707
1827
|
}
|
|
@@ -1766,11 +1886,33 @@ runtime = {}) {
|
|
|
1766
1886
|
.send({ error: message(err) });
|
|
1767
1887
|
}
|
|
1768
1888
|
});
|
|
1889
|
+
// ── Interactive documents, daemon-wide, WITHOUT spawning a bridge (wave 6, studio #263) ──────
|
|
1890
|
+
// The per-project listing (`GET /projects/:id/interactive/api/docs`) asks the project's bridge —
|
|
1891
|
+
// one `wicked-interactive serve` per project root, ≈60 s cold start — so a skin must never fan
|
|
1892
|
+
// it out on mount. This reads what the bridge reads (every project's docs root, each slug child
|
|
1893
|
+
// carrying a `versions.json`) plus what only the daemon knows (the seams that answered each doc
|
|
1894
|
+
// and the runs they launched, from the handoff ledgers). `?includeRetired=1` lists tombstones.
|
|
1895
|
+
app.get(`${V}/interactive/docs`, { config: { manifest: { responseType: 'InteractiveDocsListing', statusCodes: [200] } } }, async (req) => {
|
|
1896
|
+
const q = req.query;
|
|
1897
|
+
const flag = Array.isArray(q.includeRetired) ? q.includeRetired[0] : q.includeRetired;
|
|
1898
|
+
return listInteractiveDocs({
|
|
1899
|
+
adapter,
|
|
1900
|
+
settings: projectSettings,
|
|
1901
|
+
docRuns,
|
|
1902
|
+
includeRetired: flag === '1' || flag === 'true',
|
|
1903
|
+
});
|
|
1904
|
+
});
|
|
1769
1905
|
app.get(`${V}/chats/:id`, { config: { manifest: { responseType: 'ChatDetailResponse', statusCodes: [200, 400] } } }, async (req, reply) => {
|
|
1770
1906
|
const { id } = req.params;
|
|
1771
1907
|
try {
|
|
1772
|
-
// The scope recorded at open (crew#502) — `null` for a chat this daemon did not open
|
|
1773
|
-
|
|
1908
|
+
// The scope recorded at open (crew#502) — `null` for a chat this daemon did not open — and
|
|
1909
|
+
// the seats refused at open (F-A45-011), so the studio's admission copy survives a reload.
|
|
1910
|
+
return {
|
|
1911
|
+
chatId: id,
|
|
1912
|
+
seats: await adapter.chatSeats(id),
|
|
1913
|
+
scope: chatScopes.get(id) ?? null,
|
|
1914
|
+
refused: chatScopes.refusedOf(id) ?? null,
|
|
1915
|
+
};
|
|
1774
1916
|
}
|
|
1775
1917
|
catch (err) {
|
|
1776
1918
|
return reply.code(400).send({ error: message(err) });
|
|
@@ -2051,11 +2193,39 @@ runtime = {}) {
|
|
|
2051
2193
|
const run = views.find((v) => v.session.id === id);
|
|
2052
2194
|
if (!run)
|
|
2053
2195
|
return reply.code(404).send({ error: 'Run not found' });
|
|
2054
|
-
|
|
2196
|
+
// Wave 6 (F-7R2-007 root fix): a run PARKED at a gate on a dead seat — the stall watchdog's
|
|
2197
|
+
// escalation gate, or a verdict escalation — used to answer 409 here, so the studio's control
|
|
2198
|
+
// needed two calls (approve, then race the engine's re-dispatch of the same dead seat with a
|
|
2199
|
+
// reassign). The ENGINE's `reassign_unit` accepts ONLY an Executing run (wicked-core actor.rs:
|
|
2200
|
+
// "run X is not Executing (status: AwaitingHuman)") — there is no reassign-in-place for a gated
|
|
2201
|
+
// run — so this route performs APPROVE-THEN-REASSIGN in one call: the approve resumes the run
|
|
2202
|
+
// (the engine re-dispatches the cursor unit to its current seat), and the reassign IMMEDIATELY
|
|
2203
|
+
// supersedes that turn (cancels its epoch, closes the seat's session, bumps the attempt) and
|
|
2204
|
+
// re-dispatches to the requested seat or the council's pick. The dead seat is dispatched for
|
|
2205
|
+
// the gap between the two engine calls — a bounded window the engine contract leaves open (a
|
|
2206
|
+
// reassign-as-approval engine op would close it; documented in the PR). Audited as BOTH a gate
|
|
2207
|
+
// decision (`via: 'reassign'`) and a reassign, so the "who approved" trail has no side door.
|
|
2208
|
+
// A steering-author propose gate is NOT approvable this way: its approve LANDS the proposal
|
|
2209
|
+
// through `POST /runs/:id/gate` (crew#388), and a reassign is not a review of the rules.
|
|
2210
|
+
const gated = run.session.status === 'awaiting_human';
|
|
2211
|
+
if (run.session.status !== 'executing' && !gated) {
|
|
2055
2212
|
return reply.code(409).send({
|
|
2056
2213
|
error: `run ${id} is ${run.session.status}, not executing — only an executing run has a cursor unit to reassign`,
|
|
2057
2214
|
});
|
|
2058
2215
|
}
|
|
2216
|
+
if (gated && typeof adapter.confirmGate !== 'function') {
|
|
2217
|
+
// A partial adapter (a directly-driven route set) cannot approve the gate on the caller's
|
|
2218
|
+
// behalf — say so by name, never a TypeError dressed as a refusal.
|
|
2219
|
+
return reply.code(409).send({
|
|
2220
|
+
error: `run ${id} is awaiting_human, not executing — the engine reassigns only an executing run, and ` +
|
|
2221
|
+
`this build cannot approve the gate on your behalf (no confirmGate); approve via POST /runs/${id}/gate, then reassign`,
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
if (gated && typeof adapter.listWorkflows === 'function' && isSteeringAuthorRun(run, adapter.listWorkflows())) {
|
|
2225
|
+
return reply.code(409).send({
|
|
2226
|
+
error: `run ${id} is awaiting a steering-author propose gate — approve or reject it via POST /runs/${id}/gate (the approve lands the proposal); reassign is not a review of the rules`,
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2059
2229
|
const requestedCli = parsed.data.cli;
|
|
2060
2230
|
if (requestedCli !== undefined) {
|
|
2061
2231
|
const pool = Array.isArray(run.session.clis) ? run.session.clis : [];
|
|
@@ -2068,12 +2238,24 @@ runtime = {}) {
|
|
|
2068
2238
|
const cursor = resolveCursorUnit(run);
|
|
2069
2239
|
const ord = cursor?.ord ?? run.session.unit_ix;
|
|
2070
2240
|
try {
|
|
2241
|
+
if (gated) {
|
|
2242
|
+
const status = await adapter.confirmGate(id, true);
|
|
2243
|
+
audit.record('gate.decided', actorOf(req), {
|
|
2244
|
+
runId: id,
|
|
2245
|
+
detail: { approve: true, via: 'reassign', status },
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2071
2248
|
await adapter.reassignUnit(id, ord, requestedCli ?? null);
|
|
2072
2249
|
audit.record('run.reassigned', actorOf(req), {
|
|
2073
2250
|
runId: id,
|
|
2074
|
-
detail: { ord, ...(requestedCli !== undefined ? { cli: requestedCli } : {}) },
|
|
2251
|
+
detail: { ord, ...(requestedCli !== undefined ? { cli: requestedCli } : {}), ...(gated ? { approved: true } : {}) },
|
|
2252
|
+
});
|
|
2253
|
+
return reply.send({
|
|
2254
|
+
status: 'ok',
|
|
2255
|
+
ord,
|
|
2256
|
+
...(requestedCli !== undefined ? { cli: requestedCli } : {}),
|
|
2257
|
+
...(gated ? { approved: true } : {}),
|
|
2075
2258
|
});
|
|
2076
|
-
return reply.send({ status: 'ok', ord, ...(requestedCli !== undefined ? { cli: requestedCli } : {}) });
|
|
2077
2259
|
}
|
|
2078
2260
|
catch (err) {
|
|
2079
2261
|
return reply.code(409).send({ error: message(err) });
|
|
@@ -3496,8 +3678,14 @@ runtime = {}) {
|
|
|
3496
3678
|
registerCampaignRoutes(app, adapter, {
|
|
3497
3679
|
audit,
|
|
3498
3680
|
actorOf,
|
|
3499
|
-
roster
|
|
3681
|
+
// The roster WITH crew's standing (F-086 — parity with POST /runs and POST /testing/*): the
|
|
3682
|
+
// adapter turns `council_eligible` into the engine's per-seat bench at launch, per campaign
|
|
3683
|
+
// node (`core/engine-roster.ts` `engineCampaignDef`), so a signed-out seat is never convened
|
|
3684
|
+
// by a campaign node either.
|
|
3685
|
+
roster: () => rosterWithStanding(),
|
|
3500
3686
|
groupIndex,
|
|
3687
|
+
// Wave 6 (F-7R2-014): the registered test sets ride beside the campaigns + groups.
|
|
3688
|
+
testSets,
|
|
3501
3689
|
deliveryUrlFor: (runId) => deliveryIndex.urlFor(runId),
|
|
3502
3690
|
vacuity: vacuityProbes,
|
|
3503
3691
|
// A non-probe derivation throw in the rollup is a defect — error level, loud in diagnostics.
|
|
@@ -3514,7 +3702,9 @@ runtime = {}) {
|
|
|
3514
3702
|
registerGovernanceSteeringRoutes(app, adapter, {
|
|
3515
3703
|
audit,
|
|
3516
3704
|
actorOf,
|
|
3517
|
-
|
|
3705
|
+
// Standing too (F-086 parity): the steering-author run launches through `launchRun`, whose
|
|
3706
|
+
// roster translation (`engineRosterJson`) already benches `council_eligible: false` seats.
|
|
3707
|
+
roster: () => rosterWithStanding(),
|
|
3518
3708
|
// So the steering-author run's `run.launched` entry stamps `created_at` live too (Copilot #466).
|
|
3519
3709
|
runTimingIndex,
|
|
3520
3710
|
});
|
|
@@ -3526,8 +3716,12 @@ runtime = {}) {
|
|
|
3526
3716
|
registerTestingRoutes(app, adapter, {
|
|
3527
3717
|
audit,
|
|
3528
3718
|
actorOf,
|
|
3529
|
-
roster
|
|
3719
|
+
// The roster WITH crew's standing (wave 6): the adapter turns `council_eligible` into the
|
|
3720
|
+
// engine's per-seat bench at launch, and the author response's `plan.seats` reads it.
|
|
3721
|
+
roster: () => rosterWithStanding(),
|
|
3530
3722
|
projects: { bus: projects.bus, index: projects.index },
|
|
3723
|
+
// The author launch files each run under its repo's `qe-tests-<repo>` label (F-7R2-014).
|
|
3724
|
+
groupIndex,
|
|
3531
3725
|
// So the recon fan's `run.launched` entries stamp `created_at` live too (Copilot #466), not
|
|
3532
3726
|
// just after a restart re-hydrates the trail.
|
|
3533
3727
|
runTimingIndex,
|