wicked-crew 0.7.1 → 0.7.3

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.
Files changed (56) hide show
  1. package/dist/api/delivery-index.d.ts +61 -0
  2. package/dist/api/delivery-index.d.ts.map +1 -0
  3. package/dist/api/delivery-index.js +87 -0
  4. package/dist/api/delivery-index.js.map +1 -0
  5. package/dist/api/governance-steering.d.ts +139 -0
  6. package/dist/api/governance-steering.d.ts.map +1 -0
  7. package/dist/api/governance-steering.js +302 -0
  8. package/dist/api/governance-steering.js.map +1 -0
  9. package/dist/api/governance-wiki.d.ts +28 -0
  10. package/dist/api/governance-wiki.d.ts.map +1 -0
  11. package/dist/api/governance-wiki.js +97 -0
  12. package/dist/api/governance-wiki.js.map +1 -0
  13. package/dist/api/routes.d.ts +21 -16
  14. package/dist/api/routes.d.ts.map +1 -1
  15. package/dist/api/routes.js +196 -10
  16. package/dist/api/routes.js.map +1 -1
  17. package/dist/api/server.d.ts.map +1 -1
  18. package/dist/api/server.js +56 -1
  19. package/dist/api/server.js.map +1 -1
  20. package/dist/api/testing.d.ts +117 -0
  21. package/dist/api/testing.d.ts.map +1 -0
  22. package/dist/api/testing.js +152 -0
  23. package/dist/api/testing.js.map +1 -0
  24. package/dist/campaign/supervision.d.ts +296 -0
  25. package/dist/campaign/supervision.d.ts.map +1 -0
  26. package/dist/campaign/supervision.js +515 -0
  27. package/dist/campaign/supervision.js.map +1 -0
  28. package/dist/campaigns/plan.d.ts +64 -0
  29. package/dist/campaigns/plan.d.ts.map +1 -0
  30. package/dist/campaigns/plan.js +201 -0
  31. package/dist/campaigns/plan.js.map +1 -0
  32. package/dist/campaigns/routes.d.ts +28 -0
  33. package/dist/campaigns/routes.d.ts.map +1 -0
  34. package/dist/campaigns/routes.js +177 -0
  35. package/dist/campaigns/routes.js.map +1 -0
  36. package/dist/core/adapter.d.ts +160 -1
  37. package/dist/core/adapter.d.ts.map +1 -1
  38. package/dist/core/adapter.js +300 -1
  39. package/dist/core/adapter.js.map +1 -1
  40. package/dist/projects/routes.d.ts +4 -4
  41. package/dist/qe/acceptance.d.ts +20 -1
  42. package/dist/qe/acceptance.d.ts.map +1 -1
  43. package/dist/qe/acceptance.js +32 -0
  44. package/dist/qe/acceptance.js.map +1 -1
  45. package/dist/qe/conformance.d.ts +158 -0
  46. package/dist/qe/conformance.d.ts.map +1 -0
  47. package/dist/qe/conformance.js +244 -0
  48. package/dist/qe/conformance.js.map +1 -0
  49. package/dist/studio/assets/index-BAW9rqNT.css +32 -0
  50. package/dist/studio/assets/index-UahdRwF7.js +532 -0
  51. package/dist/studio/index.html +2 -2
  52. package/dist/studio/testid-inventory.json +4387 -0
  53. package/endpoint-manifest.json +154 -9
  54. package/package.json +3 -3
  55. package/dist/studio/assets/index-D-BFUYnY.js +0 -530
  56. package/dist/studio/assets/index-D6S9zUtO.css +0 -32
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The `/api/v1/governance/wiki/*` surface (wiki-mgmt) — the management wire for the graph-backed
3
+ * architecture wiki, so the wiki is VISIBLE (not just queryable): a scoreboard that tells a
4
+ * populated wiki from an ingested-once-and-decaying one, and a meta probe cheap enough for the
5
+ * UI to call on mount so an empty store shows an honest "nothing seeded — here's the runbook"
6
+ * instead of a blank table.
7
+ *
8
+ * Thin by design, like `campaigns/routes.ts`: the scoreboard is computed by wicked-core's
9
+ * governance layer (`wicked-governance::scoreboard`, read-only `open_store_ro` — safe beside the
10
+ * live single-writer daemon); the daemon keeps no shadow wiki state. Error posture mirrors the
11
+ * campaign surface: a build whose engine addon lacks the `governanceScoreboard` binding answers
12
+ * 501 (`GovernanceScoreboardUnsupportedError` — "upgrade the engine"), never 400 ("fix your
13
+ * request").
14
+ *
15
+ * Rule/ruleset BROWSE and RETIRE deliberately live elsewhere — this module adds no second door:
16
+ * `GET /governance/rules` (facet-filterable, retired rows flagged) is the browse surface and
17
+ * `DELETE /governance/rules/:id` (retire-not-delete, FINDING-038) is the kill switch, both in
18
+ * `api/routes.ts`.
19
+ */
20
+ import type { FastifyInstance } from 'fastify';
21
+ import { type CoreAdapter } from '../core/adapter.js';
22
+ /**
23
+ * The authoring guide the honest empty state points at: the AW-13 seed runbook — frontmatter
24
+ * conventions, the `seed_wiki.py` driver, and the `wicked-core rules` ingest/fanout/relink CLIs.
25
+ */
26
+ export declare const WIKI_AUTHORING_DOC = "https://github.com/mikeparcewski/wicked-core/blob/main/crates/wicked-governance/seed/README.md";
27
+ export declare function registerGovernanceWikiRoutes(app: FastifyInstance, adapter: CoreAdapter): void;
28
+ //# sourceMappingURL=governance-wiki.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"governance-wiki.d.ts","sourceRoot":"","sources":["../../src/api/governance-wiki.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,EAAwC,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAM5F;;;GAGG;AACH,eAAO,MAAM,kBAAkB,mGACmE,CAAC;AAMnG,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAwE7F"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The `/api/v1/governance/wiki/*` surface (wiki-mgmt) — the management wire for the graph-backed
3
+ * architecture wiki, so the wiki is VISIBLE (not just queryable): a scoreboard that tells a
4
+ * populated wiki from an ingested-once-and-decaying one, and a meta probe cheap enough for the
5
+ * UI to call on mount so an empty store shows an honest "nothing seeded — here's the runbook"
6
+ * instead of a blank table.
7
+ *
8
+ * Thin by design, like `campaigns/routes.ts`: the scoreboard is computed by wicked-core's
9
+ * governance layer (`wicked-governance::scoreboard`, read-only `open_store_ro` — safe beside the
10
+ * live single-writer daemon); the daemon keeps no shadow wiki state. Error posture mirrors the
11
+ * campaign surface: a build whose engine addon lacks the `governanceScoreboard` binding answers
12
+ * 501 (`GovernanceScoreboardUnsupportedError` — "upgrade the engine"), never 400 ("fix your
13
+ * request").
14
+ *
15
+ * Rule/ruleset BROWSE and RETIRE deliberately live elsewhere — this module adds no second door:
16
+ * `GET /governance/rules` (facet-filterable, retired rows flagged) is the browse surface and
17
+ * `DELETE /governance/rules/:id` (retire-not-delete, FINDING-038) is the kill switch, both in
18
+ * `api/routes.ts`.
19
+ */
20
+ import { GovernanceScoreboardUnsupportedError } from '../core/adapter.js';
21
+ import { API_PREFIX } from './api-prefix.js';
22
+ const V = API_PREFIX;
23
+ /**
24
+ * The authoring guide the honest empty state points at: the AW-13 seed runbook — frontmatter
25
+ * conventions, the `seed_wiki.py` driver, and the `wicked-core rules` ingest/fanout/relink CLIs.
26
+ */
27
+ export const WIKI_AUTHORING_DOC = 'https://github.com/mikeparcewski/wicked-core/blob/main/crates/wicked-governance/seed/README.md';
28
+ function message(err) {
29
+ return err instanceof Error ? err.message : String(err);
30
+ }
31
+ export function registerGovernanceWikiRoutes(app, adapter) {
32
+ // The AW-23 population/connection scoreboard. `?docsRoot=` is the OPTIONAL docs root the
33
+ // typing half needs (the same directory `rules ingest --dir` consumed — enforcement_class
34
+ // lives in doc frontmatter, never on the rule node); omitted, the scoreboard still answers
35
+ // 200 with `typing.available: false` and the reason in-band — a wiki with no local docs
36
+ // checkout is a normal deployment, not an error.
37
+ app.get(`${V}/governance/wiki/scoreboard`, {
38
+ config: {
39
+ manifest: {
40
+ responseType: '{ scoreboard: GovernanceScoreboard }',
41
+ // 501: the installed engine addon predates the governanceScoreboard binding
42
+ // (wicked-core-ts < 0.7.4) — upgrade the engine, the request was already correct.
43
+ statusCodes: [200, 500, 501],
44
+ },
45
+ },
46
+ }, async (req, reply) => {
47
+ // Fastify parses a repeated `?docsRoot=a&docsRoot=b` into an array — take the first, and
48
+ // TRIM so a whitespace-only value means "no docs root" rather than a doomed doc scan.
49
+ const raw = req.query.docsRoot;
50
+ const first = Array.isArray(raw) ? raw[0] : raw;
51
+ const docsRoot = first?.trim() || undefined;
52
+ try {
53
+ const scoreboard = await adapter.governanceScoreboard(docsRoot);
54
+ return { scoreboard };
55
+ }
56
+ catch (err) {
57
+ if (err instanceof GovernanceScoreboardUnsupportedError) {
58
+ return reply.code(501).send({ error: err.message });
59
+ }
60
+ // An engine/store failure is OURS, not the caller's — there is nothing to fix in a
61
+ // bare GET, so nothing here maps to 400.
62
+ return reply.code(500).send({ error: message(err) });
63
+ }
64
+ });
65
+ // The wiki's honest empty-state signal. Works on EVERY addon (listConformanceRules is a
66
+ // required binding since crew#40), so the UI can always distinguish "nothing seeded — run the
67
+ // seed runbook" from "the engine can't tell me": `ruleset_count` is `null` (never a fabricated
68
+ // 0) on a build without the listRuleSets binding, and `scoreboard_available` says up front
69
+ // whether the scoreboard route will answer 200 here.
70
+ app.get(`${V}/governance/wiki/meta`, {
71
+ config: {
72
+ manifest: {
73
+ responseType: '{ meta: GovernanceWikiMeta }',
74
+ statusCodes: [200],
75
+ },
76
+ },
77
+ },
78
+ // Wrapped per crew house style — campaigns/rules/scoreboard all wrap, and studio's
79
+ // Wiki page reads `body.meta` (a bare seeded:false would read as "cannot tell").
80
+ async () => {
81
+ // NOTE: pre-0.7.4 addons funnel listConformanceRules through recall, which skips retired
82
+ // rows — so `rule_count` counts the LISTABLE rules. That is the right number for "is
83
+ // anything here": a store holding only retired rules has nothing recallable to show.
84
+ const rules = await adapter.listConformanceRules();
85
+ const rulesetCount = await adapter.countRuleSets();
86
+ return {
87
+ meta: {
88
+ seeded: rules.length > 0 || (rulesetCount ?? 0) > 0,
89
+ ruleset_count: rulesetCount,
90
+ rule_count: rules.length,
91
+ scoreboard_available: adapter.wikiScoreboardSupported(),
92
+ doc: WIKI_AUTHORING_DOC,
93
+ },
94
+ };
95
+ });
96
+ }
97
+ //# sourceMappingURL=governance-wiki.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"governance-wiki.js","sourceRoot":"","sources":["../../src/api/governance-wiki.ts"],"names":[],"mappings":"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"}
@@ -10,6 +10,7 @@ import { InteractiveBridgePool } from '../interactive/bridge-pool.js';
10
10
  import { AuditLog } from './audit.js';
11
11
  import { RetryIndex } from './retry-index.js';
12
12
  import { GuidanceIndex } from './guidance-index.js';
13
+ import { DeliveryIndex } from './delivery-index.js';
13
14
  import { type AuthMode } from './auth.js';
14
15
  export { API_PREFIX } from './api-prefix.js';
15
16
  /**
@@ -48,46 +49,46 @@ export declare const LaunchSchema: z.ZodEffects<z.ZodObject<{
48
49
  problem: string;
49
50
  deliver?: "pr" | undefined;
50
51
  projectId?: string | undefined;
51
- retryOf?: string | undefined;
52
- sessionId?: string | undefined;
52
+ workflow?: string | undefined;
53
+ repoRef?: string | undefined;
53
54
  clisJson?: string | undefined;
55
+ sessionId?: string | undefined;
56
+ retryOf?: string | undefined;
54
57
  entityMode?: "shared" | "isolated" | undefined;
55
58
  humanConfirm?: string | undefined;
56
- repoRef?: string | undefined;
57
- workflow?: string | undefined;
58
59
  }, {
59
60
  problem: string;
60
61
  deliver?: "pr" | undefined;
61
62
  projectId?: string | undefined;
62
- retryOf?: string | undefined;
63
- sessionId?: string | undefined;
63
+ workflow?: string | undefined;
64
+ repoRef?: string | undefined;
64
65
  clisJson?: string | undefined;
66
+ sessionId?: string | undefined;
67
+ retryOf?: string | undefined;
65
68
  entityMode?: "shared" | "isolated" | undefined;
66
69
  humanConfirm?: string | undefined;
67
- repoRef?: string | undefined;
68
- workflow?: string | undefined;
69
70
  }>, {
70
71
  problem: string;
71
72
  deliver?: "pr" | undefined;
72
73
  projectId?: string | undefined;
73
- retryOf?: string | undefined;
74
- sessionId?: string | undefined;
74
+ workflow?: string | undefined;
75
+ repoRef?: string | undefined;
75
76
  clisJson?: string | undefined;
77
+ sessionId?: string | undefined;
78
+ retryOf?: string | undefined;
76
79
  entityMode?: "shared" | "isolated" | undefined;
77
80
  humanConfirm?: string | undefined;
78
- repoRef?: string | undefined;
79
- workflow?: string | undefined;
80
81
  }, {
81
82
  problem: string;
82
83
  deliver?: "pr" | undefined;
83
84
  projectId?: string | undefined;
84
- retryOf?: string | undefined;
85
- sessionId?: string | undefined;
85
+ workflow?: string | undefined;
86
+ repoRef?: string | undefined;
86
87
  clisJson?: string | undefined;
88
+ sessionId?: string | undefined;
89
+ retryOf?: string | undefined;
87
90
  entityMode?: "shared" | "isolated" | undefined;
88
91
  humanConfirm?: string | undefined;
89
- repoRef?: string | undefined;
90
- workflow?: string | undefined;
91
92
  }>;
92
93
  export declare const GateSchema: z.ZodObject<{
93
94
  approve: z.ZodBoolean;
@@ -157,6 +158,10 @@ export interface RuntimeDeps {
157
158
  /** Run→operator-guidance index (CREW-UX-7) — `createServer` hydrates one from the audit trail
158
159
  * so a restarted daemon still echoes `guidance`; a directly-driven route set gets a fresh one. */
159
160
  guidanceIndex?: GuidanceIndex;
161
+ /** Run→delivered-PR index (CREW-UX-8, crew#321) — `createServer` hydrates one from the audit
162
+ * trail so a restarted daemon still echoes `delivery`; a directly-driven route set gets a
163
+ * fresh one. */
164
+ deliveryIndex?: DeliveryIndex;
160
165
  openWithOs?: (target: string) => Promise<void>;
161
166
  /** Seat sign-in presence probe (seat sign-in) — injectable so route tests never read the
162
167
  * developer's real dotfiles. Defaults to the file/env heuristic in seat-signin.ts. */
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/api/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,OAAO,EAAwB,WAAW,EAAkD,MAAM,oBAAoB,CAAC;AAEvH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAMnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAWrD,OAAO,EAAyB,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAEtF,OAAO,EAAe,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAInF,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAGvD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAyF7C;;;;;;;;;;;;GAYG;AAIH,eAAO,MAAM,YAAY;;;;;;;;IAQvB;+FAC2F;;IAE3F;;wFAEoF;;IAEpF;;0FAEsF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKtF,CAAC;AAEH,eAAO,MAAM,UAAU;;;;;;;;;EAGZ,CAAC;AAEZ;;yEAEyE;AACzE,eAAO,MAAM,cAAc;;;;;;EAEhB,CAAC;AAUZ,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;EAQpB,CAAC;AAOZ,4FAA4F;AAC5F,eAAO,MAAM,cAAc;;;;;;;;;EAGhB,CAAC;AAwBZ,sEAAsE;AACtE,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,QAAQ,CAAC;IAChB,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B;oGACgG;IAChG,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB;uGACmG;IACnG,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C;2FACuF;IACvF,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC;IAC1E;;0DAEsD;IACtD,kBAAkB,CAAC,EAAE,qBAAqB,CAAC;CAC5C;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,eAAe,EACpB,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,SAAS,EACpB,gBAAgB,EAAE,gBAAgB,EAIlC,YAAY,GAAE,WAA+B,EAG7C,QAAQ,GAAE,iBAAqF,EAK/F,QAAQ,GAAE,YAA0D,EAGpE,OAAO,GAAE,WAAgB,GACxB,IAAI,CA+wDN"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/api/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,OAAO,EAAwB,WAAW,EAA4E,MAAM,oBAAoB,CAAC;AAEjJ,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAMnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAWrD,OAAO,EAAyB,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAWtF,OAAO,EAAe,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAInF,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAGvD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAgG7C;;;;;;;;;;;;GAYG;AAIH,eAAO,MAAM,YAAY;;;;;;;;IAQvB;+FAC2F;;IAE3F;;wFAEoF;;IAEpF;;0FAEsF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKtF,CAAC;AAEH,eAAO,MAAM,UAAU;;;;;;;;;EAGZ,CAAC;AAEZ;;yEAEyE;AACzE,eAAO,MAAM,cAAc;;;;;;EAEhB,CAAC;AAUZ,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;EAQpB,CAAC;AAOZ,4FAA4F;AAC5F,eAAO,MAAM,cAAc;;;;;;;;;EAGhB,CAAC;AAwBZ,sEAAsE;AACtE,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,QAAQ,CAAC;IAChB,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B;oGACgG;IAChG,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB;uGACmG;IACnG,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;qBAEiB;IACjB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C;2FACuF;IACvF,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC;IAC1E;;0DAEsD;IACtD,kBAAkB,CAAC,EAAE,qBAAqB,CAAC;CAC5C;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,eAAe,EACpB,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,SAAS,EACpB,gBAAgB,EAAE,gBAAgB,EAIlC,YAAY,GAAE,WAA+B,EAG7C,QAAQ,GAAE,iBAAqF,EAK/F,QAAQ,GAAE,YAA0D,EAGpE,OAAO,GAAE,WAAgB,GACxB,IAAI,CAk+DN"}
@@ -5,7 +5,7 @@ import { readFileSync, existsSync } from 'node:fs';
5
5
  import { promises as fsp } from 'node:fs';
6
6
  import { isAbsolute, join, relative, resolve } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
- import { ChatUnsupportedError, CoreAdapter, ElicitationUnsupportedError, humanGatePhaseIds } from '../core/adapter.js';
8
+ import { ChatUnsupportedError, CoreAdapter, ElicitationUnsupportedError, SteeringUnsupportedError, humanGatePhaseIds } from '../core/adapter.js';
9
9
  import { codeGraphDb, requirementsGraph } from '../core/repoPaths.js';
10
10
  import { QeGateCache } from '../qe/gate-events.js';
11
11
  import { buildAcceptanceView, resolveRunWorkflow } from '../qe/acceptance.js';
@@ -18,6 +18,10 @@ import { allowedRootsFor, isInsideRoot, openWithSystemDefault } from './open-pat
18
18
  import { InvalidDiffBaseError, NotARegularFileError, UnresolvableDiffBaseError, readFileCapped, worktreeDiff, } from './run-files.js';
19
19
  import { resolveProjectGraphBinding } from '../projects/graph.js';
20
20
  import { registerProjectRoutes } from '../projects/routes.js';
21
+ import { registerCampaignRoutes } from '../campaigns/routes.js';
22
+ import { registerGovernanceWikiRoutes } from './governance-wiki.js';
23
+ import { DEFAULT_STEERING_TYPE, STEERING_TYPE_VALUES, STEERING_TYPES, registerGovernanceSteeringRoutes, } from './governance-steering.js';
24
+ import { registerTestingRoutes } from './testing.js';
21
25
  import { ProjectSettingsStore } from '../projects/settings.js';
22
26
  import { boundOrigin, InteractiveBridgePool } from '../interactive/bridge-pool.js';
23
27
  import { registerInteractiveProxy } from '../interactive/proxy-routes.js';
@@ -26,6 +30,7 @@ import { MEMBERSHIP_ATTACHED, membershipAttachedKey } from '../projects/events.j
26
30
  import { AuditLog } from './audit.js';
27
31
  import { RetryIndex } from './retry-index.js';
28
32
  import { GuidanceIndex } from './guidance-index.js';
33
+ import { DeliveryIndex } from './delivery-index.js';
29
34
  import { LOCAL_ACTOR } from './auth.js';
30
35
  // Re-exported so existing `import { API_PREFIX } from './routes.js'` callers keep working; the
31
36
  // value lives in the leaf module api-prefix.ts to keep unit-output.ts out of this file's cycle.
@@ -73,6 +78,12 @@ function invalidBody(err, what) {
73
78
  : what;
74
79
  return { error, details: err.issues };
75
80
  }
81
+ // Closed facet vocabularies for the `GET /governance/rules` browse filters (wiki-mgmt) — the
82
+ // wire contract's `RuleBrowseQuery`. Sets, not zod: the query is otherwise free-form and only
83
+ // these three facets have a vocabulary to enforce.
84
+ const RULE_SEVERITIES = new Set(['info', 'warn', 'error', 'critical']);
85
+ const RULE_TYPES = new Set(['pattern', 'policy']);
86
+ const RULE_STATUSES = new Set(['active', 'retired', 'all']);
76
87
  // Repo names become directory components under ~/.wicked/repos/ — reject anything
77
88
  // that would allow path traversal (slashes, dots-only segments, control chars).
78
89
  const SAFE_REPO_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
@@ -217,10 +228,12 @@ runtime = {}) {
217
228
  const signedIn = runtime.signedIn ?? signedInHeuristic;
218
229
  const retryIndex = runtime.retryIndex ?? new RetryIndex();
219
230
  const guidanceIndex = runtime.guidanceIndex ?? new GuidanceIndex();
231
+ const deliveryIndex = runtime.deliveryIndex ?? new DeliveryIndex();
220
232
  // The run-DTO joins (DES-UX-001 §8.2/§8.3, DES-UX-002 §7.2): `project_id` from the membership
221
233
  // record — `null` = genuinely unfiled, so the field is ALWAYS present on served runs —
222
- // `retry_of` from the lineage index, and `guidance` from the guidance index, each set only
223
- // when known (absent, never null, spells "not a retry" / "no note").
234
+ // `retry_of` from the lineage index, `guidance` from the guidance index, and `delivery`
235
+ // from the delivery index (CREW-UX-8, crew#321), each set only when known (absent, never
236
+ // null, spells "not a retry" / "no note" / "delivered nothing").
224
237
  // Applied at DTO assembly on exactly the two endpoints that serve the run DTO
225
238
  // (GET /runs + GET /runs/:id); the internal sessionsDetail() consumers are untouched.
226
239
  const decorateRun = (view) => {
@@ -231,6 +244,9 @@ runtime = {}) {
231
244
  const guidance = guidanceIndex.guidanceFor(view.session.id);
232
245
  if (guidance !== undefined)
233
246
  view.session.guidance = guidance;
247
+ const delivery = deliveryIndex.deliveryFor(view.session.id);
248
+ if (delivery !== undefined)
249
+ view.session.delivery = delivery;
234
250
  return view;
235
251
  };
236
252
  // Resolved ONCE and shared by the project routes (which read/write `interactiveRoot`) and the
@@ -977,6 +993,12 @@ runtime = {}) {
977
993
  workflow,
978
994
  gateEvents: qeGateEvents,
979
995
  ...(qeRunId !== undefined && qeRunId !== '' ? { qeRunId } : {}),
996
+ // AW-14 (arch-R13a + R16): the conformance section reads the same wires the standalone
997
+ // `/governance/claims` and `/runs/:id/events` routes serve, but run-scoped and resolved
998
+ // deny-dominates BESIDE the QE gate — so a wiki-rule violation and an unenforced governed
999
+ // unit both appear on the page humans actually look at.
1000
+ claims: () => adapter.listConformanceClaims(),
1001
+ events: (rid) => adapter.runEvents(rid),
980
1002
  });
