@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,464 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* The `## Decisions` contract — ownership vocabulary and section shape.
|
|
4
|
+
*
|
|
5
|
+
* Two check families:
|
|
6
|
+
*
|
|
7
|
+
* A. **Ownership vocabulary.** `decision_resolution`'s axis is
|
|
8
|
+
* ownership, not impact (ADR-268 § 10). A `## Decisions` table's
|
|
9
|
+
* `ownership` column accepts exactly the eight class names and nothing
|
|
10
|
+
* else — `high impact`, `P1`, `critical` and friends are the old axis
|
|
11
|
+
* leaking back in, and the whole point of the ruling is that a technical
|
|
12
|
+
* decision does not become owner-owned because it is hard.
|
|
13
|
+
*
|
|
14
|
+
* B. **Section shape.** A `status: ready` roadmap that still
|
|
15
|
+
* carries an unresolved decision marker (`TBD`, *decide later*, *to be
|
|
16
|
+
* decided*, …) outside a `## Decisions` row is a plan that enters
|
|
17
|
+
* execution with an open question in it. The same marker resolved into a
|
|
18
|
+
* `## Decisions` row is green: the section IS the discharge.
|
|
19
|
+
*
|
|
20
|
+
* Corpus: top-level `agents/roadmaps/*.md` code-comment-allow provenance-comment -- the path is this script's operand, not where the code came from
|
|
21
|
+
* (`archive/`, `skipped/`, `later/`,
|
|
22
|
+
* `stubs/` excluded, as are `template.md` and `dashboard*`) — the same corpus
|
|
23
|
+
* `lint_plan_risk_register` reads, and for the same reason: a parked or
|
|
24
|
+
* archived plan is not entering execution. Archived roadmaps carry freeform
|
|
25
|
+
* `## Decisions (locked …)` prose from before this contract existed; they are
|
|
26
|
+
* out of scope rather than grandfathered by a special case.
|
|
27
|
+
*
|
|
28
|
+
* Exit codes: 0 clean · 1 violations · 2 internal error / bad argv.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import * as fs from 'node:fs';
|
|
32
|
+
import * as path from 'node:path';
|
|
33
|
+
import process from 'node:process';
|
|
34
|
+
import { fileURLToPath } from 'node:url';
|
|
35
|
+
|
|
36
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
37
|
+
|
|
38
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
39
|
+
const _PROG = 'lint_decision_classes';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The eight ownership classes (ADR-268 § 10). Kept as a literal here rather
|
|
43
|
+
* than imported from `ai_council/config.ts`: that module is the COUNCIL
|
|
44
|
+
* config loader and pulling it in would make a roadmap lint depend on the
|
|
45
|
+
* council's whole schema. The duplication is pinned by a test that reads both.
|
|
46
|
+
*/
|
|
47
|
+
export const OWNERSHIP_CLASSES: readonly string[] = [
|
|
48
|
+
'deterministic',
|
|
49
|
+
'reversible-technical',
|
|
50
|
+
'contested-technical',
|
|
51
|
+
'critical-technical',
|
|
52
|
+
'product-owned',
|
|
53
|
+
'business-owned',
|
|
54
|
+
'destructive-owned',
|
|
55
|
+
'spend-exhaustion',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/** `resolved by` vocabulary — the third column's contract. */
|
|
59
|
+
export const RESOLVER_FORMS: readonly string[] = [
|
|
60
|
+
'evidence',
|
|
61
|
+
'agent',
|
|
62
|
+
'independent:',
|
|
63
|
+
'council:',
|
|
64
|
+
'team:',
|
|
65
|
+
'owner',
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Unresolved-decision markers. Deliberately narrow: every pattern is a phrase
|
|
70
|
+
* an author writes when they KNOW a decision is open, never a word that shows
|
|
71
|
+
* up in ordinary plan prose. A detector that fires on normal writing gets
|
|
72
|
+
* weakened until it finds nothing, which is how a
|
|
73
|
+
* closure detector gets neutered.
|
|
74
|
+
*/
|
|
75
|
+
const UNRESOLVED_PATTERNS: readonly { readonly re: RegExp; readonly label: string }[] = [
|
|
76
|
+
{ re: /\bTBD\b/, label: 'TBD' },
|
|
77
|
+
{ re: /\bto be decided\b/i, label: 'to be decided' },
|
|
78
|
+
{ re: /\bdecide later\b/i, label: 'decide later' },
|
|
79
|
+
{ re: /\bdecided later\b/i, label: 'decided later' },
|
|
80
|
+
{ re: /\bopen question\b/i, label: 'open question' },
|
|
81
|
+
{ re: /\bwe(?:'| a)re not sure (?:yet|which|whether)\b/i, label: 'not sure yet' },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const DECISIONS_HEADING_RE = /^##\s+Decisions\s*$/;
|
|
85
|
+
const DISCHARGE_HEADING_RE = /^##\s+(Decisions|Blockers)\s*$/;
|
|
86
|
+
const ANY_H2_RE = /^##\s+/;
|
|
87
|
+
const FRONTMATTER_STATUS_RE = /^status:\s*(\S+)/m;
|
|
88
|
+
const EXCLUDED_NAMES_RE = /^(template\.md|dashboard.*)$/;
|
|
89
|
+
|
|
90
|
+
export interface Violation {
|
|
91
|
+
readonly file: string;
|
|
92
|
+
readonly line: number;
|
|
93
|
+
readonly reason: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Split a markdown table row into trimmed cells (leading/trailing pipe dropped). */
|
|
97
|
+
export function tableCells(line: string): string[] {
|
|
98
|
+
const trimmed = line.trim();
|
|
99
|
+
if (!trimmed.startsWith('|')) return [];
|
|
100
|
+
const inner = trimmed.replace(/^\|/, '').replace(/\|$/, '');
|
|
101
|
+
return inner.split('|').map((c) => c.trim());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isSeparatorRow(cells: readonly string[]): boolean {
|
|
105
|
+
return cells.length > 0 && cells.every((c) => /^:?-{2,}:?$/.test(c));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Frontmatter `status:` (lower-cased), or `ready` when absent — status is binary. */
|
|
109
|
+
export function readStatus(text: string): string {
|
|
110
|
+
if (!text.startsWith('---')) return 'ready';
|
|
111
|
+
const end = text.indexOf('\n---', 3);
|
|
112
|
+
if (end < 0) return 'ready';
|
|
113
|
+
const fm = text.slice(0, end);
|
|
114
|
+
const m = FRONTMATTER_STATUS_RE.exec(fm);
|
|
115
|
+
return m ? (m[1] as string).toLowerCase() : 'ready';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface DecisionsSection {
|
|
119
|
+
/** 0-based index of the heading line, or -1 when there is no section. */
|
|
120
|
+
readonly headingIdx: number;
|
|
121
|
+
/** 0-based [start, end) line range of the section body. */
|
|
122
|
+
readonly start: number;
|
|
123
|
+
readonly end: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function findDecisionsSection(lines: readonly string[]): DecisionsSection {
|
|
127
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
128
|
+
if (DECISIONS_HEADING_RE.test(lines[i] as string)) {
|
|
129
|
+
let j = i + 1;
|
|
130
|
+
while (j < lines.length && !ANY_H2_RE.test(lines[j] as string)) j += 1;
|
|
131
|
+
return { headingIdx: i, start: i + 1, end: j };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { headingIdx: -1, start: 0, end: 0 };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check family A — the ownership column of a `## Decisions` table.
|
|
139
|
+
*
|
|
140
|
+
* The header row names the columns; `ownership` is located by name rather than
|
|
141
|
+
* by position so a plan may add a column without silently moving the one the
|
|
142
|
+
* gate reads.
|
|
143
|
+
*/
|
|
144
|
+
export function checkOwnershipColumn(rel: string, lines: readonly string[]): Violation[] {
|
|
145
|
+
const out: Violation[] = [];
|
|
146
|
+
const sec = findDecisionsSection(lines);
|
|
147
|
+
if (sec.headingIdx < 0) return out;
|
|
148
|
+
|
|
149
|
+
let ownershipCol = -1;
|
|
150
|
+
let resolvedCol = -1;
|
|
151
|
+
let headerSeen = false;
|
|
152
|
+
for (let i = sec.start; i < sec.end; i += 1) {
|
|
153
|
+
const cells = tableCells(lines[i] as string);
|
|
154
|
+
if (cells.length === 0) continue;
|
|
155
|
+
if (isSeparatorRow(cells)) continue;
|
|
156
|
+
if (!headerSeen) {
|
|
157
|
+
headerSeen = true;
|
|
158
|
+
ownershipCol = cells.findIndex((c) => /^ownership$/i.test(c));
|
|
159
|
+
resolvedCol = cells.findIndex((c) => /^resolved[ _]by$/i.test(c));
|
|
160
|
+
if (ownershipCol < 0) {
|
|
161
|
+
out.push({
|
|
162
|
+
file: rel,
|
|
163
|
+
line: i + 1,
|
|
164
|
+
reason:
|
|
165
|
+
'the `## Decisions` table has no `ownership` column — the contract is ' +
|
|
166
|
+
'`| ID | ownership | resolved by | decision | evidence | revisit if |`',
|
|
167
|
+
});
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
if (resolvedCol < 0) {
|
|
171
|
+
out.push({
|
|
172
|
+
file: rel,
|
|
173
|
+
line: i + 1,
|
|
174
|
+
reason:
|
|
175
|
+
'the `## Decisions` table has no `resolved by` column — a decision with ' +
|
|
176
|
+
'no recorded resolver is not a closed decision',
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const owner = (cells[ownershipCol] ?? '').replace(/`/g, '').trim();
|
|
182
|
+
if (owner === '') {
|
|
183
|
+
out.push({ file: rel, line: i + 1, reason: 'empty `ownership` cell' });
|
|
184
|
+
} else if (!OWNERSHIP_CLASSES.includes(owner)) {
|
|
185
|
+
out.push({
|
|
186
|
+
file: rel,
|
|
187
|
+
line: i + 1,
|
|
188
|
+
reason:
|
|
189
|
+
`ownership=\`${owner}\` is not one of the eight ownership classes ` +
|
|
190
|
+
`(${OWNERSHIP_CLASSES.join(', ')}). The axis is ownership, not impact ` +
|
|
191
|
+
`(ADR-268 § 10).`,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (resolvedCol >= 0) {
|
|
195
|
+
const resolved = (cells[resolvedCol] ?? '').replace(/`/g, '').trim();
|
|
196
|
+
const ok = RESOLVER_FORMS.some((f) =>
|
|
197
|
+
f.endsWith(':') ? resolved.startsWith(f) && resolved.length > f.length : resolved === f,
|
|
198
|
+
);
|
|
199
|
+
if (!ok) {
|
|
200
|
+
out.push({
|
|
201
|
+
file: rel,
|
|
202
|
+
line: i + 1,
|
|
203
|
+
reason:
|
|
204
|
+
`resolved by=\`${resolved}\` is not one of ` +
|
|
205
|
+
`evidence, agent, independent:<session or model>, council:<record>, ` +
|
|
206
|
+
`team:<record>, owner`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The two sections where an open item is already DISCHARGED by a contract of
|
|
216
|
+
* its own, so the marker there is the record rather than the defect:
|
|
217
|
+
*
|
|
218
|
+
* - `## Decisions` — the closure record this gate exists to require.
|
|
219
|
+
* - `## Blockers` — the structured five-field blocker contract, which owns
|
|
220
|
+
* open items a human must decide. Firing here would demand a roadmap close
|
|
221
|
+
* a decision it has correctly recorded as not-the-agent's to close.
|
|
222
|
+
*/
|
|
223
|
+
export function dischargeRanges(lines: readonly string[]): [number, number][] {
|
|
224
|
+
const out: [number, number][] = [];
|
|
225
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
226
|
+
if (!DISCHARGE_HEADING_RE.test(lines[i] as string)) continue;
|
|
227
|
+
let j = i + 1;
|
|
228
|
+
while (j < lines.length && !ANY_H2_RE.test(lines[j] as string)) j += 1;
|
|
229
|
+
out.push([i, j]);
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The IDs recorded in the `## Decisions` table.
|
|
236
|
+
*
|
|
237
|
+
* A marker that NAMES its row is discharged by the record, not by deletion.
|
|
238
|
+
* Without this, the cheapest way to clear a finding is to delete the word
|
|
239
|
+
* "TBD" from the step — which loses the fact that a decision was ever open and
|
|
240
|
+
* teaches exactly the wrong repair.
|
|
241
|
+
*/
|
|
242
|
+
export function decisionRowIds(lines: readonly string[]): Set<string> {
|
|
243
|
+
const out = new Set<string>();
|
|
244
|
+
const sec = findDecisionsSection(lines);
|
|
245
|
+
if (sec.headingIdx < 0) return out;
|
|
246
|
+
let headerSeen = false;
|
|
247
|
+
for (let i = sec.start; i < sec.end; i += 1) {
|
|
248
|
+
const cells = tableCells(lines[i] as string);
|
|
249
|
+
if (cells.length === 0 || isSeparatorRow(cells)) continue;
|
|
250
|
+
if (!headerSeen) {
|
|
251
|
+
headerSeen = true;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const id = (cells[0] ?? '').replace(/`/g, '').trim();
|
|
255
|
+
if (id) out.add(id);
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** A marker line pointing at its own closure record: `closed as D1`, `see D1`. */
|
|
261
|
+
const DISCHARGE_REF_RE = /(?:closed as|resolved as|see|per)\s+`?([A-Za-z]+\d+)`?/i;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Check family B — an unresolved marker outside the `## Decisions` section.
|
|
265
|
+
*
|
|
266
|
+
* Fenced code, HTML comments and the roadmap's own prose ABOUT the gate would
|
|
267
|
+
* all produce false positives, so: code fences are skipped, and a marker on a
|
|
268
|
+
* line inside the `## Decisions` section is by definition the discharge, not
|
|
269
|
+
* the defect.
|
|
270
|
+
*/
|
|
271
|
+
export function checkUnresolvedMarkers(rel: string, lines: readonly string[]): Violation[] {
|
|
272
|
+
const out: Violation[] = [];
|
|
273
|
+
const discharge = dischargeRanges(lines);
|
|
274
|
+
const rowIds = decisionRowIds(lines);
|
|
275
|
+
let inFence = false;
|
|
276
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
277
|
+
const line = lines[i] as string;
|
|
278
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
279
|
+
inFence = !inFence;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (inFence) continue;
|
|
283
|
+
if (discharge.some(([a, b]) => i >= a && i < b)) continue;
|
|
284
|
+
if (/<!--\s*decision-marker:\s*ignore\s*-->/.test(line)) continue;
|
|
285
|
+
const ref = DISCHARGE_REF_RE.exec(line);
|
|
286
|
+
if (ref && rowIds.has(ref[1] as string)) continue;
|
|
287
|
+
for (const { re, label } of UNRESOLVED_PATTERNS) {
|
|
288
|
+
if (re.test(line)) {
|
|
289
|
+
out.push({
|
|
290
|
+
file: rel,
|
|
291
|
+
line: i + 1,
|
|
292
|
+
reason:
|
|
293
|
+
`unresolved decision marker \`${label}\` in a \`ready\` roadmap — ` +
|
|
294
|
+
`close it and record the answer as a \`## Decisions\` row`,
|
|
295
|
+
});
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function checkFile(rel: string, text: string): Violation[] {
|
|
304
|
+
const lines = text.split('\n');
|
|
305
|
+
const status = readStatus(text);
|
|
306
|
+
const out: Violation[] = [...checkOwnershipColumn(rel, lines)];
|
|
307
|
+
if (status === 'ready') {
|
|
308
|
+
out.push(...checkUnresolvedMarkers(rel, lines));
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// --------------------------------------------------------------------------
|
|
314
|
+
// CLI.
|
|
315
|
+
// --------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
function _repoRoot(): string {
|
|
318
|
+
return path.resolve(path.dirname(_HERE), '..', '..');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function listCorpus(root: string): string[] {
|
|
322
|
+
let entries: fs.Dirent[];
|
|
323
|
+
try {
|
|
324
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
325
|
+
} catch {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
return entries
|
|
329
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md') && !EXCLUDED_NAMES_RE.test(e.name))
|
|
330
|
+
.map((e) => path.join(root, e.name))
|
|
331
|
+
.sort();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function _usage(): string {
|
|
335
|
+
return (
|
|
336
|
+
`usage: ${_PROG} [-h] [--quiet] [--json] [--self-test]\n\n` +
|
|
337
|
+
`Validate the \`## Decisions\` contract on top-level agents/roadmaps/*.md.\n`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function main(argv?: readonly string[]): number {
|
|
342
|
+
const args = argv ?? process.argv.slice(2);
|
|
343
|
+
let quiet = false;
|
|
344
|
+
let json = false;
|
|
345
|
+
let selfTest = false;
|
|
346
|
+
const unknown: string[] = [];
|
|
347
|
+
for (const a of args) {
|
|
348
|
+
if (a === '-h' || a === '--help') {
|
|
349
|
+
process.stdout.write(_usage());
|
|
350
|
+
return 0;
|
|
351
|
+
} else if (a === '--quiet') quiet = true;
|
|
352
|
+
else if (a === '--json') json = true;
|
|
353
|
+
else if (a === '--self-test') selfTest = true;
|
|
354
|
+
else unknown.push(a);
|
|
355
|
+
}
|
|
356
|
+
if (unknown.length) {
|
|
357
|
+
process.stderr.write(`${_usage()}${_PROG}: error: unrecognized arguments: ${unknown.join(' ')}\n`);
|
|
358
|
+
return 2;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (selfTest) return _selfTest();
|
|
362
|
+
|
|
363
|
+
const repo = _repoRoot();
|
|
364
|
+
const root = path.join(repo, 'agents', 'roadmaps');
|
|
365
|
+
const files = listCorpus(root);
|
|
366
|
+
try {
|
|
367
|
+
assertScanned({
|
|
368
|
+
gate: _PROG,
|
|
369
|
+
scanned: files.length,
|
|
370
|
+
units: 'roadmap file(s)',
|
|
371
|
+
roots: ['agents/roadmaps'],
|
|
372
|
+
});
|
|
373
|
+
} catch (e) {
|
|
374
|
+
if (e instanceof DeadScopeError) {
|
|
375
|
+
process.stderr.write(`❌ ${e.message}\n`);
|
|
376
|
+
return 2;
|
|
377
|
+
}
|
|
378
|
+
throw e;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const violations: Violation[] = [];
|
|
382
|
+
for (const f of files) {
|
|
383
|
+
const rel = path.relative(repo, f);
|
|
384
|
+
violations.push(...checkFile(rel, fs.readFileSync(f, 'utf-8')));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (json) {
|
|
388
|
+
process.stdout.write(JSON.stringify(violations, null, 2) + '\n');
|
|
389
|
+
return violations.length ? 1 : 0;
|
|
390
|
+
}
|
|
391
|
+
if (violations.length) {
|
|
392
|
+
process.stderr.write(`❌ ${_PROG}: ${violations.length} violation(s)\n`);
|
|
393
|
+
for (const v of violations) {
|
|
394
|
+
process.stderr.write(` ${v.file}:${v.line} — ${v.reason}\n`);
|
|
395
|
+
}
|
|
396
|
+
return 1;
|
|
397
|
+
}
|
|
398
|
+
if (!quiet) {
|
|
399
|
+
process.stdout.write(`✅ ${_PROG}: ${files.length} roadmap file(s) scanned, 0 violations\n`);
|
|
400
|
+
}
|
|
401
|
+
return 0;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Both directions, in-process: a bad table must red, a good one must not. */
|
|
405
|
+
function _selfTest(): number {
|
|
406
|
+
const bad = [
|
|
407
|
+
'## Decisions',
|
|
408
|
+
'',
|
|
409
|
+
'| ID | ownership | resolved by | decision | evidence | revisit if |',
|
|
410
|
+
'|---|---|---|---|---|---|',
|
|
411
|
+
'| D1 | high impact | owner | x | y | z |',
|
|
412
|
+
'',
|
|
413
|
+
].join('\n');
|
|
414
|
+
const good = [
|
|
415
|
+
'## Decisions',
|
|
416
|
+
'',
|
|
417
|
+
'| ID | ownership | resolved by | decision | evidence | revisit if |',
|
|
418
|
+
'|---|---|---|---|---|---|',
|
|
419
|
+
'| D1 | critical-technical | council:rec-1 | x | y | z |',
|
|
420
|
+
'',
|
|
421
|
+
].join('\n');
|
|
422
|
+
const failures: string[] = [];
|
|
423
|
+
if (checkFile('bad.md', bad).length === 0) failures.push('an impact-axis class did not red');
|
|
424
|
+
if (checkFile('good.md', good).length !== 0) failures.push('a valid ownership row red');
|
|
425
|
+
if (checkFile('m.md', 'status: ready\n\nPick the codec: TBD\n').length === 0) {
|
|
426
|
+
failures.push('an unresolved marker did not red');
|
|
427
|
+
}
|
|
428
|
+
const resolved = [
|
|
429
|
+
'Pick the codec: TBD — closed as D1 below.',
|
|
430
|
+
'',
|
|
431
|
+
'## Decisions',
|
|
432
|
+
'',
|
|
433
|
+
'| ID | ownership | resolved by | decision | evidence | revisit if |',
|
|
434
|
+
'|---|---|---|---|---|---|',
|
|
435
|
+
'| D1 | contested-technical | agent | opus | bench | the bench changes |',
|
|
436
|
+
'',
|
|
437
|
+
].join('\n');
|
|
438
|
+
if (checkFile('m2.md', resolved).length !== 0) {
|
|
439
|
+
failures.push('a marker naming its own closure row red');
|
|
440
|
+
}
|
|
441
|
+
const dangling = 'Pick the codec: TBD — closed as D9.\n';
|
|
442
|
+
if (checkFile('m3.md', dangling).length === 0) {
|
|
443
|
+
failures.push('a marker naming a row that does not exist did not red');
|
|
444
|
+
}
|
|
445
|
+
if (failures.length) {
|
|
446
|
+
for (const f of failures) process.stderr.write(`❌ ${_PROG} --self-test: ${f}\n`);
|
|
447
|
+
return 1;
|
|
448
|
+
}
|
|
449
|
+
process.stdout.write(`✅ ${_PROG} --self-test: both directions hold\n`);
|
|
450
|
+
return 0;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function _isCliEntry(): boolean {
|
|
454
|
+
if (!process.argv[1]) return false;
|
|
455
|
+
try {
|
|
456
|
+
return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
457
|
+
} catch {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
463
|
+
process.exitCode = main();
|
|
464
|
+
}
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* `--strict` upgrades warnings to errors.
|
|
28
28
|
*/
|
|
29
29
|
import fs from "node:fs";
|
|
30
|
+
import { PLATFORM_METADATA_KEYS } from "./_lib/hook_platform_keys.js";
|
|
30
31
|
import path from "node:path";
|
|
31
32
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
32
33
|
import { parse as parseYaml } from "yaml";
|
|
@@ -452,10 +453,20 @@ function _check_platforms(
|
|
|
452
453
|
errors.push(`platforms.${plat}: must be mapping or null`);
|
|
453
454
|
continue;
|
|
454
455
|
}
|
|
456
|
+
const askShape = block["ask"];
|
|
457
|
+
if (askShape !== undefined && askShape !== "native" && askShape !== "text") {
|
|
458
|
+
errors.push(
|
|
459
|
+
`platforms.${plat}.ask: must be 'native' or 'text' ` +
|
|
460
|
+
`(got ${JSON.stringify(askShape)})`,
|
|
461
|
+
);
|
|
462
|
+
}
|
|
455
463
|
if (block["fallback_only"]) {
|
|
456
464
|
continue; // Copilot — intentional, no event surface
|
|
457
465
|
}
|
|
458
466
|
for (const [event, names] of Object.entries(block)) {
|
|
467
|
+
if (PLATFORM_METADATA_KEYS.has(event)) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
459
470
|
if (!EVENT_VOCABULARY.has(event)) {
|
|
460
471
|
errors.push(
|
|
461
472
|
`platforms.${plat}.${event}: unknown event ` +
|