@event4u/agent-config 16.0.0 → 16.1.0
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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +297 -137
- package/CONTRIBUTING.md +9 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/analyze/inbox.md +26 -0
- package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
- package/dist/agent-src/commands/challenge-me/closure.md +178 -0
- package/dist/agent-src/commands/challenge-me.md +9 -2
- package/dist/agent-src/commands/council/default.md +25 -5
- package/dist/agent-src/commands/feature/plan.md +28 -1
- package/dist/agent-src/commands/feature/roadmap.md +28 -1
- package/dist/agent-src/commands/implement-ticket.md +27 -0
- package/dist/agent-src/commands/jira-ticket.md +27 -0
- package/dist/agent-src/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/pr/merge.md +60 -9
- package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
- package/dist/agent-src/commands/roadmap/create.md +74 -27
- package/dist/agent-src/commands/roadmap/materialize.md +27 -0
- package/dist/agent-src/commands/roadmap/process-full.md +123 -18
- package/dist/agent-src/commands/team.md +22 -0
- package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
- package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +47 -0
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +15 -15
- package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
- package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
- package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
- package/dist/agent-src/rules/autonomous-execution.md +15 -8
- package/dist/agent-src/rules/delegation-policy.md +23 -45
- package/dist/agent-src/rules/design-fidelity.md +32 -37
- package/dist/agent-src/rules/evaluator-independence.md +43 -49
- package/dist/agent-src/rules/test-first.md +41 -0
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
- package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
- package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
- package/dist/agent-src/skills/database/data/manifest.json +4 -4
- package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
- package/dist/agent-src/skills/design-review/SKILL.md +50 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
- package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
- package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
- package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
- package/dist/agent-src/templates/agent-settings.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +188 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/dist/cli-delegate/check_update_banner.js +40 -2
- package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
- package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
- package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
- package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
- package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
- package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
- package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
- package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
- package/dist/cli-delegate/{chunk-H32DGUJH.js → chunk-P6ZHP5W2.js} +1 -1
- package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
- package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
- package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
- package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
- package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +90 -56
- package/dist/cli-delegate/cmd_doctor.js +7 -5
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +9 -7
- package/dist/cli-delegate/cmd_fleet.js +4 -2
- package/dist/cli-delegate/cmd_handoff.js +4 -4
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_preflight.js +4 -2
- package/dist/cli-delegate/cmd_prune.js +10 -8
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_get.js +28 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +10 -8
- package/dist/cli-delegate/cmd_uninstall.js +16 -13
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +12 -10
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +85 -38
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +1832 -1007
- package/dist/install/claudeRuleRewrite.js +14 -3
- package/dist/install/conflict.js +60 -10
- package/dist/install/conflictTracker.js +218 -0
- package/dist/install/fsPrimitives.js +54 -0
- package/dist/install/install.mjs +2007 -1481
- package/dist/install/manifestFiles.js +259 -0
- package/dist/install/preflight.js +14 -2
- package/dist/install/preserve.js +157 -0
- package/dist/install/recordedOwnership.js +132 -0
- package/dist/install/txlog.js +11 -2
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +398 -39
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +3 -2
- package/dist/scripts/_lib/fs_atomic.js +167 -0
- package/dist/scripts/_lib/installed_tools.js +460 -0
- package/dist/scripts/_lib/lean_projection_mode.js +3 -2
- package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
- package/dist/server/routes/install.js +8 -0
- package/dist/server/routes/wizard.js +9 -3
- package/dist/server/schemas/settings.js +17 -5
- package/dist/shared/missionExecution.js +97 -0
- package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
- package/dist/ui/index.html +1 -1
- package/docs/MIGRATION.md +103 -0
- package/docs/contracts/settings-classes.md +26 -7
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/docs/guidelines/design-fidelity-routing.md +29 -0
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/src/config/agent-settings.template.yml +139 -27
- package/src/config/auto-rule-budget.json +16 -0
- package/src/config/ci-local-parity.yml +18 -5
- package/src/config/continuity-surface.json +21 -0
- package/src/config/gate-coverage.yml +146 -13
- package/src/config/gate-violation-baselines.json +6 -6
- package/src/config/hook-token-budget.json +2 -0
- package/src/config/loop-surfaces.yaml +215 -0
- package/src/config/platform-anchor.json +3 -2
- package/src/config/release-gate-locality.yml +11 -0
- package/src/config/routing-coverage-seed.json +18 -4
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/install/claudeRuleRewrite.ts +16 -2
- package/src/install/conflict.ts +65 -9
- package/src/install/conflictTracker.ts +299 -0
- package/src/install/fsPrimitives.ts +54 -0
- package/src/install/manifestFiles.ts +312 -0
- package/src/install/preflight.ts +16 -2
- package/src/install/preserve.ts +205 -0
- package/src/install/recordedOwnership.ts +149 -0
- package/src/install/txlog.ts +11 -2
- package/src/install/types.ts +19 -4
- package/src/scripts/_cli/cmd_conformance.ts +88 -15
- package/src/scripts/_cli/cmd_doctor.ts +4 -3
- package/src/scripts/_cli/cmd_route_explain.ts +9 -2
- package/src/scripts/_cli/cmd_settings_get.ts +44 -0
- package/src/scripts/_cli/doctor_execution.ts +90 -0
- package/src/scripts/_lib/activation_payload.ts +247 -0
- package/src/scripts/_lib/agent_settings.ts +4 -2
- package/src/scripts/_lib/authority_path.ts +102 -0
- package/src/scripts/_lib/cascade_base.ts +94 -0
- package/src/scripts/_lib/changelog_eras.ts +7 -2
- package/src/scripts/_lib/continuation_ladder.ts +149 -2
- package/src/scripts/_lib/council_transport.ts +129 -0
- package/src/scripts/_lib/delivery_ready.ts +112 -0
- package/src/scripts/_lib/forge_protection.ts +267 -0
- package/src/scripts/_lib/hook_platform_keys.ts +27 -0
- package/src/scripts/_lib/json_python_parity.ts +127 -0
- package/src/scripts/_lib/lean_projection_mode.ts +14 -3
- package/src/scripts/_lib/loop_surfaces.ts +382 -0
- package/src/scripts/_lib/mission_record.ts +190 -0
- package/src/scripts/_lib/obligation_frequency.ts +217 -0
- package/src/scripts/_lib/obligations.ts +402 -0
- package/src/scripts/_lib/orchestration_record.ts +11 -20
- package/src/scripts/_lib/release_highlights.ts +199 -10
- package/src/scripts/_lib/release_holds.ts +493 -0
- package/src/scripts/_lib/release_migration_gate.ts +184 -0
- package/src/scripts/_lib/rides_along.ts +102 -0
- package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
- package/src/scripts/_lib/test_delta.ts +166 -0
- package/src/scripts/_lib/test_provenance.ts +171 -0
- package/src/scripts/_lib/typed_op_grant.ts +246 -0
- package/src/scripts/_lib/typed_op_watch.ts +185 -0
- package/src/scripts/ai_council/config.ts +8 -11
- package/src/scripts/ai_council/fallback_config.ts +5 -3
- package/src/scripts/ai_council/ownership_classes.ts +118 -0
- package/src/scripts/ask_block_census.ts +285 -15
- package/src/scripts/check_enforcement_matrix.ts +624 -0
- package/src/scripts/check_gate_reachability.ts +173 -10
- package/src/scripts/check_generator_sync.ts +954 -0
- package/src/scripts/check_preamble_payload_budget.ts +75 -2
- package/src/scripts/check_release_highlights.ts +33 -1
- package/src/scripts/check_release_holds.ts +567 -0
- package/src/scripts/check_rule_projection_integrity.ts +22 -12
- package/src/scripts/check_test_delta.ts +294 -0
- package/src/scripts/check_test_weakening.ts +299 -0
- package/src/scripts/closure_scan.ts +333 -0
- package/src/scripts/compile_hook_manifest.ts +43 -3
- package/src/scripts/council_record_shape.ts +209 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +88 -9
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/design_pass_hook.ts +128 -3
- package/src/scripts/hooks/evidence_independence.ts +96 -0
- package/src/scripts/hooks/memory_recall_hook.ts +540 -0
- package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
- package/src/scripts/hooks/rule_inject_hook.ts +49 -1
- package/src/scripts/hooks/run_continuation_hook.ts +17 -20
- package/src/scripts/hooks_doctor.ts +85 -0
- package/src/scripts/hooks_status.ts +19 -0
- package/src/scripts/install.ts +106 -206
- package/src/scripts/janitor.ts +7 -0
- package/src/scripts/lint_decision_classes.ts +464 -0
- package/src/scripts/lint_hook_manifest.ts +11 -0
- package/src/scripts/lint_major_migration_sections.ts +450 -0
- package/src/scripts/lint_plan_risk_register.ts +47 -3
- package/src/scripts/lint_roadmap_blockers.ts +61 -0
- package/src/scripts/lint_roadmap_producers.ts +202 -0
- package/src/scripts/measure_rule_budget.ts +101 -5
- package/src/scripts/new_roadmap.ts +37 -0
- package/src/scripts/orchestration_record.ts +0 -5
- package/src/scripts/release.ts +14 -98
- package/src/scripts/release_drill.ts +267 -1
- package/src/scripts/release_publication.ts +51 -0
- package/src/scripts/report_enforcement_drift.ts +566 -0
- package/src/scripts/report_host_injection_effect.ts +3 -11
- package/src/scripts/report_standing_payload_by_host.ts +26 -152
- package/src/scripts/schemas/agent-settings.schema.json +23 -0
- package/src/scripts/schemas/command.schema.json +4 -0
- package/src/scripts/scope_growth.ts +219 -0
- package/src/scripts/trigger_eval_grandfather.json +0 -1
- package/src/scripts/ui_conformance_probe.ts +622 -0
- package/src/server/schemas/settings.ts +31 -7
- package/src/shared/missionExecution.ts +126 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who may authorise a typed op — `road-to-adversarial-verification-and-long-runs`
|
|
3
|
+
* 7.3.
|
|
4
|
+
*
|
|
5
|
+
* Under a mission the sequence is: a council check that the op belongs to the
|
|
6
|
+
* mission → a native ask naming the exact object → execute. The council sits
|
|
7
|
+
* BEFORE the ask and can only subtract.
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* THE COUNCIL MAY VETO A TYPED OP. IT MAY NEVER GRANT ONE.
|
|
11
|
+
* A COUNCIL VERDICT ALONE NEVER PRODUCES THE GRANT, HOWEVER UNANIMOUS.
|
|
12
|
+
* THE GRANT COMES FROM THE ASK, AND THE ASK NAMES THE EXACT OBJECT.
|
|
13
|
+
* THE ASK ALSO NAMES THE VERB AND THE TURN. A CONFIRMATION THAT CANNOT SAY
|
|
14
|
+
* WHICH TURN IT CAME FROM IS NOT A THIS-TURN CONFIRMATION.
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* **Why the asymmetry is the whole design.** ADR-257: an unpaid route may
|
|
18
|
+
* propose and score, never decide. A council that could grant would be a second
|
|
19
|
+
* authorisation path around `non-destructive-by-default`'s this-turn
|
|
20
|
+
* confirmation — and it would be the cheaper one, so it would become the only
|
|
21
|
+
* one. Letting it VETO adds a refusal without adding an authority, which is the
|
|
22
|
+
* only direction that is safe to add for free.
|
|
23
|
+
*
|
|
24
|
+
* **An UNAVAILABLE council does not block the op**, and that is deliberate
|
|
25
|
+
* rather than lax: the council is a filter, so its absence removes a filter and
|
|
26
|
+
* leaves the ask exactly where it was. Treating unavailability as a veto would
|
|
27
|
+
* make an unconfigured council a silent kill switch on every typed op — the
|
|
28
|
+
* same-shaped wrong guess `council-availability` exists over.
|
|
29
|
+
*
|
|
30
|
+
* **What the exactness check is, and why it is two tiers.** It used to be a
|
|
31
|
+
* shape-free heuristic — eight characters, one of them not a letter — which
|
|
32
|
+
* accepted `!!!!!!!!` and `all-branches` alike, so the "exact object" guarantee
|
|
33
|
+
* the Hard Floor rests on was not real. It is now a universal floor that every
|
|
34
|
+
* op passes through, plus a per-op shape where one is declared. The floor is
|
|
35
|
+
* deliberately the weakest claim that cannot be wrong (an object with no
|
|
36
|
+
* alphanumeric run names nothing); the shape is where a category is actually
|
|
37
|
+
* refused. An op with NO declared shape keeps the old heuristic above the floor
|
|
38
|
+
* rather than being refused by default, because a Hard-Floor ask that can never
|
|
39
|
+
* be answered is worse than a loose one. The residual that leaves is real and
|
|
40
|
+
* bounded: a category still grants for an op OUTSIDE the declared set, and the
|
|
41
|
+
* declared set is drawn from the Hard Floor's own list of gated operations.
|
|
42
|
+
*
|
|
43
|
+
* Pure.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/** What the council said about this op. `null` when it was not consulted. */
|
|
47
|
+
export type CouncilVerdict = 'in-mission' | 'out-of-mission' | 'unavailable' | null;
|
|
48
|
+
|
|
49
|
+
/** The ask, as `non-destructive-by-default` requires it: an exact object. */
|
|
50
|
+
export interface ExactObjectAsk {
|
|
51
|
+
/** e.g. `force-push`. Blank is not a verb, and a blank verb never grants. */
|
|
52
|
+
readonly op: string;
|
|
53
|
+
/**
|
|
54
|
+
* The object, named concretely — a branch and SHA, a recipient and subject,
|
|
55
|
+
* an amount and card-last4. A CATEGORY is not an object.
|
|
56
|
+
*/
|
|
57
|
+
readonly object: string;
|
|
58
|
+
/** True once a human answered this ask. */
|
|
59
|
+
readonly confirmed: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* The turn the human answered in, compared against the caller's current
|
|
62
|
+
* turn. A bare boolean cannot express the Hard Floor's THIS-TURN wording:
|
|
63
|
+
* an ask persisted into a mission record and read back after a restart is
|
|
64
|
+
* otherwise indistinguishable from a confirmation given a second ago. That
|
|
65
|
+
* replay-across-restart case is the concrete threat, and the sibling module
|
|
66
|
+
* `mission_record` is what makes it concrete.
|
|
67
|
+
*/
|
|
68
|
+
readonly confirmed_turn: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type GrantState = 'vetoed' | 'ask-required' | 'granted';
|
|
72
|
+
|
|
73
|
+
export interface GrantDecision {
|
|
74
|
+
readonly state: GrantState;
|
|
75
|
+
readonly reason: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Whitespace-separated parts of an object, empties dropped. */
|
|
79
|
+
function parts(object: string): string[] {
|
|
80
|
+
return object.trim().split(/\s+/).filter((p) => p.length > 0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A part with its surrounding punctuation removed, so `@abc123,` reads as `abc123`. */
|
|
84
|
+
function bare(part: string): string {
|
|
85
|
+
return part.replace(/^[^0-9a-z]+/i, '').replace(/[^0-9a-z]+$/i, '');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Does this text name anything at all? The universal floor, and the weakest claim made here. */
|
|
89
|
+
function namesSomething(object: string): boolean {
|
|
90
|
+
return /[0-9a-z]{2,}/i.test(object);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A 7-to-40 character hex run — the token that pins a push to one commit. */
|
|
94
|
+
function isSha(part: string): boolean {
|
|
95
|
+
return /^[0-9a-f]{7,40}$/i.test(bare(part));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** An address-shaped part — the token that pins a send to one recipient. */
|
|
99
|
+
function isRecipient(part: string): boolean {
|
|
100
|
+
return /^[^\s@]+@[^\s@]+\.[a-z]{2,}$/i.test(bare(part));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A money-shaped part — a decimal amount, or digits carrying a currency marker. */
|
|
104
|
+
function isAmount(part: string): boolean {
|
|
105
|
+
return /\d[.,]\d{2}\b/.test(part) || (/\d/.test(part) && /[€$£]|\b(?:EUR|USD|GBP)\b/i.test(part));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A four-digit run — the card suffix that pins a purchase to one instrument. */
|
|
109
|
+
function isCardSuffix(part: string): boolean {
|
|
110
|
+
return /(?:^|\D)\d{4}$/.test(bare(part)) && !isAmount(part);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Is there a part that carries a name, other than the one already accounted for? */
|
|
114
|
+
function hasQualifier(ps: string[], accountedFor: (p: string) => boolean): boolean {
|
|
115
|
+
return ps.some((p) => !accountedFor(p) && /[0-9a-z]{2,}/i.test(bare(p)));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The declared per-op object shapes.
|
|
120
|
+
*
|
|
121
|
+
* Keyed by the typed-op vocabulary, looked up case-insensitively on a trimmed
|
|
122
|
+
* op. Only the ops whose exact object this repository has actually seen or that
|
|
123
|
+
* the roadmap specifies are declared — inventing a shape for an op with no
|
|
124
|
+
* example risks refusing the first real one, which is the Risk-1 direction.
|
|
125
|
+
*
|
|
126
|
+
* `tag-or-release` is deliberately absent: a release object may legitimately be
|
|
127
|
+
* a bare version, so a SHA requirement would refuse a real one.
|
|
128
|
+
*/
|
|
129
|
+
const OBJECT_SHAPES: ReadonlyMap<string, (object: string) => boolean> = new Map([
|
|
130
|
+
// A push names WHERE and WHICH COMMIT. The remote is optional — the shipped
|
|
131
|
+
// object in the unit fixture names a ref and a SHA and no remote, so
|
|
132
|
+
// requiring one would refuse an object this repository already ships.
|
|
133
|
+
...(['push', 'force-push', 'merge-to-trunk', 'history-rewrite'] as const).map(
|
|
134
|
+
(op) =>
|
|
135
|
+
[
|
|
136
|
+
op,
|
|
137
|
+
(object: string): boolean => {
|
|
138
|
+
const ps = parts(object);
|
|
139
|
+
return ps.some(isSha) && hasQualifier(ps, isSha);
|
|
140
|
+
},
|
|
141
|
+
] as const,
|
|
142
|
+
),
|
|
143
|
+
[
|
|
144
|
+
'send',
|
|
145
|
+
(object: string): boolean => {
|
|
146
|
+
const ps = parts(object);
|
|
147
|
+
return ps.some(isRecipient) && hasQualifier(ps, isRecipient);
|
|
148
|
+
},
|
|
149
|
+
] as const,
|
|
150
|
+
[
|
|
151
|
+
'purchase',
|
|
152
|
+
(object: string): boolean => {
|
|
153
|
+
const ps = parts(object);
|
|
154
|
+
return ps.some(isAmount) && ps.some(isCardSuffix);
|
|
155
|
+
},
|
|
156
|
+
] as const,
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A named object must be specific enough to be checkable.
|
|
161
|
+
*
|
|
162
|
+
* Exported so the two tiers are testable separately: a caller can ask whether an
|
|
163
|
+
* object would survive the floor without also asking which op it belongs to.
|
|
164
|
+
*/
|
|
165
|
+
export function objectIsExact(op: string, object: string): boolean {
|
|
166
|
+
if (!namesSomething(object)) return false;
|
|
167
|
+
const shape = OBJECT_SHAPES.get(op.trim().toLowerCase());
|
|
168
|
+
if (shape !== undefined) return shape(object);
|
|
169
|
+
// No declared shape: the pre-existing heuristic, above the floor.
|
|
170
|
+
return object.trim().length >= 8 && /[^a-z ]/i.test(object);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** An op names what is about to happen. Blank names nothing. */
|
|
174
|
+
function opIsNamed(op: string): boolean {
|
|
175
|
+
return op.trim().length > 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Decide whether a typed op may proceed.
|
|
180
|
+
*
|
|
181
|
+
* The order is fixed: veto first, then the verb, then the confirmation, then
|
|
182
|
+
* the turn, then the object. A confirmed ask after a veto is still a veto —
|
|
183
|
+
* otherwise the veto would be advisory, and an advisory veto is not one. The
|
|
184
|
+
* verb is read before the object because it decides which object shape applies.
|
|
185
|
+
*
|
|
186
|
+
* `currentTurn` is REQUIRED rather than optional. An optional turn would make
|
|
187
|
+
* the this-turn check something a caller opts into, and a security check that
|
|
188
|
+
* is off by default is not one.
|
|
189
|
+
*/
|
|
190
|
+
export function grantFor(
|
|
191
|
+
verdict: CouncilVerdict,
|
|
192
|
+
ask: ExactObjectAsk,
|
|
193
|
+
currentTurn: string,
|
|
194
|
+
): GrantDecision {
|
|
195
|
+
if (verdict === 'out-of-mission') {
|
|
196
|
+
return {
|
|
197
|
+
state: 'vetoed',
|
|
198
|
+
reason: `the council found ${ask.op} outside this mission; a veto is not overridden by a later confirmation`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (!opIsNamed(ask.op)) {
|
|
202
|
+
return {
|
|
203
|
+
state: 'ask-required',
|
|
204
|
+
reason: 'the ask names no operation — the verb that says what is about to happen is blank',
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
if (!ask.confirmed) {
|
|
208
|
+
return {
|
|
209
|
+
state: 'ask-required',
|
|
210
|
+
reason:
|
|
211
|
+
verdict === 'in-mission'
|
|
212
|
+
? `the council cleared ${ask.op}, which is not a grant — the ask still has to happen`
|
|
213
|
+
: `${ask.op} needs a this-turn confirmation naming the exact object`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (currentTurn.trim().length === 0 || ask.confirmed_turn.trim().length === 0) {
|
|
217
|
+
return {
|
|
218
|
+
state: 'ask-required',
|
|
219
|
+
reason: `the confirmation for ${ask.op} carries no turn, so it cannot be shown to be this turn`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (ask.confirmed_turn !== currentTurn) {
|
|
223
|
+
return {
|
|
224
|
+
state: 'ask-required',
|
|
225
|
+
reason: `the confirmation for ${ask.op} was given in turn ${ask.confirmed_turn}, not this turn (${currentTurn})`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (!objectIsExact(ask.op, ask.object)) {
|
|
229
|
+
return {
|
|
230
|
+
state: 'ask-required',
|
|
231
|
+
reason: `the confirmation named "${ask.object}", which is a category rather than an object — an approval must name the exact thing`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
return { state: 'granted', reason: `confirmed this turn for ${ask.op} on ${ask.object}` };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Could this verdict, on its own, produce a grant?
|
|
239
|
+
*
|
|
240
|
+
* Exists as its own function so the Iron Law is CHECKABLE rather than merely
|
|
241
|
+
* stated: it returns false for every verdict, including a unanimous one, and a
|
|
242
|
+
* test asserts that over the whole domain.
|
|
243
|
+
*/
|
|
244
|
+
export function verdictAloneGrants(_verdict: CouncilVerdict): boolean {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The guardrail watch over typed ops — `road-to-adversarial-verification-and-long-runs`
|
|
3
|
+
* 7.1, observation-only floor.
|
|
4
|
+
*
|
|
5
|
+
* The eleven typed ops are the Hard Floor's own list: the irreversible and
|
|
6
|
+
* externally-visible actions `non-destructive-by-default` gates. The layering is
|
|
7
|
+
* forge protection → the host hook where one is bound → this watch → model
|
|
8
|
+
* policy, and the watch exists because 7.2 measured the second layer and found
|
|
9
|
+
* SEVEN of eight hosts carry no refusing `pre_tool_use` binding at all.
|
|
10
|
+
*
|
|
11
|
+
* **This mode observes and takes no action. That is a design decision with a
|
|
12
|
+
* measured precondition, not caution.** K5 of the roadmap kills daemon
|
|
13
|
+
* enforcement before the observation floor, and the roadmap's own Risk 3 states
|
|
14
|
+
* the reason: a watchdog reading the reflog and shell history for typed ops can
|
|
15
|
+
* misclassify, and its enforcing action is stopping the host process — one false
|
|
16
|
+
* stop costs a twelve-hour run. So enforcement waits behind a measured
|
|
17
|
+
* false-positive rate under 1 % on the 30-session corpus, and
|
|
18
|
+
* {@link enforcementAllowed} refuses to unlock without that artefact.
|
|
19
|
+
*
|
|
20
|
+
* **What it can and cannot see, said plainly.** It reads lines — reflog entries,
|
|
21
|
+
* exposed shell history, forge event payloads. A line is not an intention: an op
|
|
22
|
+
* typed into a comment, a heredoc, or a string literal looks identical to one
|
|
23
|
+
* about to run. That is precisely why the first mode only writes down what it
|
|
24
|
+
* saw, and why the false-positive rate is the gate on doing anything more.
|
|
25
|
+
*
|
|
26
|
+
* Pure. The I/O — tailing a reflog, reading history, polling an event stream —
|
|
27
|
+
* belongs to the caller, so every classification is testable offline.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** The typed-op classes this watch recognises. */
|
|
31
|
+
export const TYPED_OPS = [
|
|
32
|
+
'push',
|
|
33
|
+
'force-push',
|
|
34
|
+
'merge-to-trunk',
|
|
35
|
+
'tag-or-release',
|
|
36
|
+
'deploy',
|
|
37
|
+
'bulk-delete',
|
|
38
|
+
'history-rewrite',
|
|
39
|
+
'send',
|
|
40
|
+
'publish',
|
|
41
|
+
'purchase',
|
|
42
|
+
'submit',
|
|
43
|
+
] as const;
|
|
44
|
+
export type TypedOp = (typeof TYPED_OPS)[number];
|
|
45
|
+
|
|
46
|
+
/** Where a line came from. Kept because the sources have different reliability. */
|
|
47
|
+
export type WatchSource = 'reflog' | 'shell-history' | 'forge-event';
|
|
48
|
+
|
|
49
|
+
export interface Observation {
|
|
50
|
+
readonly op: TypedOp;
|
|
51
|
+
readonly source: WatchSource;
|
|
52
|
+
/** The line, truncated. Evidence, never re-executed. */
|
|
53
|
+
readonly evidence: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The recognisers, most specific first.
|
|
58
|
+
*
|
|
59
|
+
* ORDER MATTERS and is asserted by a test: `git push --force` is a force-push,
|
|
60
|
+
* not a push, and a table walked in declaration order gives the specific class
|
|
61
|
+
* only if it is declared before the general one. A misordered table would report
|
|
62
|
+
* the milder op for the more dangerous line, which is the wrong direction for
|
|
63
|
+
* every downstream reader.
|
|
64
|
+
*/
|
|
65
|
+
const RECOGNISERS: readonly (readonly [TypedOp, RegExp])[] = [
|
|
66
|
+
['force-push', /\bgit\b[^\n]*\bpush\b[^\n]*(--force\b|--force-with-lease\b|\s-f\b)/],
|
|
67
|
+
['history-rewrite', /\bgit\b[^\n]*\b(rebase|filter-repo|filter-branch)\b|\bgit\b[^\n]*reset\s+--hard\b/],
|
|
68
|
+
['merge-to-trunk', /\bgit\b[^\n]*\bmerge\b[^\n]*\b(main|master|trunk|production)\b|\bgh\b[^\n]*\bpr\b[^\n]*\bmerge\b/],
|
|
69
|
+
['tag-or-release', /\bgit\b[^\n]*\bpush\b[^\n]*\b--tags?\b|\bgh\b[^\n]*\brelease\b[^\n]*\bcreate\b/],
|
|
70
|
+
['deploy', /\b(terraform\s+apply|kubectl\s+apply|helm\s+(install|upgrade)|serverless\s+deploy)\b/],
|
|
71
|
+
['bulk-delete', /\brm\s+-[a-z]*r[a-z]*f\b|\bgit\b[^\n]*\brm\s+-r\b|\b(DROP\s+TABLE|TRUNCATE)\b/i],
|
|
72
|
+
['push', /\bgit\b[^\n]*\bpush\b/],
|
|
73
|
+
['purchase', /\b(stripe|checkout|charge|payment)\b[^\n]*\b(create|confirm|capture)\b/i],
|
|
74
|
+
['publish', /\bnpm\s+publish\b|\bgh\b[^\n]*\brelease\b[^\n]*\bupload\b/],
|
|
75
|
+
['send', /\b(sendmail|mail\s+-s|curl\b[^\n]*\/messages\b)/],
|
|
76
|
+
['submit', /\bcurl\b[^\n]*\s-X\s*POST\b[^\n]*\b(submit|apply|order)\b/i],
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const EVIDENCE_MAX = 200;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Classify one line, or `null` when nothing matched.
|
|
83
|
+
*
|
|
84
|
+
* Deliberately returns at most ONE op. A line is one action, and reporting two
|
|
85
|
+
* classes for it would double-count in the false-positive denominator the
|
|
86
|
+
* enforcement gate depends on.
|
|
87
|
+
*/
|
|
88
|
+
export function classifyLine(line: string, source: WatchSource): Observation | null {
|
|
89
|
+
for (const [op, re] of RECOGNISERS) {
|
|
90
|
+
if (re.test(line)) {
|
|
91
|
+
return { op, source, evidence: line.trim().slice(0, EVIDENCE_MAX) };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Classify a batch, preserving order and dropping non-matches. */
|
|
98
|
+
export function observe(
|
|
99
|
+
lines: readonly string[],
|
|
100
|
+
source: WatchSource,
|
|
101
|
+
): Observation[] {
|
|
102
|
+
return lines
|
|
103
|
+
.map((l) => classifyLine(l, source))
|
|
104
|
+
.filter((o): o is Observation => o !== null);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The two modes. There is no third, and `enforce` is gated. */
|
|
108
|
+
export type WatchMode = 'observe' | 'enforce';
|
|
109
|
+
|
|
110
|
+
/** The false-positive measurement the enforcing mode waits behind. */
|
|
111
|
+
export interface FalsePositiveMeasurement {
|
|
112
|
+
/** Sessions the corpus covered. The roadmap names 30. */
|
|
113
|
+
readonly sessions: number;
|
|
114
|
+
readonly observations: number;
|
|
115
|
+
readonly falsePositives: number;
|
|
116
|
+
/** Where the artefact lives, so the claim is checkable. */
|
|
117
|
+
readonly artefact: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The corpus size the roadmap fixed before any measurement was taken. */
|
|
121
|
+
export const REQUIRED_SESSIONS = 30;
|
|
122
|
+
/** The rate enforcement waits behind. */
|
|
123
|
+
export const MAX_FALSE_POSITIVE_RATE = 0.01;
|
|
124
|
+
|
|
125
|
+
export interface EnforcementVerdict {
|
|
126
|
+
readonly allowed: boolean;
|
|
127
|
+
readonly reason: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* May the enforcing mode be enabled?
|
|
132
|
+
*
|
|
133
|
+
* `null` — no measurement at all — is a refusal with its own reason, never a
|
|
134
|
+
* pass. A gate that unlocks when it cannot measure is the shape this whole
|
|
135
|
+
* roadmap is written against, and it is the one an absent-artefact check gets
|
|
136
|
+
* wrong by default.
|
|
137
|
+
*/
|
|
138
|
+
export function enforcementAllowed(
|
|
139
|
+
m: FalsePositiveMeasurement | null,
|
|
140
|
+
): EnforcementVerdict {
|
|
141
|
+
if (m === null) {
|
|
142
|
+
return {
|
|
143
|
+
allowed: false,
|
|
144
|
+
reason:
|
|
145
|
+
'no false-positive measurement exists. Enforcement stops the host process; a ' +
|
|
146
|
+
'rate nobody measured is not a control (K5).',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (m.sessions < REQUIRED_SESSIONS) {
|
|
150
|
+
return {
|
|
151
|
+
allowed: false,
|
|
152
|
+
reason: `measured over ${String(m.sessions)} session(s), below the ${String(REQUIRED_SESSIONS)} the corpus was fixed at`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (m.observations === 0) {
|
|
156
|
+
return {
|
|
157
|
+
allowed: false,
|
|
158
|
+
reason:
|
|
159
|
+
'zero observations: a rate over an empty denominator is not a measurement, ' +
|
|
160
|
+
'and it is the reading a broken recogniser produces',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const rate = m.falsePositives / m.observations;
|
|
164
|
+
if (rate >= MAX_FALSE_POSITIVE_RATE) {
|
|
165
|
+
return {
|
|
166
|
+
allowed: false,
|
|
167
|
+
reason: `false-positive rate ${rate.toFixed(4)} is not below ${String(MAX_FALSE_POSITIVE_RATE)}`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return { allowed: true, reason: `rate ${rate.toFixed(4)} over ${String(m.observations)} observations in ${m.artefact}` };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* What the watch does with an observation, per mode.
|
|
175
|
+
*
|
|
176
|
+
* In `observe` the action is always `record` — there is no branch that acts, so
|
|
177
|
+
* no configuration flips this mode into one that does.
|
|
178
|
+
*/
|
|
179
|
+
export function actionFor(
|
|
180
|
+
mode: WatchMode,
|
|
181
|
+
measurement: FalsePositiveMeasurement | null,
|
|
182
|
+
): 'record' | 'record-and-ask' {
|
|
183
|
+
if (mode === 'observe') return 'record';
|
|
184
|
+
return enforcementAllowed(measurement).allowed ? 'record-and-ask' : 'record';
|
|
185
|
+
}
|
|
@@ -543,6 +543,7 @@ export type QuorumMinPresent = number;
|
|
|
543
543
|
|
|
544
544
|
export type { FallbackConfig } from './fallback_config.js';
|
|
545
545
|
import { buildFallback, buildSecondModel, type FallbackConfig as _FallbackConfig } from './fallback_config.js';
|
|
546
|
+
import { OWNERSHIP_CLASSES, allOwnerLocked, defaultModeFor, ownershipModeError } from './ownership_classes.js';
|
|
546
547
|
import {
|
|
547
548
|
_f,
|
|
548
549
|
_pyOct,
|
|
@@ -1182,13 +1183,7 @@ const _LOCKED_IMPACT_CLASSES: ReadonlySet<string> = new Set([
|
|
|
1182
1183
|
'user_required',
|
|
1183
1184
|
]);
|
|
1184
1185
|
|
|
1185
|
-
const
|
|
1186
|
-
trivial: 'agent',
|
|
1187
|
-
low_impact: 'agent',
|
|
1188
|
-
medium_impact: 'council',
|
|
1189
|
-
high_impact: 'user',
|
|
1190
|
-
user_required: 'user',
|
|
1191
|
-
};
|
|
1186
|
+
const _ALL_OWNER_LOCKED = allOwnerLocked(_LOCKED_IMPACT_CLASSES);
|
|
1192
1187
|
|
|
1193
1188
|
function _build_decision_resolution(d: Dict): DecisionResolutionConfig {
|
|
1194
1189
|
if (!_isDict(d)) {
|
|
@@ -1207,14 +1202,14 @@ function _build_decision_resolution(d: Dict): DecisionResolutionConfig {
|
|
|
1207
1202
|
);
|
|
1208
1203
|
}
|
|
1209
1204
|
const classes = new Map<string, DecisionResolutionEntry>();
|
|
1210
|
-
for (const cls of _IMPACT_CLASSES) {
|
|
1205
|
+
for (const cls of [..._IMPACT_CLASSES, ...OWNERSHIP_CLASSES]) {
|
|
1211
1206
|
const entry_raw = _orEmpty(classes_raw[cls] ?? null);
|
|
1212
1207
|
if (!_isDict(entry_raw)) {
|
|
1213
1208
|
throw new CouncilConfigError(
|
|
1214
1209
|
`\`decision_resolution.classes.${cls}\` must be a mapping.`,
|
|
1215
1210
|
);
|
|
1216
1211
|
}
|
|
1217
|
-
const mode = _get(entry_raw, 'mode',
|
|
1212
|
+
const mode = _get(entry_raw, 'mode', defaultModeFor(cls));
|
|
1218
1213
|
if (!(_isStr(mode) && _VALID_RESOLUTION_MODES.has(mode))) {
|
|
1219
1214
|
throw new CouncilConfigError(
|
|
1220
1215
|
`decision_resolution.classes.${cls}.mode=${_pyRepr(mode)} not in ` +
|
|
@@ -1230,10 +1225,12 @@ function _build_decision_resolution(d: Dict): DecisionResolutionConfig {
|
|
|
1230
1225
|
`the user.`,
|
|
1231
1226
|
);
|
|
1232
1227
|
}
|
|
1228
|
+
const ownErr = ownershipModeError(cls, mode, _pyRepr);
|
|
1229
|
+
if (ownErr !== null) throw new CouncilConfigError(ownErr);
|
|
1233
1230
|
// Iron Law: `dispatch` is not configurable for locked classes
|
|
1234
1231
|
// (step-9 P8/P11 · U3). Any nested `dispatch` key — including
|
|
1235
1232
|
// smuggled-in YAML anchor merges — is a hard schema error.
|
|
1236
|
-
if (
|
|
1233
|
+
if (_ALL_OWNER_LOCKED.has(cls) && 'dispatch' in entry_raw) {
|
|
1237
1234
|
throw new CouncilConfigError(
|
|
1238
1235
|
`decision_resolution.classes.${cls}.dispatch=` +
|
|
1239
1236
|
`${_pyRepr(entry_raw.dispatch)}: dispatch is not ` +
|
|
@@ -1251,7 +1248,7 @@ function _build_decision_resolution(d: Dict): DecisionResolutionConfig {
|
|
|
1251
1248
|
// UOTL Phase 4.1 — the optional local rung, REFUSED on a locked
|
|
1252
1249
|
// class rather than ignored: a dropped key reads as configured.
|
|
1253
1250
|
const second_model = buildSecondModel(
|
|
1254
|
-
entry_raw as Record<string, unknown>, cls,
|
|
1251
|
+
entry_raw as Record<string, unknown>, cls, _ALL_OWNER_LOCKED, _FALLBACK_DEPS);
|
|
1255
1252
|
classes.set(cls, { mode, confidence_threshold: threshold, second_model });
|
|
1256
1253
|
}
|
|
1257
1254
|
const fast_path_raw = _getOr(d, 'fast_path', {});
|
|
@@ -107,9 +107,11 @@ export function buildSecondModel(
|
|
|
107
107
|
if (locked.has(cls)) {
|
|
108
108
|
throw d.error(
|
|
109
109
|
`decision_resolution.classes.${cls}.second_model=${d.repr(entry['second_model'])}: ` +
|
|
110
|
-
`a second-model rung is not configurable for
|
|
111
|
-
`
|
|
112
|
-
`on
|
|
110
|
+
`a second-model rung is not configurable for an owner-routed class ` +
|
|
111
|
+
`(high-impact / user-required on the impact axis; product-owned / ` +
|
|
112
|
+
`business-owned / destructive-owned on the ownership axis) — those ` +
|
|
113
|
+
`classes are LOCKED to \`user\` (Iron Law) and never resolve on a ` +
|
|
114
|
+
`model of any kind.`,
|
|
113
115
|
);
|
|
114
116
|
}
|
|
115
117
|
const raw = entry['second_model'];
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ownership axis of `decision_resolution` (ADR-268 § 10).
|
|
3
|
+
*
|
|
4
|
+
* A separate module rather than more constants in `config.ts`, for two
|
|
5
|
+
* reasons. That file is 700+ lines over the source ceiling already, so every
|
|
6
|
+
* line added there moves a ratchet; and these sets are read by the loader AND
|
|
7
|
+
* by the roadmap-side gate, which has no business importing the council's
|
|
8
|
+
* whole schema to learn eight class names.
|
|
9
|
+
*
|
|
10
|
+
* The axis changes from IMPACT to OWNERSHIP: a technical decision does not
|
|
11
|
+
* become owner-owned because it is hard or high-impact. The five impact names
|
|
12
|
+
* stay accepted for one minor so no installed `.ai-council.yml` fails to load,
|
|
13
|
+
* and they keep their own (impact-axis) Iron Law — they are deliberately NOT
|
|
14
|
+
* in this module.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** The eight ownership classes, in table order. */
|
|
18
|
+
export const OWNERSHIP_CLASSES: readonly string[] = [
|
|
19
|
+
'deterministic',
|
|
20
|
+
'reversible-technical',
|
|
21
|
+
'contested-technical',
|
|
22
|
+
'critical-technical',
|
|
23
|
+
'product-owned',
|
|
24
|
+
'business-owned',
|
|
25
|
+
'destructive-owned',
|
|
26
|
+
'spend-exhaustion',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Iron Law: ONLY these three are locked to `user`. `critical-technical` is
|
|
31
|
+
* deliberately absent — locking it is the § 0 break ADR-268 names by hand
|
|
32
|
+
* ("routing a technical decision to the owner because it is hard or
|
|
33
|
+
* high-impact"), so the absence is load-bearing rather than an oversight.
|
|
34
|
+
*/
|
|
35
|
+
export const OWNER_LOCKED_CLASSES: ReadonlySet<string> = new Set([
|
|
36
|
+
'product-owned',
|
|
37
|
+
'business-owned',
|
|
38
|
+
'destructive-owned',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Iron Law: `spend-exhaustion` PAUSES AND REPORTS (ADR-268 § 8). It is never
|
|
43
|
+
* routed to the owner as a question, so `mode: user` on it is a hard schema
|
|
44
|
+
* error rather than an unusual-but-legal setting.
|
|
45
|
+
*/
|
|
46
|
+
export const NEVER_OWNER_CLASSES: ReadonlySet<string> = new Set(['spend-exhaustion']);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Both owner-routed sets as one, given the impact axis's own locked set.
|
|
50
|
+
*
|
|
51
|
+
* A function rather than a constant because the impact half lives in the
|
|
52
|
+
* loader: importing it here would make this module depend on the schema it is
|
|
53
|
+
* imported BY, which is the cycle the split exists to avoid.
|
|
54
|
+
*/
|
|
55
|
+
export function allOwnerLocked(impactLocked: ReadonlySet<string>): ReadonlySet<string> {
|
|
56
|
+
return new Set<string>([...impactLocked, ...OWNER_LOCKED_CLASSES]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Default `mode` per class, BOTH axes — the resolver column of each table.
|
|
61
|
+
*
|
|
62
|
+
* The five impact rows moved here with the eight ownership ones so there is
|
|
63
|
+
* exactly one place a reader asks "what does this class do when nobody
|
|
64
|
+
* configured it", rather than one map per axis drifting against the other.
|
|
65
|
+
*/
|
|
66
|
+
export const DEFAULT_RESOLUTION_MODES: Readonly<Record<string, string>> = {
|
|
67
|
+
trivial: 'agent',
|
|
68
|
+
low_impact: 'agent',
|
|
69
|
+
medium_impact: 'council',
|
|
70
|
+
high_impact: 'user',
|
|
71
|
+
user_required: 'user',
|
|
72
|
+
deterministic: 'agent',
|
|
73
|
+
'reversible-technical': 'agent',
|
|
74
|
+
'contested-technical': 'council',
|
|
75
|
+
'critical-technical': 'council',
|
|
76
|
+
'product-owned': 'user',
|
|
77
|
+
'business-owned': 'user',
|
|
78
|
+
'destructive-owned': 'user',
|
|
79
|
+
'spend-exhaustion': 'agent',
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** The default `mode` for a class, or `agent` for a name neither axis knows. */
|
|
83
|
+
export function defaultModeFor(cls: string): string {
|
|
84
|
+
return DEFAULT_RESOLUTION_MODES[cls] ?? 'agent';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The ownership axis's mode check, as one function.
|
|
89
|
+
*
|
|
90
|
+
* Both Iron Laws live here rather than at the call site so the loader carries
|
|
91
|
+
* three lines instead of twenty-four — and so a reader looking for "which
|
|
92
|
+
* classes are locked" finds the rule next to the sets it reads.
|
|
93
|
+
*
|
|
94
|
+
* Returns the error message, or `null` when the mode is legal. `repr` is the
|
|
95
|
+
* caller's own value formatter, so the message reads identically to every
|
|
96
|
+
* other schema error in the loader.
|
|
97
|
+
*/
|
|
98
|
+
export function ownershipModeError(
|
|
99
|
+
cls: string,
|
|
100
|
+
mode: string,
|
|
101
|
+
repr: (v: unknown) => string,
|
|
102
|
+
): string | null {
|
|
103
|
+
if (OWNER_LOCKED_CLASSES.has(cls) && mode !== 'user') {
|
|
104
|
+
return (
|
|
105
|
+
`decision_resolution.classes.${cls}.mode=${repr(mode)}: ` +
|
|
106
|
+
`class \`${cls}\` is LOCKED to \`user\` (Iron Law) — product, business and ` +
|
|
107
|
+
`destructive decisions are owner-owned and never resolve on a model.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (NEVER_OWNER_CLASSES.has(cls) && mode === 'user') {
|
|
111
|
+
return (
|
|
112
|
+
`decision_resolution.classes.${cls}.mode=${repr(mode)}: ` +
|
|
113
|
+
`class \`${cls}\` is never owner-routed (Iron Law) — spend exhaustion ` +
|
|
114
|
+
`pauses and reports, it does not ask.`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|