@event4u/agent-config 14.18.0 → 14.20.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 +120 -129
- package/README.md +71 -149
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +44 -43
- package/dist/agent-src/commands/review/changes.md +19 -19
- package/dist/agent-src/commands/review.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +37 -4
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +2 -2
- package/dist/agent-src/rules/guidelines.md +1 -1
- package/dist/agent-src/rules/session-canary.md +2 -2
- package/dist/agent-src/rules/token-efficiency.md +1 -1
- package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
- package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
- package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
- package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
- package/dist/agent-src/skills/design-review/SKILL.md +1 -1
- package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
- package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
- package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
- package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
- package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
- package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +17 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +14 -1
- package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
- package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
- package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
- package/dist/cli/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- package/dist/cli/mcp/dispatch.js +10 -0
- package/dist/cli/mcp/stdio.js +33 -2
- package/dist/cli/mcp/telemetry.js +227 -0
- package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-C6IPZRDA.js} +484 -353
- package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-NUMQHLS5.js} +304 -136
- package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-PI2GU3DP.js} +163 -1
- package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
- package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
- package/dist/cli-delegate/cmd_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +489 -121
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +42 -12
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_session_recycle.js +59 -902
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +34 -33
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +4455 -3025
- package/dist/install/install.mjs +975 -689
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +2087 -1134
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/dist/scripts/mcp_server/telemetry.js +148 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/naming.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
- package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
- package/docs/guidelines/design-antipatterns.md +87 -64
- package/docs/guidelines/redundancy-taxonomy.md +28 -0
- package/package.json +1 -1
- package/src/agent-src/README.md +1 -1
- package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
- package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
- package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
- package/src/config/assurance-capability-registry.json +9 -0
- package/src/config/ci-local-parity.yml +6 -0
- package/src/config/gate-coverage.yml +225 -0
- package/src/config/gate-violation-baselines.json +18 -6
- package/src/config/release-gate-locality.yml +12 -0
- package/src/scripts/_cli/cmd_doctor.ts +2 -2
- package/src/scripts/_cli/cmd_handoff.ts +1 -1
- package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
- package/src/scripts/_cli/cmd_update.ts +1 -1
- package/src/scripts/_cli/cmd_versions.ts +1 -1
- package/src/scripts/_lib/adr_frontmatter.ts +44 -0
- package/src/scripts/_lib/audit_field_provenance.ts +44 -3
- package/src/scripts/_lib/consequence_objects.ts +152 -0
- package/src/scripts/_lib/exec_evidence.ts +7 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
- package/src/scripts/_lib/host_capability.ts +38 -0
- package/src/scripts/_lib/host_permission_checks.ts +251 -0
- package/src/scripts/_lib/install_reach_checks.ts +33 -1
- package/src/scripts/_lib/mcp_bridge.ts +222 -10
- package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
- package/src/scripts/_lib/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- package/src/scripts/_lib/orchestration_record.ts +23 -1
- package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
- package/src/scripts/_lib/release_highlights.ts +93 -0
- package/src/scripts/_lib/release_material.ts +64 -1
- package/src/scripts/_lib/release_position.ts +96 -0
- package/src/scripts/_lib/review_skipped_record.ts +17 -1
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
- package/src/scripts/_lib/security_lint.ts +179 -7
- package/src/scripts/_lib/self_repair.ts +141 -6
- package/src/scripts/_lib/self_repair_store.ts +273 -7
- package/src/scripts/_lib/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/subagent_capsule.ts +155 -3
- package/src/scripts/_lib/surface.ts +86 -0
- package/src/scripts/_lib/ui_authority.ts +116 -0
- package/src/scripts/adr/regenerate_index.ts +54 -1
- package/src/scripts/adr_cite_check.ts +71 -15
- package/src/scripts/ai_council/clients.ts +1 -1
- package/src/scripts/autonomy_friction_corpus.ts +229 -0
- package/src/scripts/build_proof.ts +15 -2
- package/src/scripts/build_rule_trigger_matrix.ts +5 -0
- package/src/scripts/check_beta_review_markers.ts +141 -17
- package/src/scripts/check_enforcement_coverage.ts +1 -1
- package/src/scripts/check_enforcement_denominator.ts +121 -11
- package/src/scripts/check_finding_dispositions.ts +66 -9
- package/src/scripts/check_gate_coverage.ts +96 -0
- package/src/scripts/check_mcp_doc_drift.ts +217 -0
- package/src/scripts/check_pack_size.ts +321 -3
- package/src/scripts/check_release_highlights.ts +74 -1
- package/src/scripts/check_skill_activation_claim.ts +179 -0
- package/src/scripts/check_trigger_evals.ts +1 -1
- package/src/scripts/conformance_scan.ts +44 -3
- package/src/scripts/explain_run.ts +1 -1
- package/src/scripts/generate_capability_matrix.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +75 -1
- package/src/scripts/handoff_context_hook.ts +54 -7
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +14 -11
- package/src/scripts/hooks/augment-dispatcher.sh +1 -1
- package/src/scripts/hooks/category_a.ts +291 -0
- package/src/scripts/hooks/cline-dispatcher.sh +1 -1
- package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
- package/src/scripts/hooks/dispatch_hook.ts +17 -22
- package/src/scripts/hooks/git_command_classifier.ts +156 -1
- package/src/scripts/hooks/host_lowering.ts +215 -0
- package/src/scripts/hooks/host_lowering.yaml +185 -0
- package/src/scripts/hooks/host_semantics.ts +105 -37
- package/src/scripts/hooks/journal_record_hook.ts +10 -0
- package/src/scripts/hooks/permission_gate.ts +117 -0
- package/src/scripts/hooks/session_eol_hook.ts +18 -7
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install.ts +57 -86
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/lint_agent_security.ts +316 -25
- package/src/scripts/lint_confusables.ts +42 -6
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_handoffs.ts +0 -132
- package/src/scripts/lint_hidden_unicode.ts +49 -7
- package/src/scripts/lint_hook_manifest.ts +87 -3
- package/src/scripts/lint_instruction_smuggling.ts +42 -6
- package/src/scripts/lint_mcp_config_security.ts +42 -6
- package/src/scripts/lint_motion_authority_drift.ts +731 -0
- package/src/scripts/lint_readme_size.ts +158 -12
- package/src/scripts/lint_reasoning_fixtures.ts +216 -0
- package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
- package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
- package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/mcp_server/resources.ts +41 -0
- package/src/scripts/mcp_server/telemetry.ts +19 -4
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/release.ts +24 -24
- package/src/scripts/release_publication.ts +6 -2
- package/src/scripts/report_skill_activation.ts +319 -7
- package/src/scripts/schemas/review-findings.schema.json +5 -0
- package/src/scripts/schemas/skill.schema.json +2 -2
- package/src/scripts/self_repair_cli.ts +31 -1
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/skill_scout.ts +69 -0
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
- package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
- package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
|
@@ -109,7 +109,39 @@ export interface WorkerCapsule {
|
|
|
109
109
|
* affordable precisely here — an envelope is consume-once and expires in
|
|
110
110
|
* hours, so the migration window is a session, not a release.
|
|
111
111
|
*/
|
|
112
|
-
export const CAPSULE_SCHEMA_VERSION =
|
|
112
|
+
export const CAPSULE_SCHEMA_VERSION = 4;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Versions a reader ACCEPTS. Version 4 is the first bump that is purely
|
|
116
|
+
* ADDITIVE, and that is why it widens the accepted set instead of replacing it.
|
|
117
|
+
*
|
|
118
|
+
* The compatibility contract is stated here rather than inferred.
|
|
119
|
+
*
|
|
120
|
+
* - **Scope.** The version is ENVELOPE-WIDE, not per-variant. One number
|
|
121
|
+
* describes the whole schema module, so a variant added later does not
|
|
122
|
+
* silently re-date records of another variant.
|
|
123
|
+
* - **New reader, old record.** A v3 record validates. Its required set is v3's
|
|
124
|
+
* — the fields added at v4 are not demanded of it, because demanding them
|
|
125
|
+
* would be a retroactive requirement on records already written.
|
|
126
|
+
* - **Old reader, new record.** A v3-only reader rejects a v4 record with the
|
|
127
|
+
* version violation named. That is the pre-existing loud-discard behaviour and
|
|
128
|
+
* is affordable for the same reason v3 gave: a record is consume-once and
|
|
129
|
+
* expires in {@link RECYCLE_MAX_AGE_HOURS} hours, so the migration window is a
|
|
130
|
+
* session, not a release.
|
|
131
|
+
* - **Unknown version.** Anything outside this set is rejected with the set
|
|
132
|
+
* printed. Absent is not a pass — an undated record is not "probably current".
|
|
133
|
+
* - **Unknown variant.** Rejected by `variant` check, never coerced. A reader
|
|
134
|
+
* that does not know a variant must not guess at its required fields.
|
|
135
|
+
* - **Rollback.** Reverting to a v3-only reader after v4 records exist discards
|
|
136
|
+
* those records loudly rather than mis-reading them; the loss is one session's
|
|
137
|
+
* resume, which is the same loss as no record at all.
|
|
138
|
+
*
|
|
139
|
+
* Adding a required field is what forced v3 to be a hard break. v4 adds
|
|
140
|
+
* `successful_approaches`, `open_questions` and `predecessor`, and requires the
|
|
141
|
+
* first and third **only of records that declare themselves v4** — so no
|
|
142
|
+
* previously valid record becomes invalid.
|
|
143
|
+
*/
|
|
144
|
+
export const ACCEPTED_CAPSULE_VERSIONS: readonly number[] = [3, 4];
|
|
113
145
|
|
|
114
146
|
/** The two CHECKPOINT variants sharing this schema. */
|
|
115
147
|
export const CAPSULE_VARIANTS = ['worker', 'main_session'] as const;
|
|
@@ -280,6 +312,30 @@ export function validateCapsule(input: unknown): string[] {
|
|
|
280
312
|
* wire gate; this variant is consumed from a FILE by
|
|
281
313
|
* `handoff_context_hook`, so the module itself must be the strict gate.
|
|
282
314
|
*/
|
|
315
|
+
/**
|
|
316
|
+
* The four questions a resuming session asks, and the field that answers each —
|
|
317
|
+
* written down rather than left implicit, because the gap that produced
|
|
318
|
+
* `successful_approaches` stayed invisible for exactly as long as the mapping
|
|
319
|
+
* was unwritten: three of the four had a field, the fourth had nothing, and no
|
|
320
|
+
* reader of the schema had a reason to notice.
|
|
321
|
+
*
|
|
322
|
+
* *What was it about?* is answered directly by `task`, with `summary` carrying
|
|
323
|
+
* the one-line outcome. *What was the goal?* is answered only by PROXY:
|
|
324
|
+
* `acceptance_criteria` states what **done** means, which is the goal expressed
|
|
325
|
+
* as a test rather than as an intention, and `next_task` narrows it to the
|
|
326
|
+
* successor's first move. *What did NOT work?* is `failed_approaches`, direct,
|
|
327
|
+
* required, explicit `none`. *What DID work?* is `successful_approaches`,
|
|
328
|
+
* direct, required at version 4, explicit `none`.
|
|
329
|
+
*
|
|
330
|
+
* The one proxy is labelled a proxy on purpose. A goal can be met by criteria
|
|
331
|
+
* nobody wrote down, so reading `acceptance_criteria` as the goal itself is the
|
|
332
|
+
* error the label exists to prevent.
|
|
333
|
+
*
|
|
334
|
+
* Everything else the schema carries — `remaining`, `not_carried_forward`,
|
|
335
|
+
* `constraints`, `decisions`, `open_questions`, the drift anchor — answers a
|
|
336
|
+
* question the four do not ask. That is not surplus; it is simply outside this
|
|
337
|
+
* mapping.
|
|
338
|
+
*/
|
|
283
339
|
export interface MainSessionRecycleEnvelope {
|
|
284
340
|
/** Must be {@link CAPSULE_SCHEMA_VERSION}. */
|
|
285
341
|
capsule_version: number;
|
|
@@ -371,6 +427,59 @@ export interface MainSessionRecycleEnvelope {
|
|
|
371
427
|
* successor re-burning a recorded dead end.
|
|
372
428
|
*/
|
|
373
429
|
failed_approaches: string[];
|
|
430
|
+
/**
|
|
431
|
+
* Approaches this session tried and KEPT — "did X, it worked because Y".
|
|
432
|
+
*
|
|
433
|
+
* **The counterpart `failed_approaches` did not have**, and its absence was
|
|
434
|
+
* a structural defect rather than an omission: a record that can say what to
|
|
435
|
+
* avoid and cannot say what to repeat is pessimistic by construction, and a
|
|
436
|
+
* successor re-derives every working decision from scratch while being
|
|
437
|
+
* warned off the dead ends.
|
|
438
|
+
*
|
|
439
|
+
* Required at capsule version 4 with at least one entry, for exactly the
|
|
440
|
+
* reason `failed_approaches` is: a session that kept nothing writes the
|
|
441
|
+
* single entry `none` explicitly, so "nothing worked" stays distinguishable
|
|
442
|
+
* from "nobody wrote it down". An optional field would re-create the silence
|
|
443
|
+
* one layer down.
|
|
444
|
+
*/
|
|
445
|
+
successful_approaches?: string[];
|
|
446
|
+
/**
|
|
447
|
+
* Questions this session could not settle — the successor's first reading
|
|
448
|
+
* list. The worker variant has carried `open_risks` since v1
|
|
449
|
+
* (`WorkerCapsule.open_risks`) and the handoff template asks for open
|
|
450
|
+
* questions in prose; the session variant had neither, so the one thing a
|
|
451
|
+
* resuming session most needs to not silently drop had no field.
|
|
452
|
+
*
|
|
453
|
+
* Optional, unlike `successful_approaches`: an empty question list is not
|
|
454
|
+
* the same asymmetry — a record with nothing open is a complete claim, and
|
|
455
|
+
* `remaining` already carries the work.
|
|
456
|
+
*/
|
|
457
|
+
open_questions?: string[];
|
|
458
|
+
/**
|
|
459
|
+
* The session that WROTE this record.
|
|
460
|
+
*
|
|
461
|
+
* Added with `predecessor` and not separable from it: an edge needs two
|
|
462
|
+
* endpoints, and before v4 a record carried neither. `workspace` answers
|
|
463
|
+
* "which checkout", which is what the identity check needs; it cannot
|
|
464
|
+
* answer "which session", which is what a chain needs.
|
|
465
|
+
*/
|
|
466
|
+
session_id?: string;
|
|
467
|
+
/**
|
|
468
|
+
* The session this one continues, or the explicit string `none`.
|
|
469
|
+
*
|
|
470
|
+
* Required at capsule version 4. Before it, `grep -rn
|
|
471
|
+
* "predecessor_session\|lineage_id" src/` returned 0 and the reader
|
|
472
|
+
* injected whatever record was lying at the path — which is not the same
|
|
473
|
+
* question as "which record is mine" that the session key answers. A key
|
|
474
|
+
* says whose record this is; a predecessor edge says which record this
|
|
475
|
+
* session should be reading.
|
|
476
|
+
*
|
|
477
|
+
* `none` is a STATED ABSENCE, never an empty string or a missing field. The
|
|
478
|
+
* first session in a workspace has no predecessor and says so, so a reader
|
|
479
|
+
* never waits on something that will not arrive — and a NAMED predecessor
|
|
480
|
+
* that is absent is a refusal, never a fall-through to a different record.
|
|
481
|
+
*/
|
|
482
|
+
predecessor?: string;
|
|
374
483
|
/**
|
|
375
484
|
* Drift anchor — all three, or none of them is one. Identity is the
|
|
376
485
|
* canonicalized remote (or the realpath of the common git dir when there
|
|
@@ -615,6 +724,10 @@ const RECYCLE_ENVELOPE_KEYS: ReadonlySet<string> = new Set([
|
|
|
615
724
|
'next_task',
|
|
616
725
|
'suggested_skills',
|
|
617
726
|
'failed_approaches',
|
|
727
|
+
'successful_approaches',
|
|
728
|
+
'open_questions',
|
|
729
|
+
'predecessor',
|
|
730
|
+
'session_id',
|
|
618
731
|
'repo_identity',
|
|
619
732
|
'branch',
|
|
620
733
|
'head',
|
|
@@ -644,9 +757,16 @@ export function validateRecycleEnvelope(input: unknown): string[] {
|
|
|
644
757
|
}
|
|
645
758
|
}
|
|
646
759
|
|
|
647
|
-
|
|
648
|
-
|
|
760
|
+
const version = e['capsule_version'];
|
|
761
|
+
if (typeof version !== 'number' || !ACCEPTED_CAPSULE_VERSIONS.includes(version)) {
|
|
762
|
+
errors.push(`capsule_version must be one of ${ACCEPTED_CAPSULE_VERSIONS.join(' | ')}`);
|
|
649
763
|
}
|
|
764
|
+
// Requiredness is version-conditional, and only in the additive direction:
|
|
765
|
+
// a v4 record owes the v4 fields, a v3 record owes exactly what v3 owed.
|
|
766
|
+
// Demanding the new fields of an already-written record would be a
|
|
767
|
+
// retroactive requirement, which is the one thing the recorded schema lock
|
|
768
|
+
// forbids ("add fields or variants, never repurpose or remove one").
|
|
769
|
+
const atV4 = version === 4;
|
|
650
770
|
if (e['variant'] !== 'main_session') {
|
|
651
771
|
errors.push("variant must be 'main_session'");
|
|
652
772
|
}
|
|
@@ -697,6 +817,38 @@ export function validateRecycleEnvelope(input: unknown): string[] {
|
|
|
697
817
|
);
|
|
698
818
|
}
|
|
699
819
|
|
|
820
|
+
// Version 4 additions. `successful_approaches` mirrors `failed_approaches`
|
|
821
|
+
// exactly — required with >= 1 entry, `none` written explicitly — because
|
|
822
|
+
// the asymmetry between the two is the defect the field exists to remove,
|
|
823
|
+
// and an optional counterpart to a required field re-creates it.
|
|
824
|
+
checkList(errors, 'successful_approaches', e['successful_approaches'], MAX_LINE_CHARS, atV4);
|
|
825
|
+
if (Array.isArray(e['successful_approaches']) && e['successful_approaches'].length === 0) {
|
|
826
|
+
errors.push(
|
|
827
|
+
'successful_approaches must carry at least one entry — write "none" explicitly, so ' +
|
|
828
|
+
'silence and "nothing worked" stay distinguishable',
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
checkList(errors, 'open_questions', e['open_questions'], MAX_LINE_CHARS, false);
|
|
832
|
+
|
|
833
|
+
// The predecessor edge (Phase 2.3). Required at v4, and an EMPTY value is
|
|
834
|
+
// not an answer: `none` is a claim ("this session starts a chain"), absence
|
|
835
|
+
// is a reader waiting for something that will not arrive.
|
|
836
|
+
const predecessor = e['predecessor'];
|
|
837
|
+
if (predecessor === undefined) {
|
|
838
|
+
if (atV4) {
|
|
839
|
+
errors.push(
|
|
840
|
+
'predecessor is required — name the session this one continues, or write "none" ' +
|
|
841
|
+
'explicitly for the first session in a workspace',
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
} else if (!isShortLine(predecessor, MAX_REF_CHARS)) {
|
|
845
|
+
errors.push(`predecessor must be a single line of 1–${MAX_REF_CHARS} chars ("none" when there is no predecessor)`);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (e['session_id'] !== undefined && !isShortLine(e['session_id'], MAX_REF_CHARS)) {
|
|
849
|
+
errors.push(`session_id must be a single line of 1–${MAX_REF_CHARS} chars`);
|
|
850
|
+
}
|
|
851
|
+
|
|
700
852
|
// Drift anchor (Phase 3.2). Optional as a set — an older envelope simply
|
|
701
853
|
// has none — but each present field must still be a short line.
|
|
702
854
|
for (const field of ['repo_identity', 'branch', 'head', 'status_summary', 'last_verify'] as const) {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* surface — the IDE / CLI / cloud dimension of a dispatch, as an ENVELOPE
|
|
3
|
+
* field rather than a platform key.
|
|
4
|
+
*
|
|
5
|
+
* WHY A FIELD AND NOT A PLATFORM. `_lib/session_register.ts` records that the
|
|
6
|
+
* capability lattice has no IDE/CLI dimension, which is why it reports cursor
|
|
7
|
+
* covered whether or not its per-turn slots fire there. Splitting `cursor`
|
|
8
|
+
* into `cursor-ide` / `cursor-cli` would change `--platform`, the eight
|
|
9
|
+
* platform identifiers, and every manifest row keyed on them. A field changes
|
|
10
|
+
* none of those and is still the thing `host_lowering.yaml`'s `(host, surface,
|
|
11
|
+
* slot)` key needs.
|
|
12
|
+
*
|
|
13
|
+
* WHAT THIS CAN AND CANNOT DECIDE — the honest half.
|
|
14
|
+
*
|
|
15
|
+
* `unknown` is the default and, today, very nearly the only answer. Nothing in
|
|
16
|
+
* this tree distinguishes an IDE session from a CLI one on any host: no host
|
|
17
|
+
* payload observed here carries such a marker, and inferring it from a TTY or
|
|
18
|
+
* a terminal name would be a guess wearing a function's authority — the exact
|
|
19
|
+
* move the surrounding roadmap exists to remove. So there is no `ide` or `cli`
|
|
20
|
+
* branch. When a host is observed to send a distinguishing marker, it is added
|
|
21
|
+
* to {@link CLOUD_PAYLOAD_MARKERS}'s sibling with a dated citation, and the
|
|
22
|
+
* branch arrives with the evidence rather than ahead of it.
|
|
23
|
+
*
|
|
24
|
+
* The `cloud` branch is a DECLARED VOCABULARY, not an observation. This package
|
|
25
|
+
* will honour these payload keys if a host sends one; none has been observed
|
|
26
|
+
* sending one. That distinction is the point: the mechanism is real and tested,
|
|
27
|
+
* the host claim is absent and says so.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** The closed surface vocabulary. `unknown` is the safe default. */
|
|
31
|
+
export const SURFACES = ['ide', 'cli', 'cloud', 'unknown'] as const;
|
|
32
|
+
export type Surface = (typeof SURFACES)[number];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Payload keys whose truthy presence declares a background / cloud agent.
|
|
36
|
+
*
|
|
37
|
+
* Declared, not observed — see the module header. Kept as a list so a host that
|
|
38
|
+
* starts sending one needs no code change, only a citation in this comment.
|
|
39
|
+
*/
|
|
40
|
+
export const CLOUD_PAYLOAD_MARKERS: readonly string[] = [
|
|
41
|
+
'background_agent',
|
|
42
|
+
'is_background',
|
|
43
|
+
'isBackgroundAgent',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/** Explicit override, for a caller that already knows the surface. */
|
|
47
|
+
export const SURFACE_ENV_VAR = 'AGENT_CONFIG_SURFACE';
|
|
48
|
+
|
|
49
|
+
function _isSurface(v: unknown): v is Surface {
|
|
50
|
+
return typeof v === 'string' && (SURFACES as readonly string[]).includes(v);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the surface for one dispatch.
|
|
55
|
+
*
|
|
56
|
+
* Precedence: an explicit environment override, then a declared payload
|
|
57
|
+
* marker, then `unknown`. An unrecognised override value is ignored rather
|
|
58
|
+
* than trusted — a typo must not become a capability claim.
|
|
59
|
+
*/
|
|
60
|
+
export function detectSurface(
|
|
61
|
+
payload: unknown,
|
|
62
|
+
env: Record<string, string | undefined> = process.env,
|
|
63
|
+
): Surface {
|
|
64
|
+
const override = env[SURFACE_ENV_VAR];
|
|
65
|
+
if (_isSurface(override)) return override;
|
|
66
|
+
|
|
67
|
+
if (typeof payload === 'object' && payload !== null && !Array.isArray(payload)) {
|
|
68
|
+
const obj = payload as Record<string, unknown>;
|
|
69
|
+
for (const key of CLOUD_PAYLOAD_MARKERS) {
|
|
70
|
+
if (obj[key]) return 'cloud';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return 'unknown';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Read a persisted or caller-supplied surface value.
|
|
78
|
+
*
|
|
79
|
+
* Tolerant, not a cast: an unrecognised value — a row written by a newer build,
|
|
80
|
+
* a typo, an absent field — reads as `unknown`. Same posture as
|
|
81
|
+
* `readRunTerminalState`, and for the same reason: a value this build cannot
|
|
82
|
+
* place must not fail a write or a read.
|
|
83
|
+
*/
|
|
84
|
+
export function readSurface(v: unknown): Surface {
|
|
85
|
+
return _isSurface(v) ? v : 'unknown';
|
|
86
|
+
}
|
|
@@ -341,3 +341,119 @@ export function operationConflicts(authority: UiAuthority, op: Operation): Confl
|
|
|
341
341
|
}
|
|
342
342
|
return out;
|
|
343
343
|
}
|
|
344
|
+
|
|
345
|
+
/* -------------------------------------------------------------------------- */
|
|
346
|
+
/* Antipattern signal classes against the precedence list */
|
|
347
|
+
/* -------------------------------------------------------------------------- */
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* The five classes a `design-antipatterns.md` catalog row may claim.
|
|
351
|
+
*
|
|
352
|
+
* Kept here, beside the precedence list, rather than in the catalog gate: the
|
|
353
|
+
* question "may this signal overrule what the user supplied" is a precedence
|
|
354
|
+
* question, and precedence lives in exactly one module by construction.
|
|
355
|
+
*/
|
|
356
|
+
export type SignalClass =
|
|
357
|
+
| 'floor'
|
|
358
|
+
| 'invariant'
|
|
359
|
+
| 'craft-presumption'
|
|
360
|
+
| 'style-preference'
|
|
361
|
+
| 'reference-constraint';
|
|
362
|
+
|
|
363
|
+
/** The two classes that are opinions rather than thresholds. */
|
|
364
|
+
export const SOFT_CLASSES = ['craft-presumption', 'style-preference'] as const;
|
|
365
|
+
|
|
366
|
+
export interface SignalVerdict {
|
|
367
|
+
/**
|
|
368
|
+
* `blocks` — refuse the surface. `flags` — surface it as a rebuttable
|
|
369
|
+
* presumption. `yields` — say nothing: something with more authority has
|
|
370
|
+
* already decided this.
|
|
371
|
+
*/
|
|
372
|
+
outcome: 'blocks' | 'flags' | 'yields';
|
|
373
|
+
/** The precedence level that outranked it, numbered as in this file's header. */
|
|
374
|
+
outranked_by: { level: number; source: ProvenanceSource; detail: string } | null;
|
|
375
|
+
reason: string;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Where a soft signal sits against the precedence list.
|
|
380
|
+
*
|
|
381
|
+
* A craft presumption is a good default and nothing more. When the run has a
|
|
382
|
+
* supplied reference (4), a coherent incumbent (5), or a project `DESIGN.md`
|
|
383
|
+
* register (6), that artifact has already decided the question the presumption
|
|
384
|
+
* would re-open, and re-opening it is the redesign-by-taste failure
|
|
385
|
+
* `design-fidelity` exists to stop.
|
|
386
|
+
*
|
|
387
|
+
* A `floor` or an `invariant` is NOT a taste call — an accessibility threshold
|
|
388
|
+
* or a structural correctness rule — so nothing on the list releases it, and a
|
|
389
|
+
* reference artifact that violates one is a finding about the reference.
|
|
390
|
+
*
|
|
391
|
+
* LEVELS 1–3 ARE DELIBERATELY NOT MECHANISED HERE. Explicit user authority, a
|
|
392
|
+
* registered hard constraint, and the surface brief all outrank a presumption a
|
|
393
|
+
* fortiori, but `Signals` carries them only as whole-surface fields
|
|
394
|
+
* (`surface_mode`, `register`, `change_intent`) with no per-pattern statement.
|
|
395
|
+
* Reading a brief's mere presence as blanket permission for every antipattern
|
|
396
|
+
* would silence the catalog on every briefed surface, which is the opposite of
|
|
397
|
+
* what those levels mean. A per-pattern override is `DESIGN.md`'s declared
|
|
398
|
+
* suppression, which is level 6 and IS handled.
|
|
399
|
+
*/
|
|
400
|
+
export function resolveSignal(signalClass: SignalClass, signals: Signals = {}): SignalVerdict {
|
|
401
|
+
if (signalClass === 'floor' || signalClass === 'invariant') {
|
|
402
|
+
return {
|
|
403
|
+
outcome: 'blocks',
|
|
404
|
+
outranked_by: null,
|
|
405
|
+
reason:
|
|
406
|
+
`a ${signalClass} is a threshold, not a preference — no supplied artifact releases it, `
|
|
407
|
+
+ 'and an artifact that violates one is a finding about the artifact',
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (signalClass === 'reference-constraint') {
|
|
412
|
+
const present = signals.reference !== undefined || signals.design_md?.present === true;
|
|
413
|
+
return present
|
|
414
|
+
? {
|
|
415
|
+
outcome: 'blocks',
|
|
416
|
+
outranked_by: null,
|
|
417
|
+
reason: 'the reference or DESIGN.md this constraint is bound to is present in this run, so it carries that artifact\'s own authority',
|
|
418
|
+
}
|
|
419
|
+
: {
|
|
420
|
+
outcome: 'flags',
|
|
421
|
+
outranked_by: null,
|
|
422
|
+
reason: 'the reference this constraint is bound to is not present in this run, so it has no artifact behind it',
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (signals.reference !== undefined) {
|
|
427
|
+
return {
|
|
428
|
+
outcome: 'yields',
|
|
429
|
+
outranked_by: { level: 4, source: 'reference-artifact', detail: signals.reference.path ?? signals.reference.maturity },
|
|
430
|
+
reason: `a ${signalClass} never outranks a supplied reference artifact`,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
if (signals.incumbent?.coherent === true) {
|
|
434
|
+
return {
|
|
435
|
+
outcome: 'yields',
|
|
436
|
+
outranked_by: { level: 5, source: 'incumbent-scan', detail: 'coherent incumbent' },
|
|
437
|
+
reason: `a ${signalClass} never outranks a coherent incumbent`,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
if (signals.design_md?.present === true) {
|
|
441
|
+
return {
|
|
442
|
+
outcome: 'yields',
|
|
443
|
+
outranked_by: { level: 6, source: 'design-md', detail: signals.design_md.register ?? 'register unstated' },
|
|
444
|
+
reason: `a ${signalClass} never outranks a project DESIGN.md register`,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
if (signals.product_md?.present === true) {
|
|
448
|
+
return {
|
|
449
|
+
outcome: 'yields',
|
|
450
|
+
outranked_by: { level: 6, source: 'product-md', detail: signals.product_md.register ?? 'register unstated' },
|
|
451
|
+
reason: `a ${signalClass} never outranks a project PRODUCT.md register`,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
outcome: 'flags',
|
|
456
|
+
outranked_by: null,
|
|
457
|
+
reason: `a ${signalClass} with nothing above it on the precedence list is a rebuttable presumption`,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
@@ -16,7 +16,7 @@ import * as fs from 'node:fs';
|
|
|
16
16
|
import * as path from 'node:path';
|
|
17
17
|
import process from 'node:process';
|
|
18
18
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
19
|
-
import { readAdrAxisCells, readAdrFrontmatterScalars } from '../_lib/adr_frontmatter.js';
|
|
19
|
+
import { dated_trigger_day, readAdrAxisCells, readAdrFrontmatterScalars } from '../_lib/adr_frontmatter.js';
|
|
20
20
|
import { generatedByItalic } from '../_lib/generated_by.js';
|
|
21
21
|
|
|
22
22
|
// ^ADR-(\d{3})-([a-z0-9-]+)\.md$
|
|
@@ -262,6 +262,41 @@ export function scanOutside(repoRoot: string, roots: readonly string[] = OUTSIDE
|
|
|
262
262
|
return out;
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
/**
|
|
266
|
+
* The rows whose `review_trigger` opens with a calendar date.
|
|
267
|
+
*
|
|
268
|
+
* WHY THIS SECTION CARRIES THE DATE AND NOT THE FIRED/NOT-FIRED STATE
|
|
269
|
+
* ------------------------------------------------------------------
|
|
270
|
+
* `regenerate_index --check` byte-compares this file in CI
|
|
271
|
+
* (`.github/workflows/rule-backstops.yml`), so anything rendered here has to be
|
|
272
|
+
* a function of the tree alone. A `fired` / `not-fired` column would be a
|
|
273
|
+
* function of the tree AND the day, which means the index would go stale on a
|
|
274
|
+
* calendar boundary nobody committed and red every open pull request until
|
|
275
|
+
* someone regenerated it. The date is the decidable half; the reader compares
|
|
276
|
+
* it to today, and `adr_cite_check` prints the comparison for a machine.
|
|
277
|
+
*/
|
|
278
|
+
export interface DatedTriggerRow {
|
|
279
|
+
label: string;
|
|
280
|
+
path: string;
|
|
281
|
+
day: string;
|
|
282
|
+
decision: string;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function datedTriggerRows(rows: readonly Row[]): DatedTriggerRow[] {
|
|
286
|
+
const out: DatedTriggerRow[] = [];
|
|
287
|
+
for (const r of rows) {
|
|
288
|
+
const day = dated_trigger_day(r.review_trigger ?? '');
|
|
289
|
+
if (day === null) continue;
|
|
290
|
+
out.push({
|
|
291
|
+
label: 'num' in r ? `ADR-${r.num as string}` : (r.path as string).slice(0, -3),
|
|
292
|
+
path: r.path as string,
|
|
293
|
+
day,
|
|
294
|
+
decision: r.decision ?? r.slug ?? '—',
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return out.sort((a, b) => a.day.localeCompare(b.day) || a.label.localeCompare(b.label));
|
|
298
|
+
}
|
|
299
|
+
|
|
265
300
|
/** Mirror `render(num, leg)`. */
|
|
266
301
|
export function render(num: Row[], leg: Row[], outside: OutsideRow[] = []): string {
|
|
267
302
|
const out = [
|
|
@@ -277,6 +312,24 @@ export function render(num: Row[], leg: Row[], outside: OutsideRow[] = []): stri
|
|
|
277
312
|
if (leg.length > 0) {
|
|
278
313
|
out.push('', '## Unnumbered (legacy)', '', HEAD, ...leg.map(row));
|
|
279
314
|
}
|
|
315
|
+
const dated = datedTriggerRows([...num, ...leg]);
|
|
316
|
+
if (dated.length > 0) {
|
|
317
|
+
out.push(
|
|
318
|
+
'',
|
|
319
|
+
'## Dated review triggers',
|
|
320
|
+
'',
|
|
321
|
+
'A `review_trigger` that opens with a calendar date is the one reopen condition',
|
|
322
|
+
'this repository can decide without interpreting anything, so it is listed where a',
|
|
323
|
+
'reader meets it rather than only in a command they would have to know to run.',
|
|
324
|
+
'Compare the expiry to today; `./scripts-run src/scripts/adr_cite_check <ADR-NNN>`',
|
|
325
|
+
'prints `fired` or `not-fired` for the same date. Every other trigger is semantic',
|
|
326
|
+
'and reports `indeterminate` — a trigger that merely mentions a date is not listed.',
|
|
327
|
+
'',
|
|
328
|
+
'| Expiry | # | Decision |',
|
|
329
|
+
'|---|---|---|',
|
|
330
|
+
...dated.map((d) => `| ${d.day} | [${d.label}](${d.path}) | ${d.decision} |`),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
280
333
|
if (outside.length > 0) {
|
|
281
334
|
out.push(
|
|
282
335
|
'',
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* (measured: ADR-035 still asserts a rejection ADR-232 reopened).
|
|
26
26
|
* - **review_trigger** — verbatim, plus a state.
|
|
27
27
|
*
|
|
28
|
-
* `indeterminate` is a FIRST-CLASS trigger result, not a failure.
|
|
28
|
+
* `indeterminate` is a FIRST-CLASS trigger result, not a failure. Almost every
|
|
29
29
|
* `review_trigger` in the corpus is a semantic condition ("reopen when the
|
|
30
30
|
* capacity premise changes", "if a fifth band appears"), so forcing them to a
|
|
31
31
|
* boolean would convert uncertainty into either permission or blockage. Both
|
|
@@ -33,6 +33,29 @@
|
|
|
33
33
|
* trigger grammar as a prerequisite for exactly this reason. An `indeterminate`
|
|
34
34
|
* result means: this may not be presented as an unqualified lock — route it.
|
|
35
35
|
*
|
|
36
|
+
* THE DATED SUB-CLASS, AND WHY THE 2026-08-19 REJECTION DOES NOT REACH IT
|
|
37
|
+
* ----------------------------------------------------------------------
|
|
38
|
+
* The sentence above used to open "Every `review_trigger` in the corpus is a
|
|
39
|
+
* semantic condition"; it is weakened rather than deleted because the claim was
|
|
40
|
+
* true when written and one record has since falsified it.
|
|
41
|
+
*
|
|
42
|
+
* What those two seats rejected was forcing SEMANTIC conditions to a boolean.
|
|
43
|
+
* A trigger that OPENS with a calendar date is not one: `fired` / `not-fired`
|
|
44
|
+
* is decided by comparing that date to `asOf()`, which is arithmetic and
|
|
45
|
+
* carries no interpretation of the tree, so a second reader of the same commit
|
|
46
|
+
* gets the same answer. That is the entire carve-out — a leading
|
|
47
|
+
* `Expiry YYYY-MM-DD`, or a bare leading ISO date, and nothing else. A
|
|
48
|
+
* condition that merely MENTIONS a date somewhere in its prose ("revisit at the
|
|
49
|
+
* 2026-11-10 review date, or earlier on either observable event") is a semantic
|
|
50
|
+
* condition and stays `indeterminate`, which is what keeps this from being the
|
|
51
|
+
* rejected grammar by increments.
|
|
52
|
+
*
|
|
53
|
+
* `dated-unparsed` exists so the carve-out cannot fail silently. A trigger that
|
|
54
|
+
* announces itself as dated and whose date this parser cannot read is a defect
|
|
55
|
+
* in the record or in the parser; returning `indeterminate` for it would hide
|
|
56
|
+
* that defect behind the exact state every trigger had before the carve-out
|
|
57
|
+
* existed, where no test could ever see it as a regression.
|
|
58
|
+
*
|
|
36
59
|
* It also prints the two descriptive axes (`provenance`, `evidence`) and
|
|
37
60
|
* `authority_basis`, and for an accepted E0/E1 record that does NOT carry
|
|
38
61
|
* `authority_basis: owner_intent` it prints
|
|
@@ -73,12 +96,16 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
73
96
|
import {
|
|
74
97
|
type AdrFrontmatter,
|
|
75
98
|
authorityBasisOf,
|
|
99
|
+
dated_trigger_day,
|
|
100
|
+
DATED_TRIGGER_PROBE,
|
|
101
|
+
iso_day,
|
|
76
102
|
evidenceOf,
|
|
77
103
|
isLowEvidenceAccepted,
|
|
78
104
|
provenanceOf,
|
|
79
105
|
readAdrFrontmatter,
|
|
80
106
|
readAdrFrontmatterScalars,
|
|
81
107
|
} from './_lib/adr_frontmatter.js';
|
|
108
|
+
import { asOf } from './_lib/as_of.js';
|
|
82
109
|
import { GateLedger } from './_lib/gate_ledger.js';
|
|
83
110
|
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
84
111
|
import { DeadScopeError, reportScanned } from './_lib/scan_scope.js';
|
|
@@ -149,8 +176,12 @@ export const SURFACES_NOT_SCANNED = [
|
|
|
149
176
|
*/
|
|
150
177
|
export const PARTIAL_COVERAGE: readonly string[] = [];
|
|
151
178
|
|
|
152
|
-
/**
|
|
153
|
-
|
|
179
|
+
/**
|
|
180
|
+
* A trigger state. `none` means the ADR never recorded a reopen condition;
|
|
181
|
+
* `dated-unparsed` means it recorded one that announces a date this parser
|
|
182
|
+
* could not read, which is a defect and not an answer.
|
|
183
|
+
*/
|
|
184
|
+
export type TriggerState = 'none' | 'indeterminate' | 'fired' | 'not-fired' | 'dated-unparsed';
|
|
154
185
|
|
|
155
186
|
export interface CiteResult {
|
|
156
187
|
ref: string;
|
|
@@ -469,12 +500,21 @@ export function amendment_blocks(body: string): string[] {
|
|
|
469
500
|
/**
|
|
470
501
|
* A trigger's state, from what is mechanically decidable and nothing else.
|
|
471
502
|
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
503
|
+
* Two clauses return a decided answer, and both read a leading calendar date
|
|
504
|
+
* and nothing else — see `DATED_TRIGGER_RE` for why the anchor is the safety
|
|
505
|
+
* property. Every semantic trigger still returns `indeterminate`, which is a
|
|
506
|
+
* first-class result here and not a failure.
|
|
507
|
+
*
|
|
508
|
+
* The clock is read ONLY when a dated trigger is actually present — `asOf()`
|
|
509
|
+
* warns on stderr for an unpinned run, and a corpus with no dated trigger has
|
|
510
|
+
* no verdict that depends on the hour, so announcing one there would be noise
|
|
511
|
+
* on a stream other tools read beside the payload.
|
|
512
|
+
*
|
|
513
|
+
* @param asOfDate the "now" to compare against; defaults to the `asOf()` seam,
|
|
514
|
+
* so a CI run compares against the commit date and is
|
|
515
|
+
* reproducible from the commit alone.
|
|
476
516
|
*/
|
|
477
|
-
export function trigger_state(fm: Record<string, string
|
|
517
|
+
export function trigger_state(fm: Record<string, string>, asOfDate?: Date): TriggerState {
|
|
478
518
|
const t = (fm['review_trigger'] ?? '').trim();
|
|
479
519
|
if (t === '') return 'none';
|
|
480
520
|
// The transitional migration value records that no condition has been
|
|
@@ -485,6 +525,9 @@ export function trigger_state(fm: Record<string, string>): TriggerState {
|
|
|
485
525
|
// nobody has evaluated. Found in completion review; the field did not
|
|
486
526
|
// exist in this file's corpus when the mapping was written.
|
|
487
527
|
if (t.toLowerCase() === 'unclassified') return 'none';
|
|
528
|
+
const day = dated_trigger_day(t);
|
|
529
|
+
if (day !== null) return iso_day(asOfDate ?? asOf()) >= day ? 'fired' : 'not-fired';
|
|
530
|
+
if (DATED_TRIGGER_PROBE.test(t)) return 'dated-unparsed';
|
|
488
531
|
return 'indeterminate';
|
|
489
532
|
}
|
|
490
533
|
|
|
@@ -530,6 +573,15 @@ export function verdict_for(r: Omit<CiteResult, 'verdict'>): string {
|
|
|
530
573
|
if (r.trigger_state === 'none') {
|
|
531
574
|
return 'LIVE, NO REOPEN CONDITION — the ADR records no `review_trigger`, so nothing would ever reopen it on its own. Treat that as a defect in the ADR, not as strength of the lock.';
|
|
532
575
|
}
|
|
576
|
+
if (r.trigger_state === 'fired') {
|
|
577
|
+
return 'LIVE, TRIGGER FIRED — the reopen condition is a date and that date has passed. The record still binds until someone acts on it, and citing it without naming the fired trigger presents a lapsed review as a current one.';
|
|
578
|
+
}
|
|
579
|
+
if (r.trigger_state === 'not-fired') {
|
|
580
|
+
return 'LIVE, TRIGGER NOT YET FIRED — the reopen condition is a date still in the future. This is the one state in which a trigger genuinely adds nothing to the citation, and it stops being true on a known day.';
|
|
581
|
+
}
|
|
582
|
+
if (r.trigger_state === 'dated-unparsed') {
|
|
583
|
+
return 'LIVE, DATED TRIGGER UNREADABLE — the reopen condition announces a date this tool could not parse. That is a defect in the record or in the parser, not a verdict: fix the trigger to open with `Expiry YYYY-MM-DD` rather than reading this as "unknown".';
|
|
584
|
+
}
|
|
533
585
|
return 'LIVE, TRIGGER INDETERMINATE — the reopen condition is semantic and this tool cannot decide it. Not an unqualified lock: evaluate the condition against the current tree and route the result.';
|
|
534
586
|
}
|
|
535
587
|
|
|
@@ -823,21 +875,25 @@ export interface CorpusRow {
|
|
|
823
875
|
/** Does the record carry a `review_trigger` at all? */
|
|
824
876
|
has_trigger: boolean;
|
|
825
877
|
/** Only meaningful when `has_trigger`; see `TRIGGER_VERDICTS`. */
|
|
826
|
-
trigger:
|
|
878
|
+
trigger: TriggerState;
|
|
827
879
|
reopen_policy: string;
|
|
828
880
|
/** Cited anywhere OUTSIDE `docs/decisions/`. */
|
|
829
881
|
cited_outside: boolean;
|
|
830
882
|
}
|
|
831
883
|
|
|
832
884
|
/**
|
|
833
|
-
* The
|
|
885
|
+
* The states a record CARRYING a trigger can be in, and why `none` is not one.
|
|
886
|
+
*
|
|
887
|
+
* `none` is outside the denominator, not a bucket inside it: the survey asks
|
|
888
|
+
* that these sum to "the number of ADRs carrying a trigger", and a record with
|
|
889
|
+
* no trigger carries none.
|
|
834
890
|
*
|
|
835
|
-
* `
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
891
|
+
* `dated-unparsed` was added with the dated sub-class and belongs in the sum
|
|
892
|
+
* for the same reason `none` does not — a record in that state DOES carry a
|
|
893
|
+
* trigger. Leaving it out would have made the sum line report a mismatch, i.e.
|
|
894
|
+
* announced a defect in the survey where the defect is in the ADR.
|
|
839
895
|
*/
|
|
840
|
-
export const TRIGGER_VERDICTS = ['fired', 'not-fired', 'indeterminate'] as const;
|
|
896
|
+
export const TRIGGER_VERDICTS = ['fired', 'not-fired', 'indeterminate', 'dated-unparsed'] as const;
|
|
841
897
|
|
|
842
898
|
/** Every ADR file, surveyed. */
|
|
843
899
|
export function corpus_survey(repo_root: string = REPO_ROOT): CorpusRow[] {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// External-AI clients for the council — ported from the retired Python
|
|
2
2
|
// `src/scripts/ai_council/clients.py` (ADR-200).
|
|
3
3
|
//
|
|
4
|
-
// Mirrors the contract from `scripts/skill_trigger_eval.
|
|
4
|
+
// Mirrors the contract from `src/scripts/skill_trigger_eval.ts`:
|
|
5
5
|
// - Tokens come exclusively from `~/.event4u/agent-config/<provider>.key`
|
|
6
6
|
// (legacy `~/.config/agent-config/<provider>.key` is read as a fallback so
|
|
7
7
|
// pre-2.4 installs keep working until the user moves the files into the new
|