981
1003
  });
982
1004
  // The steering gate (§11.1). approve+amend = approve-with-steer; approve:false = reject (cancels).
@@ -1312,12 +1334,92 @@ runtime = {}) {
1312
1334
  return { runId: id, total: events.length, returned: filtered.length, events: filtered };
1313
1335
  });
1314
1336
  // ── Governance reads (crew#40) ──────────────────────────────────────────────
1315
- app.get(`${V}/governance/policies`, async () => {
1337
+ // STEERING fold: the READ half of the old policy surface stays — retired-not-deleted means a
1338
+ // past gate decision citing a policy id must stay resolvable, and the engine keeps serving the
1339
+ // listing (as a thin shim over the unified store once the model merges). The WRITE half
1340
+ // (POST/DELETE below) answers 410 on a steering engine — see the fold comment there.
1341
+ app.get(`${V}/governance/policies`, { config: { manifest: { responseType: '{ policies: GovernancePolicy[] }', statusCodes: [200] } } }, async () => {
1316
1342
  const policies = await adapter.listPolicies();
1317
1343
  return { policies };
1318
1344
  });
1319
- app.get(`${V}/governance/rules`, async () => {
1320
- const rules = await adapter.listConformanceRules();
1345
+ // The wiki BROWSE surface (wiki-mgmt): facet filters over the listed rows. EXACT matches by
1346
+ // design — unlike `/governance/rules/preview`, whose recall semantics treat an absent rule
1347
+ // facet as a wildcard (enforcement's question: "which rules apply HERE"), browse answers
1348
+ // "show me the rules TAGGED `layer=api`", and a wildcard rule flooding every layer filter
1349
+ // would bury the tagged ones. `?status=` keeps the AW-24 kill switch visible: every row keeps
1350
+ // its `retired` flag, and `retired`/`active` narrow to one side. Default is `all` — the
1351
+ // route's historical behavior, though pre-0.7.4 addons funnel the listing through recall and
1352
+ // return active rows only (the engine's fact to fix, not this filter's to mask).
1353
+ app.get(`${V}/governance/rules`, {
1354
+ config: {
1355
+ manifest: {
1356
+ responseType: '{ rules: ConformanceRule[] }',
1357
+ // 501: `?type=` on a pre-steering engine (wicked-core-ts < 0.7.5) — see the facet below.
1358
+ statusCodes: [200, 400, 501],
1359
+ },
1360
+ },
1361
+ }, async (req, reply) => {
1362
+ const q = req.query;
1363
+ // Same normalization as `repoParam` below: first value of a repeated param, trimmed, and
1364
+ // an empty/whitespace value means "facet not given" rather than a filter matching nothing.
1365
+ const facet = (k) => {
1366
+ const raw = q[k];
1367
+ const first = Array.isArray(raw) ? raw[0] : raw;
1368
+ const trimmed = first?.trim();
1369
+ return trimmed ? trimmed : undefined;
1370
+ };
1371
+ // Closed vocabularies reject loudly (400 names the valid set): `?severity=hihg` silently
1372
+ // matching nothing would read as "no critical rules" — an empty answer to a question the
1373
+ // caller didn't ask.
1374
+ const invalid = (name, got, valid) => reply.code(400).send({ error: `${name} must be one of ${valid} (got \`${got}\`)` });
1375
+ const severity = facet('severity');
1376
+ if (severity !== undefined && !RULE_SEVERITIES.has(severity)) {
1377
+ return invalid('severity', severity, 'info|warn|error|critical');
1378
+ }
1379
+ const ruleType = facet('rule_type');
1380
+ if (ruleType !== undefined && !RULE_TYPES.has(ruleType)) {
1381
+ return invalid('rule_type', ruleType, 'pattern|policy');
1382
+ }
1383
+ // `?type=` — the STEERING facet (one Steering sub-page per type). Closed vocabulary, and
1384
+ // presence-gated on the steering engine: rows served by a pre-0.7.5 addon carry no
1385
+ // `steering_type`, so filtering them would answer "no <type> rules" to a question this
1386
+ // engine cannot answer — 501 ("upgrade the engine"), never an empty non-answer.
1387
+ const steeringType = facet('type');
1388
+ if (steeringType !== undefined && !STEERING_TYPES.has(steeringType)) {
1389
+ return invalid('type', steeringType, STEERING_TYPE_VALUES.join('|'));
1390
+ }
1391
+ if (steeringType !== undefined && !adapter.steeringSupported()) {
1392
+ return reply
1393
+ .code(501)
1394
+ .send({ error: new SteeringUnsupportedError('Filtering rules by steering type').message });
1395
+ }
1396
+ // `?include_retired=` — the boolean spelling of the retire filter (`true` ⇒ `all`,
1397
+ // `false` ⇒ `active`). Mutually exclusive with `?status=`: the two are one filter in two
1398
+ // spellings, and silently picking a winner when they contradict would answer a question
1399
+ // the caller didn't ask.
1400
+ const includeRetired = facet('include_retired');
1401
+ if (includeRetired !== undefined && includeRetired !== 'true' && includeRetired !== 'false') {
1402
+ return invalid('include_retired', includeRetired, 'true|false');
1403
+ }
1404
+ const statusGiven = facet('status');
1405
+ if (includeRetired !== undefined && statusGiven !== undefined) {
1406
+ return reply.code(400).send({
1407
+ error: 'send `status` or `include_retired`, not both — they are two spellings of the same retire filter and can contradict',
1408
+ });
1409
+ }
1410
+ const status = statusGiven ?? (includeRetired === undefined ? 'all' : includeRetired === 'true' ? 'all' : 'active');
1411
+ if (!RULE_STATUSES.has(status)) {
1412
+ return invalid('status', status, 'active|retired|all');
1413
+ }
1414
+ const layer = facet('layer');
1415
+ const rules = (await adapter.listConformanceRules()).filter((r) => (severity === undefined || r.severity === severity) &&
1416
+ (ruleType === undefined || r.rule_type === ruleType) &&
1417
+ (layer === undefined || r.targets.layer === layer) &&
1418
+ // Absent `steering_type` reads as the engine's serde default (architecture) — a
1419
+ // steering engine always stamps it, but filter defensively over mixed-era rows.
1420
+ (steeringType === undefined || (r.steering_type ?? DEFAULT_STEERING_TYPE) === steeringType) &&
1421
+ // `retired` is absent on rows written before the field existed, which read as active.
1422
+ (status === 'all' || (status === 'retired') === (r.retired === true)));
1321
1423
  return { rules };
1322
1424
  });
1323
1425
  app.get(`${V}/governance/claims`, async () => {
@@ -1372,8 +1474,27 @@ runtime = {}) {
1372
1474
  return reply.code(repoReadStatus(msg)).send({ error: msg });
1373
1475
  }
1374
1476
  });
1375
- // ── Governance writes (crew#42) ────────────────────────────────────────────
1376
- app.post(`${V}/governance/policies`, async (req, reply) => {
1477
+ // ── Governance writes (crew#42; STEERING fold) ─────────────────────────────
1478
+ // The OLD policy WRITE surface, folded into the unified rules routes (STEERING program): on a
1479
+ // steering engine (wicked-core-ts ≥ 0.7.5, where policy rows have MIGRATED into steering
1480
+ // rules) these answer 410 Gone with a pointer at the rules CRUD — the wire contract's honest
1481
+ // spelling of "this resource model no longer exists" (a silent alias would accept a write into
1482
+ // a store decide()/select() no longer read). On a PRE-steering engine the legacy behavior
1483
+ // stands untouched: folding before the model merges would strand policy writes with no unified
1484
+ // store to land in.
1485
+ //
1486
+ // The READ (`GET /governance/policies` above) deliberately stays: retired-not-deleted means
1487
+ // past decisions citing a policy id must stay resolvable, the engine keeps a read shim for
1488
+ // exactly that, and the released studio 0.4.2 still renders the listing.
1489
+ const policyWriteGone = (reply, pointer) => reply.code(410).send({
1490
+ error: 'the policy model merged into steering rules (STEERING program): policies are now ' +
1491
+ `steering rules with effect/trigger/obligations/criteria as first-class fields — use ${pointer}`,
1492
+ see: pointer,
1493
+ });
1494
+ app.post(`${V}/governance/policies`, { config: { manifest: { statusCodes: [200, 400, 410] } } }, async (req, reply) => {
1495
+ if (adapter.steeringSupported()) {
1496
+ return policyWriteGone(reply, `POST ${V}/governance/rules`);
1497
+ }
1377
1498
  try {
1378
1499
  const policy = req.body;
1379
1500
  await adapter.upsertPolicy(policy);
@@ -1384,9 +1505,44 @@ runtime = {}) {
1384
1505
  return reply.code(400).send({ error: message(err) });
1385
1506
  }
1386
1507
  });
1387
- app.post(`${V}/governance/rules`, async (req, reply) => {
1508
+ // The steering fields a rule write may carry — the merged model's additions (STEERING). On an
1509
+ // engine that predates the model, `upsertConformanceRule` would ACCEPT the write and silently
1510
+ // drop every one of them (ConformanceRule has no `deny_unknown_fields`), persisting a rule
1511
+ // that recalls and enforces differently than the caller wrote — so their presence is gated
1512
+ // loudly (the extraWriteRoots doctrine): 501, upgrade the engine. A write WITHOUT them is the
1513
+ // legacy request and passes through on any engine.
1514
+ const STEERING_RULE_FIELDS = [
1515
+ 'steering_type',
1516
+ 'applies_to',
1517
+ 'excludes',
1518
+ 'weight',
1519
+ 'effect',
1520
+ 'trigger',
1521
+ 'obligations',
1522
+ 'criteria',
1523
+ ];
1524
+ app.post(`${V}/governance/rules`, {
1525
+ config: {
1526
+ manifest: {
1527
+ requestType: 'ConformanceRule',
1528
+ // 501: the body carries steering fields but the installed engine predates the
1529
+ // steering model (wicked-core-ts < 0.7.5) and would silently drop them.
1530
+ statusCodes: [200, 400, 501],
1531
+ },
1532
+ },
1533
+ }, async (req, reply) => {
1388
1534
  try {
1389
1535
  const rule = req.body;
1536
+ if (!adapter.steeringSupported() && rule !== null && typeof rule === 'object') {
1537
+ const carried = STEERING_RULE_FIELDS.filter((f) => rule[f] !== undefined);
1538
+ if (carried.length > 0) {
1539
+ return reply.code(501).send({
1540
+ error: `${new SteeringUnsupportedError('Writing steering rule fields').message} — ` +
1541
+ `the installed engine would silently drop ${carried.map((f) => `\`${f}\``).join(', ')} ` +
1542
+ 'and persist a rule that enforces differently than you wrote',
1543
+ });
1544
+ }
1545
+ }
1390
1546
  await adapter.upsertConformanceRule(rule);
1391
1547
  audit.record('governance.rule.upserted', actorOf(req), { detail: { id: rule?.id } });
1392
1548
  return { status: 'ok' };
@@ -1397,8 +1553,13 @@ runtime = {}) {
1397
1553
  });
1398
1554
  // Retire, not delete. The record survives so past decisions citing it stay explicable; it just
1399
1555
  // stops being enforced (FINDING-038 — a mis-authored policy otherwise denied forever).
1400
- app.delete(`${V}/governance/policies/:id`, async (req, reply) => {
1556
+ app.delete(`${V}/governance/policies/:id`, { config: { manifest: { statusCodes: [200, 400, 404, 410] } } }, async (req, reply) => {
1401
1557
  const { id } = req.params;
1558
+ // Folded like the POST above: on a steering engine the migrated row retires through the
1559
+ // rules kill switch (same id — migration keeps ids stable so decision audits resolve).
1560
+ if (adapter.steeringSupported()) {
1561
+ return policyWriteGone(reply, `DELETE ${V}/governance/rules/${id}`);
1562
+ }
1402
1563
  try {
1403
1564
  const existed = await adapter.retirePolicy(id);
1404
1565
  if (!existed)
@@ -1921,6 +2082,31 @@ runtime = {}) {
1921
2082
  });
1922
2083
  // ── Projects (DES-PROJECT-001) — the 9-route experience-plane surface ────────
1923
2084
  registerProjectRoutes(app, adapter, { ...projects, settings: projectSettings }, security);
2085
+ // ── Campaigns (crew#342 + TH-9) — the engine's durable Run-DAG scheduler over REST ──────────
2086
+ // Progress streams as campaign* CoreEvents on the existing allowlist-free /ws relay; these
2087
+ // routes are launch/resume/cancel + the reads a scoreboard builds from.
2088
+ registerCampaignRoutes(app, adapter, {
2089
+ audit,
2090
+ actorOf,
2091
+ roster: () => CoreAdapter.roster(),
2092
+ });
2093
+ // ── Governance wiki management (wiki-mgmt) — scoreboard + honest empty-state meta ──────────
2094
+ // Browse (`GET /governance/rules` + facets) and retire (`DELETE /governance/rules/:id`) stay
2095
+ // ABOVE with the other governance routes; this registers only the wiki-specific reads.
2096
+ // Both wiki reads feed the Steering page header too — they stay, unchanged (STEERING).
2097
+ registerGovernanceWikiRoutes(app, adapter);
2098
+ // ── Steering management (STEERING program) — batch import + "add with chat" authoring ──────
2099
+ // Per-rule CRUD stays ABOVE with the other governance routes (a steering rule IS a
2100
+ // conformance rule); this registers only the import batch and the governed authoring launch.
2101
+ registerGovernanceSteeringRoutes(app, adapter, {
2102
+ audit,
2103
+ actorOf,
2104
+ roster: () => CoreAdapter.roster(),
2105
+ });
2106
+ // ── Testing (crew-testing) — governance evals + eval corpora ────────────────
2107
+ // Run the evals (does the steering corpus catch what it claims to?) and import a named eval
2108
+ // corpus. Both presence-gated on the engine's evals bindings (501 on wicked-core-ts < 0.7.5).
2109
+ registerTestingRoutes(app, adapter, { audit, actorOf });
1924
2110
  // ── The wicked-interactive bridge, reverse-proxied (DES-MERGE-001 §5.3/§7.2) ──
1925
2111
  // Mounted BESIDE the routes above and under the same `${V}` prefix, so it inherits one
1926
2112
  // origin, one auth hook, and one CORS posture — the whole point of slice 1.