wicked-crew 0.7.46 → 0.7.47
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/capture.d.ts +2 -2
- package/dist/api/delivery-index.d.ts +21 -0
- package/dist/api/delivery-index.d.ts.map +1 -1
- package/dist/api/delivery-index.js +36 -0
- package/dist/api/delivery-index.js.map +1 -1
- package/dist/api/elicitation-cache.d.ts +12 -0
- package/dist/api/elicitation-cache.d.ts.map +1 -1
- package/dist/api/elicitation-cache.js +17 -1
- package/dist/api/elicitation-cache.js.map +1 -1
- package/dist/api/eval-sample.d.ts +6 -6
- package/dist/api/governance-steering.d.ts.map +1 -1
- package/dist/api/governance-steering.js +1 -0
- package/dist/api/governance-steering.js.map +1 -1
- package/dist/api/mcp.d.ts +4 -4
- package/dist/api/policy-landing.d.ts +55 -0
- package/dist/api/policy-landing.d.ts.map +1 -0
- package/dist/api/policy-landing.js +174 -0
- package/dist/api/policy-landing.js.map +1 -0
- package/dist/api/recording.d.ts +228 -0
- package/dist/api/recording.d.ts.map +1 -0
- package/dist/api/recording.js +1140 -0
- package/dist/api/recording.js.map +1 -0
- package/dist/api/routes.d.ts +31 -39
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +264 -104
- package/dist/api/routes.js.map +1 -1
- package/dist/api/run-timing-index.d.ts +17 -0
- package/dist/api/run-timing-index.d.ts.map +1 -1
- package/dist/api/run-timing-index.js +43 -1
- package/dist/api/run-timing-index.js.map +1 -1
- package/dist/api/server.d.ts +12 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +74 -1
- package/dist/api/server.js.map +1 -1
- package/dist/api/testing.d.ts +4 -4
- package/dist/api/testing.d.ts.map +1 -1
- package/dist/api/testing.js +3 -0
- package/dist/api/testing.js.map +1 -1
- package/dist/api/watch-routes.d.ts +30 -0
- package/dist/api/watch-routes.d.ts.map +1 -0
- package/dist/api/watch-routes.js +104 -0
- package/dist/api/watch-routes.js.map +1 -0
- package/dist/campaigns/routes.d.ts +2 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +4 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/core/adapter.d.ts +36 -0
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +177 -5
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/run-identity.js +1 -1
- package/dist/core/run-identity.js.map +1 -1
- package/dist/core/walkthrough-root.d.ts +21 -0
- package/dist/core/walkthrough-root.d.ts.map +1 -0
- package/dist/core/walkthrough-root.js +66 -0
- package/dist/core/walkthrough-root.js.map +1 -0
- package/dist/decisions/derive.d.ts +79 -0
- package/dist/decisions/derive.d.ts.map +1 -0
- package/dist/decisions/derive.js +282 -0
- package/dist/decisions/derive.js.map +1 -0
- package/dist/decisions/events.d.ts +23 -0
- package/dist/decisions/events.d.ts.map +1 -0
- package/dist/decisions/events.js +22 -0
- package/dist/decisions/events.js.map +1 -0
- package/dist/decisions/ingest.d.ts +28 -0
- package/dist/decisions/ingest.d.ts.map +1 -0
- package/dist/decisions/ingest.js +92 -0
- package/dist/decisions/ingest.js.map +1 -0
- package/dist/decisions/land.d.ts +100 -0
- package/dist/decisions/land.d.ts.map +1 -0
- package/dist/decisions/land.js +466 -0
- package/dist/decisions/land.js.map +1 -0
- package/dist/decisions/ledger.d.ts +60 -0
- package/dist/decisions/ledger.d.ts.map +1 -0
- package/dist/decisions/ledger.js +246 -0
- package/dist/decisions/ledger.js.map +1 -0
- package/dist/decisions/routes.d.ts +10 -0
- package/dist/decisions/routes.d.ts.map +1 -0
- package/dist/decisions/routes.js +141 -0
- package/dist/decisions/routes.js.map +1 -0
- package/dist/decisions/types.d.ts +120 -0
- package/dist/decisions/types.d.ts.map +1 -0
- package/dist/decisions/types.js +19 -0
- package/dist/decisions/types.js.map +1 -0
- package/dist/interactive/draft-events.d.ts.map +1 -1
- package/dist/interactive/draft-events.js +1 -0
- package/dist/interactive/draft-events.js.map +1 -1
- package/dist/projects/events.d.ts.map +1 -1
- package/dist/projects/events.js +2 -1
- package/dist/projects/events.js.map +1 -1
- package/dist/projects/state-home-preflight.d.ts.map +1 -1
- package/dist/projects/state-home-preflight.js +3 -0
- package/dist/projects/state-home-preflight.js.map +1 -1
- package/dist/projects/state-home-registry.d.ts.map +1 -1
- package/dist/projects/state-home-registry.js +2 -0
- package/dist/projects/state-home-registry.js.map +1 -1
- package/dist/projects/state-home.d.ts +6 -0
- package/dist/projects/state-home.d.ts.map +1 -1
- package/dist/projects/state-home.js +28 -1
- package/dist/projects/state-home.js.map +1 -1
- package/dist/qe/acceptance.d.ts +45 -4
- package/dist/qe/acceptance.d.ts.map +1 -1
- package/dist/qe/acceptance.js +93 -4
- package/dist/qe/acceptance.js.map +1 -1
- package/dist/qe/ledger.d.ts +20 -0
- package/dist/qe/ledger.d.ts.map +1 -1
- package/dist/qe/ledger.js +34 -0
- package/dist/qe/ledger.js.map +1 -1
- package/dist/qe/walkthrough-acceptance.d.ts +82 -0
- package/dist/qe/walkthrough-acceptance.d.ts.map +1 -0
- package/dist/qe/walkthrough-acceptance.js +248 -0
- package/dist/qe/walkthrough-acceptance.js.map +1 -0
- package/dist/skills/base-skill.d.ts.map +1 -1
- package/dist/skills/base-skill.js +3 -2
- package/dist/skills/base-skill.js.map +1 -1
- package/dist/skills/plugin-source.d.ts +19 -1
- package/dist/skills/plugin-source.d.ts.map +1 -1
- package/dist/skills/plugin-source.js +31 -2
- package/dist/skills/plugin-source.js.map +1 -1
- package/dist/skills/runtime.d.ts +11 -2
- package/dist/skills/runtime.d.ts.map +1 -1
- package/dist/skills/runtime.js +29 -1
- package/dist/skills/runtime.js.map +1 -1
- package/dist/skills/store.d.ts +14 -1
- package/dist/skills/store.d.ts.map +1 -1
- package/dist/skills/store.js +19 -5
- package/dist/skills/store.js.map +1 -1
- package/dist/standing-orders/routes.d.ts +4 -4
- package/dist/standing-orders/store.d.ts +2 -2
- package/dist/watch/checks/check-failed.d.ts +24 -0
- package/dist/watch/checks/check-failed.d.ts.map +1 -0
- package/dist/watch/checks/check-failed.js +56 -0
- package/dist/watch/checks/check-failed.js.map +1 -0
- package/dist/watch/checks/deliver-audit.d.ts +19 -0
- package/dist/watch/checks/deliver-audit.d.ts.map +1 -0
- package/dist/watch/checks/deliver-audit.js +129 -0
- package/dist/watch/checks/deliver-audit.js.map +1 -0
- package/dist/watch/checks/index.d.ts +8 -0
- package/dist/watch/checks/index.d.ts.map +1 -0
- package/dist/watch/checks/index.js +19 -0
- package/dist/watch/checks/index.js.map +1 -0
- package/dist/watch/checks/lag.d.ts +24 -0
- package/dist/watch/checks/lag.d.ts.map +1 -0
- package/dist/watch/checks/lag.js +55 -0
- package/dist/watch/checks/lag.js.map +1 -0
- package/dist/watch/checks/quiet-after-claim.d.ts +17 -0
- package/dist/watch/checks/quiet-after-claim.d.ts.map +1 -0
- package/dist/watch/checks/quiet-after-claim.js +89 -0
- package/dist/watch/checks/quiet-after-claim.js.map +1 -0
- package/dist/watch/checks/ungated.d.ts +12 -0
- package/dist/watch/checks/ungated.d.ts.map +1 -0
- package/dist/watch/checks/ungated.js +64 -0
- package/dist/watch/checks/ungated.js.map +1 -0
- package/dist/watch/emit.d.ts +122 -0
- package/dist/watch/emit.d.ts.map +1 -0
- package/dist/watch/emit.js +443 -0
- package/dist/watch/emit.js.map +1 -0
- package/dist/watch/events.d.ts +21 -0
- package/dist/watch/events.d.ts.map +1 -0
- package/dist/watch/events.js +21 -0
- package/dist/watch/events.js.map +1 -0
- package/dist/watch/keys.d.ts +15 -0
- package/dist/watch/keys.d.ts.map +1 -0
- package/dist/watch/keys.js +23 -0
- package/dist/watch/keys.js.map +1 -0
- package/dist/watch/lanes.d.ts +41 -0
- package/dist/watch/lanes.d.ts.map +1 -0
- package/dist/watch/lanes.js +73 -0
- package/dist/watch/lanes.js.map +1 -0
- package/dist/watch/loader.d.ts +47 -0
- package/dist/watch/loader.d.ts.map +1 -0
- package/dist/watch/loader.js +178 -0
- package/dist/watch/loader.js.map +1 -0
- package/dist/watch/registry.d.ts +137 -0
- package/dist/watch/registry.d.ts.map +1 -0
- package/dist/watch/registry.js +530 -0
- package/dist/watch/registry.js.map +1 -0
- package/dist/watch/ring.d.ts +32 -0
- package/dist/watch/ring.d.ts.map +1 -0
- package/dist/watch/ring.js +90 -0
- package/dist/watch/ring.js.map +1 -0
- package/dist/watch/router.d.ts +25 -0
- package/dist/watch/router.d.ts.map +1 -0
- package/dist/watch/router.js +87 -0
- package/dist/watch/router.js.map +1 -0
- package/dist/watch/sources.d.ts +42 -0
- package/dist/watch/sources.d.ts.map +1 -0
- package/dist/watch/sources.js +104 -0
- package/dist/watch/sources.js.map +1 -0
- package/dist/watch/types.d.ts +110 -0
- package/dist/watch/types.d.ts.map +1 -0
- package/dist/watch/types.js +6 -0
- package/dist/watch/types.js.map +1 -0
- package/endpoint-manifest.json +178 -1
- package/package.json +3 -2
- package/watch/entries/deliver-audit.json +17 -0
- package/watch/entries/quiet-after-claim.json +16 -0
- package/watch/entries/registry-check-failed.json +12 -0
- package/watch/entries/registry-lagging.json +12 -0
- package/watch/entries/ungated.json +13 -0
- package/dist/api/demo.d.ts +0 -90
- package/dist/api/demo.d.ts.map +0 -1
- package/dist/api/demo.js +0 -492
- package/dist/api/demo.js.map +0 -1
package/dist/api/routes.js
CHANGED
|
@@ -38,7 +38,7 @@ import { DEFAULT_STEERING_TYPE, STEERING_TYPE_VALUES, STEERING_TYPES, registerGo
|
|
|
38
38
|
import { isSteeringAuthorRun, landSteeringProposal } from './steering-landing.js';
|
|
39
39
|
import { ApproveEditSchema, approveEdited, captureLaunchRoots, editInFlight, FileProposalSchema, registerCaptureRoutes } from './capture.js';
|
|
40
40
|
import { registerTestingRoutes } from './testing.js';
|
|
41
|
-
import { DEMO_PRESET, demoLaunchRoots, registerDemoRoutes } from './
|
|
41
|
+
import { DEMO_PRESET, demoLaunchRoots, registerDemoRoutes, registerWalkthroughRoutes, walkthroughAcceptance } from './recording.js';
|
|
42
42
|
import { registerSkillsRoutes } from './skills.js';
|
|
43
43
|
import { registerMcpRoutes } from './mcp.js';
|
|
44
44
|
import { McpPolicies } from '../mcp/policies.js';
|
|
@@ -46,8 +46,10 @@ import { disabledSkillsHealth } from '../skills/runtime.js';
|
|
|
46
46
|
import { phaseSkillFindings, withPhaseSkillGaps } from '../skills/phase-skill-gaps.js';
|
|
47
47
|
import { STATE_HOME_BLOCKER_CODE, STATE_HOME_REMEDY, isEngineStateHomeRefusal, stateHomeBlockerBody, } from '../projects/state-home-preflight.js';
|
|
48
48
|
import { BASE_SKILL_POLICIES, BASE_SKILL_REF_SHAPE, baseSkillRemedy } from '../skills/base-skill.js';
|
|
49
|
+
import { REQUIRED_GARDEN_VERSION } from '../skills/plugin-source.js';
|
|
49
50
|
import { noEligibleSeatBody, parseNoEligibleSeat } from '../core/engine-roster.js';
|
|
50
51
|
import { ProjectSettingsStore } from '../projects/settings.js';
|
|
52
|
+
import { isHumanOperator } from './watch-routes.js';
|
|
51
53
|
import { boundOrigin, InteractiveBridgePool } from '../interactive/bridge-pool.js';
|
|
52
54
|
import { composeDeliverText, configuredPublicOrigin, extractFollowUps, factsFromRun, FOLLOW_UPS_MAX, framedDeliverText, runUrlFor, } from '../core/deliver-text.js';
|
|
53
55
|
import { deliverRepoFor, deliverTargetView, isGitHubLogin, readDeliverOriginUrl, resolvePullRequest as resolvePullRequestViaGh, } from '../core/deliver.js';
|
|
@@ -69,7 +71,7 @@ import { DeliveryFreeze, frozenRefusal, isDeliverUnit, registerDeliveryFreezeRou
|
|
|
69
71
|
import { GroupIndex } from './group-index.js';
|
|
70
72
|
import { RunTimingIndex, recordRunLaunched } from './run-timing-index.js';
|
|
71
73
|
import { GuidanceIndex } from './guidance-index.js';
|
|
72
|
-
import { DeliveryIndex, gitRunBranchIsEmpty, gitWorktreeIsClean, isDeliverConflictStranded, deliveryRecordFrom, pushedState, canDeliverResolver, } from './delivery-index.js';
|
|
74
|
+
import { DELIVER_REQUESTED_ACTION, DeliveryIndex, gitRunBranchIsEmpty, gitWorktreeIsClean, isDeliverConflictStranded, deliveryRecordFrom, pushedState, canDeliverResolver, FREE_TEXT_NOTICE, freeTextOutcome, } from './delivery-index.js';
|
|
73
75
|
import { DeliveryDerivationCache } from './delivery-cache.js';
|
|
74
76
|
import { gitReprovisionWorktree, runDeliverScript, } from './post-hoc-deliver.js';
|
|
75
77
|
import { LOCAL_ACTOR } from './auth.js';
|
|
@@ -77,6 +79,11 @@ import { LOCAL_ACTOR } from './auth.js';
|
|
|
77
79
|
// value lives in the leaf module api-prefix.ts to keep unit-output.ts out of this file's cycle.
|
|
78
80
|
export { API_PREFIX } from './api-prefix.js';
|
|
79
81
|
import { API_PREFIX } from './api-prefix.js';
|
|
82
|
+
import { landPolicyRule, policyProposalToRule } from './policy-landing.js';
|
|
83
|
+
import { DecisionError, DecisionService } from '../decisions/land.js';
|
|
84
|
+
import { ingestDecision } from '../decisions/ingest.js';
|
|
85
|
+
import { registerDecisionRoutes } from '../decisions/routes.js';
|
|
86
|
+
import { resolveDecisionsMode } from '../decisions/types.js';
|
|
80
87
|
const V = API_PREFIX;
|
|
81
88
|
/** How long `GET /health` waits on the engine's actor before answering `ping: "busy"` (crew#471). */
|
|
82
89
|
export const HEALTH_PING_WAIT_MS = 1_000;
|
|
@@ -185,80 +192,8 @@ function isStringRecord(v) {
|
|
|
185
192
|
!Array.isArray(v) &&
|
|
186
193
|
Object.values(v).every((x) => typeof x === 'string'));
|
|
187
194
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
* ConformanceRule (DES-MEM-FACETED-001 §5.2). A policy proposal's `kind_type` is
|
|
191
|
-
* `policy:<steering_type>` and its `payload` is `{ rule, severity }`; estate writes NOTHING for it
|
|
192
|
-
* (the AW-11 "no rules.write on estate" invariant) and hands the payload back for crew — the ONE
|
|
193
|
-
* governed rules-write path — to land. Returns the rule to upsert plus the resolved steering type,
|
|
194
|
-
* or a loud `error` string when the proposal cannot be shaped into a valid rule (a malformed
|
|
195
|
-
* `kind_type` / missing `rule` / out-of-enum `severity`) — the caller reports it as a failed
|
|
196
|
-
* landing, never a silent drop (the crew#388 anti-silent-loss doctrine).
|
|
197
|
-
*
|
|
198
|
-
* The rule id is DETERMINISTIC (`proposal:<id>`) so a re-driven landing UPSERTS the same rule
|
|
199
|
-
* (idempotent) instead of minting a duplicate; it sits OUTSIDE the reserved `PAT-/POL-` namespace,
|
|
200
|
-
* which UI/chat/proposal-authored rules are free to do (INV-C1). The rule carries NO `effect`
|
|
201
|
-
* (recall-only — exactly what `{rule, severity}` supports: an enforcement rule would need a
|
|
202
|
-
* non-blank `applies_to`, which the payload does not carry, and INV-S3 fails such a rule closed).
|
|
203
|
-
* `steering_type` is validated against the vocabulary here so the engine's INV-S1 never rejects it
|
|
204
|
-
* as an unknown page.
|
|
205
|
-
*/
|
|
206
|
-
export function policyProposalToRule(proposalId, kindType, payload, facets) {
|
|
207
|
-
const steeringType = kindType.startsWith('policy:') ? kindType.slice('policy:'.length) : '';
|
|
208
|
-
if (!STEERING_TYPES.has(steeringType)) {
|
|
209
|
-
return {
|
|
210
|
-
error: `the approved proposal's kind_type ${JSON.stringify(kindType)} does not name a steering ` +
|
|
211
|
-
`type — expected \`policy:<${STEERING_TYPE_VALUES.join('|')}>\``,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
const body = typeof payload === 'object' && payload !== null && !Array.isArray(payload)
|
|
215
|
-
? payload
|
|
216
|
-
: {};
|
|
217
|
-
const statement = typeof body['rule'] === 'string' ? body['rule'].trim() : '';
|
|
218
|
-
if (statement === '') {
|
|
219
|
-
return {
|
|
220
|
-
error: 'the approved policy proposal payload has no `rule` string to make a rule statement from',
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
const rawSeverity = body['severity'];
|
|
224
|
-
// Normalize common LLM drift before the enum check: a model naturally writes the English word
|
|
225
|
-
// "warning", but the engine's enum is the short "warn" (info/error/critical are already the
|
|
226
|
-
// natural words). Tolerate case/whitespace too. A capture worker that says "warning" must still
|
|
227
|
-
// land its policy — otherwise every derived policy at the middle band silently fails to land.
|
|
228
|
-
const normSeverity = typeof rawSeverity === 'string'
|
|
229
|
-
? (() => {
|
|
230
|
-
const s = rawSeverity.trim().toLowerCase();
|
|
231
|
-
return s === 'warning' ? 'warn' : s;
|
|
232
|
-
})()
|
|
233
|
-
: rawSeverity;
|
|
234
|
-
const severity = normSeverity === 'info' || normSeverity === 'warn' || normSeverity === 'error' || normSeverity === 'critical'
|
|
235
|
-
? normSeverity
|
|
236
|
-
: undefined;
|
|
237
|
-
if (rawSeverity !== undefined && severity === undefined) {
|
|
238
|
-
return {
|
|
239
|
-
error: `the approved policy proposal payload has an invalid severity ${JSON.stringify(rawSeverity)} — ` +
|
|
240
|
-
'expected info|warn|error|critical (or the natural "warning" for warn)',
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
const language = facets?.['language'];
|
|
244
|
-
const rule = {
|
|
245
|
-
id: `proposal:${proposalId}`,
|
|
246
|
-
rule_type: 'policy',
|
|
247
|
-
statement,
|
|
248
|
-
// A policy proposal SHOULD carry severity; a missing one defaults to `warn` (the middle band)
|
|
249
|
-
// rather than failing the landing, but a present-but-garbage one fails loud above.
|
|
250
|
-
severity: severity ?? 'warn',
|
|
251
|
-
// REQUIRED engine-side (f32, no serde default, INV-C2 `[0,1]`); the payload carries none, so a
|
|
252
|
-
// fixed authority — the same 0.8 the steering-author landing defaults to.
|
|
253
|
-
confidence: 0.8,
|
|
254
|
-
// Only the `language` facet maps onto the engine's Targets facet object; `repo`/`project`
|
|
255
|
-
// facets have no ConformanceRule slot and are dropped (see the route's open question).
|
|
256
|
-
targets: typeof language === 'string' && language !== '' ? { language } : {},
|
|
257
|
-
provenance: { source: 'proposal', source_kinds: [] },
|
|
258
|
-
steering_type: steeringType,
|
|
259
|
-
};
|
|
260
|
-
return { rule, steeringType: steeringType };
|
|
261
|
-
}
|
|
195
|
+
// `policyProposalToRule` lives beside THE landing (policy-landing.ts); re-exported for its callers.
|
|
196
|
+
export { policyProposalToRule } from './policy-landing.js';
|
|
262
197
|
/** A JSON object whose every value is a number (a coverage breakdown map). */
|
|
263
198
|
function isNumberRecord(v) {
|
|
264
199
|
return (typeof v === 'object' &&
|
|
@@ -741,6 +676,25 @@ runtime = {}) {
|
|
|
741
676
|
// default is the real spawn-per-call `wicked-estate-mcp` client; route tests inject a stub so no
|
|
742
677
|
// process is ever spawned.
|
|
743
678
|
const estateTool = runtime.callEstateTool ?? callEstateTool;
|
|
679
|
+
// DC-S4a: decision capture over the structured hosts (gate note, elicitation, inject). Words are
|
|
680
|
+
// recorded post-commit, off the request path, and only from a human actor (decisions/ingest.ts).
|
|
681
|
+
const decisions = runtime.decisionLedger !== undefined
|
|
682
|
+
? new DecisionService({
|
|
683
|
+
ledger: runtime.decisionLedger,
|
|
684
|
+
mode: runtime.decisionsMode ?? resolveDecisionsMode(),
|
|
685
|
+
authMode: security.authMode,
|
|
686
|
+
estateTool,
|
|
687
|
+
adapter,
|
|
688
|
+
audit,
|
|
689
|
+
emit: runtime.decisionsEmit ?? null,
|
|
690
|
+
...(runtime.broadcast !== undefined
|
|
691
|
+
? { broadcast: (frame) => runtime.broadcast?.(frame) }
|
|
692
|
+
: {}),
|
|
693
|
+
log: (m) => app.log.warn(m),
|
|
694
|
+
})
|
|
695
|
+
: null;
|
|
696
|
+
if (decisions !== null)
|
|
697
|
+
registerDecisionRoutes(app, decisions);
|
|
744
698
|
/** Repo root for a repo ref, from the registry — shared by the reprovision path below. */
|
|
745
699
|
const repoRootOf = async (repoRef) => (await adapter.listRepos()).find((r) => r.id === repoRef)?.root_path;
|
|
746
700
|
/** DES-L9: PR number → head branch, through `gh pr view` unless the runtime injected an answerer. */
|
|
@@ -875,6 +829,20 @@ runtime = {}) {
|
|
|
875
829
|
view.session.launch_actor = launchActor;
|
|
876
830
|
const state = resolveDelivery(view, conflictStrand);
|
|
877
831
|
view.session.delivery = state.delivery;
|
|
832
|
+
// crew#762: was delivery asked for? The launch's decision (`deliver: 'pr'`), a post-hoc attempt
|
|
833
|
+
// or a delivery on record says yes; a launch that said `deliver: 'none'` with neither says no.
|
|
834
|
+
// ABSENT when the daemon holds no launch record and no attempt (never fabricated).
|
|
835
|
+
const launchDeliver = runTimingIndex.launchDeliverFor(view.session.id);
|
|
836
|
+
if (launchDeliver === 'pr' || deliveryIndex.wasRequested(view.session.id) || deliveryIndex.isDelivered(view.session.id)) {
|
|
837
|
+
view.session.deliver_requested = true;
|
|
838
|
+
}
|
|
839
|
+
else if (launchDeliver === 'none') {
|
|
840
|
+
view.session.deliver_requested = false;
|
|
841
|
+
}
|
|
842
|
+
// crew#755: a completed repo-less free-text run answered in text and changed nothing — say so.
|
|
843
|
+
const outcome = freeTextOutcome(view);
|
|
844
|
+
if (outcome !== undefined)
|
|
845
|
+
view.session.outcome = outcome;
|
|
878
846
|
if (state.deliverUrl !== undefined)
|
|
879
847
|
view.session.deliverUrl = state.deliverUrl;
|
|
880
848
|
if (state.deliverBranch !== undefined)
|
|
@@ -888,6 +856,10 @@ runtime = {}) {
|
|
|
888
856
|
const chatGrounded = runTimingIndex.chatGroundedFor(view.session.id);
|
|
889
857
|
if (chatGrounded !== undefined)
|
|
890
858
|
view.session.chat_grounded = chatGrounded;
|
|
859
|
+
// C1: the chat the run was launched from, on every run (terminal ones too) — ABSENT otherwise.
|
|
860
|
+
const chatId = runTimingIndex.chatIdFor(view.session.id);
|
|
861
|
+
if (chatId !== undefined)
|
|
862
|
+
view.session.chat_id = chatId;
|
|
891
863
|
return view;
|
|
892
864
|
};
|
|
893
865
|
// Resolved ONCE and shared by the project routes (which read/write `interactiveRoot`) and the
|
|
@@ -898,6 +870,17 @@ runtime = {}) {
|
|
|
898
870
|
// driving this function directly (tests) has no hooks, so downstream code
|
|
899
871
|
// still gets the ONE actor shape via this accessor.
|
|
900
872
|
const actorOf = (req) => req.actor ?? LOCAL_ACTOR;
|
|
873
|
+
// crew#753: the blocking `skills.garden` finding (an installed garden older than the minimum), or
|
|
874
|
+
// `null`. Read from the skills runtime's cached health; no seam ⇒ nothing to judge.
|
|
875
|
+
const gardenBlocker = () => {
|
|
876
|
+
const f = runtime.skills?.health().findings.find((x) => x.kind === 'skills.garden' && x.severity === 'error');
|
|
877
|
+
return f === undefined ? null : { kind: f.kind, severity: f.severity, message: f.message };
|
|
878
|
+
};
|
|
879
|
+
/** The typed 422 a launch or onboard answers while {@link gardenBlocker} holds. */
|
|
880
|
+
const gardenRefusalBody = () => {
|
|
881
|
+
const f = gardenBlocker();
|
|
882
|
+
return f === null ? null : { code: 'garden_required', error: f.message, required: REQUIRED_GARDEN_VERSION };
|
|
883
|
+
};
|
|
901
884
|
// Liveness — also proves the actor + event pump are up.
|
|
902
885
|
//
|
|
903
886
|
// crew#471: the engine's ping is a round trip through its single-writer actor, and a launch that
|
|
@@ -927,9 +910,15 @@ runtime = {}) {
|
|
|
927
910
|
// F-E2E-030: what this deployment can keep. A composer reads `capabilities.deliverGate`
|
|
928
911
|
// before it promises "pauses at the deliver gate"; a stub-driven route set without the
|
|
929
912
|
// probe honestly reports no gate.
|
|
930
|
-
const capabilities =
|
|
931
|
-
|
|
932
|
-
|
|
913
|
+
const capabilities = {
|
|
914
|
+
...(typeof adapter.engineCapabilities === 'function'
|
|
915
|
+
? adapter.engineCapabilities()
|
|
916
|
+
: { deliverGate: false, revisesPr: false, chatIdOnLaunch: false, seatChipOnCreate: false }),
|
|
917
|
+
// C1: `AgentSession.chat_id` is served from the daemon's own launch index, whatever the engine.
|
|
918
|
+
runChatId: true,
|
|
919
|
+
// WT-W1: repo-bound launches mint an evidence root only on an addon that takes the field.
|
|
920
|
+
walkthroughRoots: typeof adapter.evidenceRootsSupported === 'function' ? adapter.evidenceRootsSupported() : false,
|
|
921
|
+
};
|
|
933
922
|
// wicked-core#411 / crew#497: the state-home blocker rides the health probe as a WARNING. The
|
|
934
923
|
// daemon still SERVES (status stays ok — studio must load and show the blocker) but refuses to
|
|
935
924
|
// launch while the state home holds an entry the worker Read fence cannot classify. Re-surveyed
|
|
@@ -950,6 +939,8 @@ runtime = {}) {
|
|
|
950
939
|
const warnings = [
|
|
951
940
|
...(stateHome === null ? [] : stateHome.findings.map((f) => ({ kind: f.kind, severity: f.severity, message: f.message }))),
|
|
952
941
|
...(baseSkill?.finding ? [{ kind: baseSkill.finding.kind, severity: baseSkill.finding.severity, message: baseSkill.finding.message }] : []),
|
|
942
|
+
// crew#753: a garden older than the minimum is a blocking finding on the first screen too.
|
|
943
|
+
...[gardenBlocker()].filter((f) => f !== null),
|
|
953
944
|
...phaseSkillFindings(runtime.phaseSkills?.gaps() ?? []),
|
|
954
945
|
// DES-TEAMING-002 T0: the daemon's bus did not open at boot, so the engine was handed none
|
|
955
946
|
// and runs un-teamed. A daemon-level notice (status stays ok: it serves); the per-run
|
|
@@ -1369,11 +1360,39 @@ runtime = {}) {
|
|
|
1369
1360
|
return message(err);
|
|
1370
1361
|
}
|
|
1371
1362
|
};
|
|
1363
|
+
/**
|
|
1364
|
+
* crew#753: what refuses an onboarding launch, judged BEFORE anything is registered — so a
|
|
1365
|
+
* refused onboard leaves no repo row (core has no unregister). The same three intake refusals a
|
|
1366
|
+
* launch meets: a garden older than the minimum (422 `garden_required`), the state-home blocker
|
|
1367
|
+
* (409) and a base skill the handed generation lacks, e.g. no garden at all (422
|
|
1368
|
+
* `base_skill_refused`). `null` when the launch would be admitted.
|
|
1369
|
+
*/
|
|
1370
|
+
const onboardRefusal = async () => {
|
|
1371
|
+
const garden = gardenRefusalBody();
|
|
1372
|
+
if (garden !== null)
|
|
1373
|
+
return { status: 422, body: garden };
|
|
1374
|
+
if (runtime.stateHome !== undefined) {
|
|
1375
|
+
const stateHome = await runtime.stateHome.refresh();
|
|
1376
|
+
if (stateHome.refusesLaunches)
|
|
1377
|
+
return { status: 409, body: stateHomeBlockerBody(stateHome) };
|
|
1378
|
+
}
|
|
1379
|
+
const posture = runtime.skills?.baseSkill() ?? null;
|
|
1380
|
+
if (posture !== null && !posture.present && posture.finding !== null) {
|
|
1381
|
+
return {
|
|
1382
|
+
status: 422,
|
|
1383
|
+
body: { code: 'base_skill_refused', error: posture.finding.message, baseSkill: posture, remedy: baseSkillRemedy(posture.inCatalog) },
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
return null;
|
|
1387
|
+
};
|
|
1372
1388
|
app.post(`${V}/repos`, async (req, reply) => {
|
|
1373
1389
|
const parsed = RegisterRepoSchema.safeParse(req.body);
|
|
1374
1390
|
if (!parsed.success) {
|
|
1375
1391
|
return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
|
|
1376
1392
|
}
|
|
1393
|
+
const refused = await onboardRefusal();
|
|
1394
|
+
if (refused !== null)
|
|
1395
|
+
return reply.code(refused.status).send(refused.body);
|
|
1377
1396
|
const { name, rootPath, gitUrl, projectId, channel, actor } = parsed.data;
|
|
1378
1397
|
let repo;
|
|
1379
1398
|
let runId;
|
|
@@ -1416,6 +1435,9 @@ runtime = {}) {
|
|
|
1416
1435
|
const body = OnboardBodySchema.safeParse(req.body ?? {});
|
|
1417
1436
|
if (!body.success)
|
|
1418
1437
|
return reply.code(400).send(invalidBody(body.error, 'Invalid request body'));
|
|
1438
|
+
const refused = await onboardRefusal();
|
|
1439
|
+
if (refused !== null)
|
|
1440
|
+
return reply.code(refused.status).send(refused.body);
|
|
1419
1441
|
let runId;
|
|
1420
1442
|
try {
|
|
1421
1443
|
runId = await adapter.launchOnboardingRun(repo.id, repo.name);
|
|
@@ -1449,7 +1471,7 @@ runtime = {}) {
|
|
|
1449
1471
|
return reply.code(400).send(invalidBody(parsed.error, 'Invalid request body'));
|
|
1450
1472
|
}
|
|
1451
1473
|
const b = parsed.data;
|
|
1452
|
-
// The demo preset launches only from POST /projects/:id/demo (api/
|
|
1474
|
+
// The demo preset launches only from POST /projects/:id/demo (api/recording.ts), which mints the run's
|
|
1453
1475
|
// demo root and brief first: launched here directly it would have neither.
|
|
1454
1476
|
if (b.workflow === DEMO_PRESET && (b.sessionId === undefined || !demoLaunchRoots.has(b.sessionId))) {
|
|
1455
1477
|
return reply.code(400).send({
|
|
@@ -1468,6 +1490,10 @@ runtime = {}) {
|
|
|
1468
1490
|
return reply.code(409).send(stateHomeBlockerBody(stateHome));
|
|
1469
1491
|
}
|
|
1470
1492
|
}
|
|
1493
|
+
// crew#753: a garden older than the minimum refuses the launch by name (found vs required).
|
|
1494
|
+
const garden = gardenRefusalBody();
|
|
1495
|
+
if (garden !== null)
|
|
1496
|
+
return reply.code(422).send(garden);
|
|
1471
1497
|
// crew#645: an expired login reads signed out BEFORE routing — wait (bounded) for every seat's
|
|
1472
1498
|
// missing or stale login check, so the standing below is the seats' own answer.
|
|
1473
1499
|
if (b.clisJson === undefined)
|
|
@@ -1485,7 +1511,7 @@ runtime = {}) {
|
|
|
1485
1511
|
const captureRoot = captureLaunchRoots.get(input.sessionId);
|
|
1486
1512
|
if (captureRoot !== undefined)
|
|
1487
1513
|
input.extraWriteRoots = [captureRoot];
|
|
1488
|
-
// A demo launch (api/
|
|
1514
|
+
// A demo launch (api/recording.ts) writes every file into the demo root the demo route minted for it.
|
|
1489
1515
|
const demoRoot = demoLaunchRoots.get(input.sessionId);
|
|
1490
1516
|
if (demoRoot !== undefined)
|
|
1491
1517
|
input.extraWriteRoots = [demoRoot];
|
|
@@ -1730,7 +1756,11 @@ runtime = {}) {
|
|
|
1730
1756
|
// is always reproducible even if the chat is idle-reclaimed before the run finishes.
|
|
1731
1757
|
if (b.chatId !== undefined)
|
|
1732
1758
|
runtime.linkChatRun?.(b.chatId, runId);
|
|
1733
|
-
|
|
1759
|
+
// crew#755: a launch that named neither a workflow nor a plan runs the engine's free-text
|
|
1760
|
+
// planner (one unit, the brief verbatim). Say so in the answer, with how to get work done,
|
|
1761
|
+
// instead of letting it pass for a launch of real work.
|
|
1762
|
+
const freeText = input.workflow === undefined && input.plan === undefined ? { freeText: { notice: FREE_TEXT_NOTICE } } : {};
|
|
1763
|
+
return reply.code(201).send({ runId, ...(linkedIssues !== undefined ? { linkedIssues } : {}), ...freeText });
|
|
1734
1764
|
}
|
|
1735
1765
|
catch (err) {
|
|
1736
1766
|
const msg = message(err);
|
|
@@ -2024,6 +2054,10 @@ runtime = {}) {
|
|
|
2024
2054
|
});
|
|
2025
2055
|
}
|
|
2026
2056
|
deliverInFlight.add(id);
|
|
2057
|
+
// crew#762: the attempt is the durable fact "delivery was requested" — recorded BEFORE the
|
|
2058
|
+
// script runs, so a lift that then fails still reads requested after a reload or restart.
|
|
2059
|
+
audit.record(DELIVER_REQUESTED_ACTION, actorOf(req), { runId: id, detail: { via: 'post-hoc' } });
|
|
2060
|
+
deliveryIndex.markRequested(id);
|
|
2027
2061
|
// The worktree the script runs in. Usually the run's own; but the engine REAPS a
|
|
2028
2062
|
// failed-deliver run's worktree once its work is committed (crew#418/#432) — the work then lives
|
|
2029
2063
|
// on the `wicked/<id>` branch. For such a strand, stand a throwaway worktree back up from
|
|
@@ -2782,7 +2816,10 @@ runtime = {}) {
|
|
|
2782
2816
|
// catalog entry re-verifies evidence, a registered workflow's def, nothing for free text — and
|
|
2783
2817
|
// an unknown run (or one the daemon cannot read) is declared and denied with the reason.
|
|
2784
2818
|
const verifiedCatalog = typeof adapter.verifiedEvidenceCatalog === 'function' ? await adapter.verifiedEvidenceCatalog() : null;
|
|
2785
|
-
const
|
|
2819
|
+
const knownCatalog = typeof adapter.catalogIds === 'function' ? await adapter.catalogIds() : null;
|
|
2820
|
+
const requirement = acceptanceRequirementOf(run, adapter.listWorkflows(), verifiedCatalog, knownCatalog);
|
|
2821
|
+
// WT-W2: every walkthrough step the requirement names is resolved from its proof root and seal.
|
|
2822
|
+
const walkthroughs = await walkthroughAcceptance(adapter, run, requirement.phases);
|
|
2786
2823
|
let repo = null;
|
|
2787
2824
|
if (run.session.repo_ref !== null) {
|
|
2788
2825
|
const repos = await adapter.listRepos();
|
|
@@ -2799,6 +2836,7 @@ runtime = {}) {
|
|
|
2799
2836
|
// unit both appear on the page humans actually look at.
|
|
2800
2837
|
claims: () => adapter.listConformanceClaims(),
|
|
2801
2838
|
events: (rid) => adapter.runEvents(rid),
|
|
2839
|
+
...(walkthroughs.gates.length > 0 ? { walkthroughs } : {}),
|
|
2802
2840
|
});
|
|
2803
2841
|
});
|
|
2804
2842
|
/**
|
|
@@ -2920,6 +2958,19 @@ runtime = {}) {
|
|
|
2920
2958
|
status,
|
|
2921
2959
|
},
|
|
2922
2960
|
});
|
|
2961
|
+
// DC-S4a host: the gate note (amend / request_changes text) — a bare approve or reject is a
|
|
2962
|
+
// `choice`. Post-commit, off the request path; a non-human actor (a standing order) records nothing.
|
|
2963
|
+
if (decisions !== null) {
|
|
2964
|
+
decisions.track(ingestDecision(decisions, {
|
|
2965
|
+
host: 'gate',
|
|
2966
|
+
actor,
|
|
2967
|
+
words: parsed.data.amend ?? '',
|
|
2968
|
+
choice: parsed.data.action ?? (parsed.data.approve ? 'approve' : 'reject'),
|
|
2969
|
+
projectId: projects.index.projectOf(id) ?? null,
|
|
2970
|
+
runId: id,
|
|
2971
|
+
...(parsed.data.ord !== undefined ? { ord: parsed.data.ord } : {}),
|
|
2972
|
+
}));
|
|
2973
|
+
}
|
|
2923
2974
|
// APPROVE of the steering-author propose gate = the doctrine's landing moment: the
|
|
2924
2975
|
// approved proposal is written to the governance store with `provenance.source: "chat"`,
|
|
2925
2976
|
// audited per rule, idempotent on replay, and LOUD on failure — the response carries the
|
|
@@ -3046,6 +3097,16 @@ runtime = {}) {
|
|
|
3046
3097
|
try {
|
|
3047
3098
|
await adapter.injectWorkerMessage(id, parsed.data.message, parsed.data.target);
|
|
3048
3099
|
audit.record('run.injected', actorOf(req), { runId: id, detail: { target: parsed.data.target } });
|
|
3100
|
+
// DC-S4a host: the operator's mid-run steering text (review S9).
|
|
3101
|
+
if (decisions !== null) {
|
|
3102
|
+
decisions.track(ingestDecision(decisions, {
|
|
3103
|
+
host: 'inject',
|
|
3104
|
+
actor: actorOf(req),
|
|
3105
|
+
words: parsed.data.message,
|
|
3106
|
+
projectId: projects.index.projectOf(id) ?? null,
|
|
3107
|
+
runId: id,
|
|
3108
|
+
}));
|
|
3109
|
+
}
|
|
3049
3110
|
return reply.send({ status: 'ok' });
|
|
3050
3111
|
}
|
|
3051
3112
|
catch (err) {
|
|
@@ -3298,6 +3359,19 @@ runtime = {}) {
|
|
|
3298
3359
|
runId: id,
|
|
3299
3360
|
detail: { elicitationId: taken.entry.elicitationId, action: body.action },
|
|
3300
3361
|
});
|
|
3362
|
+
// DC-S4a host: a free-text answer is words; a picked option (or a decline) is a `choice`.
|
|
3363
|
+
if (decisions !== null) {
|
|
3364
|
+
const picked = taken.entry.options !== null;
|
|
3365
|
+
decisions.track(ingestDecision(decisions, {
|
|
3366
|
+
host: 'elicitation',
|
|
3367
|
+
actor: actorOf(req),
|
|
3368
|
+
words: response !== null && !picked ? response : '',
|
|
3369
|
+
...(response !== null && picked ? { choice: response } : body.action !== 'accept' ? { choice: body.action } : {}),
|
|
3370
|
+
projectId: projects.index.projectOf(id) ?? null,
|
|
3371
|
+
runId: id,
|
|
3372
|
+
elicitationId: taken.entry.elicitationId,
|
|
3373
|
+
}));
|
|
3374
|
+
}
|
|
3301
3375
|
return { status: 'resolved' };
|
|
3302
3376
|
});
|
|
3303
3377
|
// The durable history of one run (FINDING-057).
|
|
@@ -3656,6 +3730,21 @@ runtime = {}) {
|
|
|
3656
3730
|
});
|
|
3657
3731
|
}
|
|
3658
3732
|
}
|
|
3733
|
+
// DC-S3: `targets.project` and `supersedes` shipped with `considerRules` (wicked-core-ts
|
|
3734
|
+
// 0.7.35). An older engine would accept and drop them, so a project rule would land
|
|
3735
|
+
// everywhere: refuse, naming the fields (codex on DC-S3).
|
|
3736
|
+
if (!adapter.projectRulesSupported() && rule !== null && typeof rule === 'object') {
|
|
3737
|
+
const carried = [
|
|
3738
|
+
...(rule.targets?.project !== undefined ? ['targets.project'] : []),
|
|
3739
|
+
...(rule.supersedes !== undefined ? ['supersedes'] : []),
|
|
3740
|
+
];
|
|
3741
|
+
if (carried.length > 0) {
|
|
3742
|
+
return reply.code(501).send({
|
|
3743
|
+
error: `the installed engine would silently drop ${carried.map((f) => `\`${f}\``).join(', ')} ` +
|
|
3744
|
+
'and land the rule everywhere; upgrade wicked-core-ts (>= 0.7.35)',
|
|
3745
|
+
});
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3659
3748
|
await adapter.upsertConformanceRule(rule);
|
|
3660
3749
|
audit.record('governance.rule.upserted', actorOf(req), { detail: { id: rule?.id } });
|
|
3661
3750
|
return { status: 'ok' };
|
|
@@ -4332,6 +4421,26 @@ runtime = {}) {
|
|
|
4332
4421
|
});
|
|
4333
4422
|
}
|
|
4334
4423
|
}
|
|
4424
|
+
// The watch registry's switches (DES-TRIGGER-REGISTRY-001 §4.2, TR-W5a): only `enabled` and
|
|
4425
|
+
// `threshold` of a shipped entry, and `llm`. Turning a watcher off is the one way to make the
|
|
4426
|
+
// platform stop telling the operator something, so only a PERSON may do it — the "human"
|
|
4427
|
+
// predicate decision capture uses; a worker's bearer gets 403. A bad patch is a 400 naming the
|
|
4428
|
+
// key (never a silent drop), and the daemon stamps who changed what and when.
|
|
4429
|
+
let watchNext;
|
|
4430
|
+
if (Object.hasOwn(patch, 'watch')) {
|
|
4431
|
+
const actor = actorOf(req);
|
|
4432
|
+
if (!isHumanOperator(actor)) {
|
|
4433
|
+
return reply.code(403).send({ error: 'Only a person can change what is watched (watch.* settings); a worker or system actor cannot' });
|
|
4434
|
+
}
|
|
4435
|
+
const registry = runtime.watchRegistry?.() ?? null;
|
|
4436
|
+
if (registry === null) {
|
|
4437
|
+
return reply.code(400).send({ error: 'watch: watching is not configured on this daemon' });
|
|
4438
|
+
}
|
|
4439
|
+
const problem = registry.validateSettingsPatch(patch.watch);
|
|
4440
|
+
if (problem !== null)
|
|
4441
|
+
return reply.code(400).send({ error: problem });
|
|
4442
|
+
watchNext = registry.mergeSettings((await adapter.getSettings()).watch, patch.watch, actor.id, Date.now());
|
|
4443
|
+
}
|
|
4335
4444
|
// Only known engine keys and validated `studio.*` keys are persisted. A key that is
|
|
4336
4445
|
// NEITHER is dropped, not refused: request bodies are forward-additive too (DES-STUDIO-001
|
|
4337
4446
|
// §5.1), so an older daemon meeting a newer client's engine key must not fail the whole
|
|
@@ -4364,11 +4473,15 @@ runtime = {}) {
|
|
|
4364
4473
|
for (const key of studioKeys) {
|
|
4365
4474
|
safe[key] = patch[key];
|
|
4366
4475
|
}
|
|
4476
|
+
if (watchNext !== undefined)
|
|
4477
|
+
safe.watch = watchNext;
|
|
4367
4478
|
// `Object.hasOwn`, NOT `k in safe`: `in` walks the prototype chain, so a dropped key named
|
|
4368
4479
|
// `toString` / `valueOf` / `constructor` would test as "kept" and vanish from `ignored` —
|
|
4369
4480
|
// the exact silent drop this route exists to end.
|
|
4370
4481
|
const ignored = Object.keys(patch).filter((k) => !Object.hasOwn(safe, k));
|
|
4371
4482
|
const settings = await adapter.updateSettings(safe);
|
|
4483
|
+
if (watchNext !== undefined)
|
|
4484
|
+
runtime.watchRegistry?.()?.applySettings(settings.watch);
|
|
4372
4485
|
// Re-apply the worker-config root to this process's env (seat sign-in). The engine reads
|
|
4373
4486
|
// WICKED_WORKER_HOME per worker spawn — never cached — so this alone makes the change live
|
|
4374
4487
|
// at the next spawn: no daemon restart, no engine restart.
|
|
@@ -4383,7 +4496,12 @@ runtime = {}) {
|
|
|
4383
4496
|
// `changed` names every persisted key, engine and `studio.*` alike; `ignored` (present only
|
|
4384
4497
|
// when there is one) is where a dropped unknown key stops being invisible.
|
|
4385
4498
|
audit.record('settings.updated', actorOf(req), {
|
|
4386
|
-
detail: {
|
|
4499
|
+
detail: {
|
|
4500
|
+
changed: Object.keys(safe),
|
|
4501
|
+
...(ignored.length > 0 ? { ignored } : {}),
|
|
4502
|
+
// Which watchers were switched or re-thresholded (health reads the stamps; this is the trail).
|
|
4503
|
+
...(watchNext !== undefined ? { watch: patch.watch } : {}),
|
|
4504
|
+
},
|
|
4387
4505
|
});
|
|
4388
4506
|
return { settings };
|
|
4389
4507
|
});
|
|
@@ -4514,6 +4632,46 @@ runtime = {}) {
|
|
|
4514
4632
|
if (editInFlight(id)) {
|
|
4515
4633
|
return reply.code(409).send({ error: `proposal ${id} is already being accepted` });
|
|
4516
4634
|
}
|
|
4635
|
+
// DC-S4a: a proposal THIS ledger filed for a decision lands through `remember()` — the one
|
|
4636
|
+
// landing path, with the human check inside it (§4.5). A payload naming an id the ledger does
|
|
4637
|
+
// not know is an ordinary policy proposal (no ORIGIN claim) and takes the path below.
|
|
4638
|
+
let decisionId = decisions?.deps.ledger.decisionForProposal(id) ?? null;
|
|
4639
|
+
if (decisions !== null && decisionId === null) {
|
|
4640
|
+
// The ledger links a proposal to its decision only once the `offered` outcome is written; a
|
|
4641
|
+
// proposal crew filed but has not linked yet (the publish window, or a crash in it) names a
|
|
4642
|
+
// decision this ledger HOLDS — it must still land through `remember()` (codex on DC-S4a).
|
|
4643
|
+
try {
|
|
4644
|
+
const pending = (await estateTool('proposal.list', { state: 'pending' }));
|
|
4645
|
+
const named = pending.proposals.find((p) => p.id === id)?.payload?.decision?.id;
|
|
4646
|
+
if (typeof named === 'string' && decisions.deps.ledger.record(named) !== null)
|
|
4647
|
+
decisionId = named;
|
|
4648
|
+
}
|
|
4649
|
+
catch (err) {
|
|
4650
|
+
return estateUpstreamError(reply, err);
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
if (decisions !== null && decisionId !== null) {
|
|
4654
|
+
try {
|
|
4655
|
+
const landed = await decisions.remember(decisionId, actorOf(req), { how: 'needs-you' });
|
|
4656
|
+
const view = decisions.deps.ledger.view(decisionId);
|
|
4657
|
+
const landing = {
|
|
4658
|
+
outcome: 'landed',
|
|
4659
|
+
ruleId: landed.rule_id,
|
|
4660
|
+
...(view !== null ? { steering_type: view.edits?.steering_type ?? view.derived.steering_type } : {}),
|
|
4661
|
+
...(landed.project !== undefined ? { project: landed.project } : {}),
|
|
4662
|
+
};
|
|
4663
|
+
return {
|
|
4664
|
+
outcome: 'handed_off',
|
|
4665
|
+
payload: { rule: view?.edits?.statement ?? view?.derived.statement ?? '', severity: 'warn', capture: 'decision', decision: { id: decisionId } },
|
|
4666
|
+
landing,
|
|
4667
|
+
};
|
|
4668
|
+
}
|
|
4669
|
+
catch (err) {
|
|
4670
|
+
if (err instanceof DecisionError)
|
|
4671
|
+
return reply.code(err.status).send({ error: err.message });
|
|
4672
|
+
throw err;
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4517
4675
|
let approved;
|
|
4518
4676
|
try {
|
|
4519
4677
|
approved = (await estateTool('proposal.approve', { id }));
|
|
@@ -4561,26 +4719,13 @@ runtime = {}) {
|
|
|
4561
4719
|
if ('error' in built) {
|
|
4562
4720
|
return landFailed(built.error);
|
|
4563
4721
|
}
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
return landFailed(`the store refused the steering rule derived from the approved policy proposal: ${message(err)}`);
|
|
4569
|
-
}
|
|
4570
|
-
audit.record('governance.rule.upserted', actorOf(req), {
|
|
4571
|
-
detail: {
|
|
4572
|
-
id: built.rule.id,
|
|
4573
|
-
source: 'proposal',
|
|
4574
|
-
via: 'proposal-approve',
|
|
4575
|
-
steeringType: built.steeringType,
|
|
4576
|
-
proposalId: id,
|
|
4577
|
-
},
|
|
4722
|
+
// THE landing (probe, upsert, read-back, audit) — shared with the decision landing (DC-S4a).
|
|
4723
|
+
const landing = await landPolicyRule({ adapter, audit }, built, actorOf(req), {
|
|
4724
|
+
via: 'proposal-approve',
|
|
4725
|
+
proposalId: id,
|
|
4578
4726
|
});
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
ruleId: built.rule.id,
|
|
4582
|
-
steering_type: built.steeringType,
|
|
4583
|
-
};
|
|
4727
|
+
if (landing.outcome === 'failed')
|
|
4728
|
+
return landFailed(landing.error ?? 'the landing failed');
|
|
4584
4729
|
return { outcome: 'handed_off', payload: approved.payload, landing };
|
|
4585
4730
|
});
|
|
4586
4731
|
// POST /proposals/:id/reject → proposal.reject → { ok: true }.
|
|
@@ -4594,6 +4739,19 @@ runtime = {}) {
|
|
|
4594
4739
|
if (editInFlight(id.trim())) {
|
|
4595
4740
|
return reply.code(409).send({ error: `proposal ${id.trim()} is being accepted` });
|
|
4596
4741
|
}
|
|
4742
|
+
// DC-S4a B12: rejecting a decision's proposal dismisses the decision too (one object, two places).
|
|
4743
|
+
const rejectedDecision = decisions?.deps.ledger.decisionForProposal(id.trim()) ?? null;
|
|
4744
|
+
if (decisions !== null && rejectedDecision !== null) {
|
|
4745
|
+
try {
|
|
4746
|
+
await decisions.dismiss(rejectedDecision, actorOf(req), 'not-a-rule');
|
|
4747
|
+
return { ok: true };
|
|
4748
|
+
}
|
|
4749
|
+
catch (err) {
|
|
4750
|
+
if (err instanceof DecisionError)
|
|
4751
|
+
return reply.code(err.status).send({ error: err.message });
|
|
4752
|
+
throw err;
|
|
4753
|
+
}
|
|
4754
|
+
}
|
|
4597
4755
|
try {
|
|
4598
4756
|
return (await estateTool('proposal.reject', { id }));
|
|
4599
4757
|
}
|
|
@@ -4814,6 +4972,8 @@ runtime = {}) {
|
|
|
4814
4972
|
registerCaptureRoutes(app);
|
|
4815
4973
|
// The Demo experience (studio#373): the demo preset's launch, view, files and script edit.
|
|
4816
4974
|
registerDemoRoutes(app, adapter);
|
|
4975
|
+
// WT-W3: the storyline PUT checks the open gate through the same resolution the gate routes use.
|
|
4976
|
+
registerWalkthroughRoutes(app, adapter, { resolveOpenGate, audit });
|
|
4817
4977
|
// ── Presets (DES-TEAMING-002 §8.4, seam C2) — saved phase selections in the engine's store;
|
|
4818
4978
|
// a launch names one via `workflow`, and the ENGINE resolves it.
|
|
4819
4979
|
registerPresetRoutes(app, adapter, security);
|
|
@@ -4959,6 +5119,6 @@ runtime = {}) {
|
|
|
4959
5119
|
pool: interactiveBridges,
|
|
4960
5120
|
log: (m) => app.log.warn(m),
|
|
4961
5121
|
});
|
|
4962
|
-
return { decideGate };
|
|
5122
|
+
return { decideGate, decisions };
|
|
4963
5123
|
}
|
|
4964
5124
|
//# sourceMappingURL=routes.js.map
|