@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
|
@@ -30,14 +30,27 @@ import { createHash } from 'node:crypto';
|
|
|
30
30
|
import { redact_low_impact_entry } from '../ai_council/redact_low_impact_entry.js';
|
|
31
31
|
import { is_kernel_rule } from './kernel_rules.js';
|
|
32
32
|
|
|
33
|
-
/**
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Where a record came from.
|
|
35
|
+
*
|
|
36
|
+
* `user-reported` and `self-detected` both require something to have FIRED — a
|
|
37
|
+
* user complaint, or a deterministic detector. `model-noticed` is the third
|
|
38
|
+
* intake: a model notices an edge case in a rule, or a methodology the user
|
|
39
|
+
* just explained that no skill carries. Nothing fired, and before this source
|
|
40
|
+
* existed such an observation had no record type at all and was handed to a
|
|
41
|
+
* host-native memory tool no reader of this repository can query.
|
|
42
|
+
*
|
|
43
|
+
* It is a SOURCE and not a second store: the record shape, the fingerprint
|
|
44
|
+
* derivation, the noclobber path and the creation cap are all unchanged.
|
|
45
|
+
*/
|
|
46
|
+
export type DefectSource = 'user-reported' | 'self-detected' | 'model-noticed';
|
|
35
47
|
|
|
36
48
|
/** The defect classes a record can carry. One class = one detector. */
|
|
37
49
|
export type DefectClass =
|
|
38
50
|
| 'user-reported'
|
|
39
51
|
| 'council-availability-claim'
|
|
40
|
-
| 'language-mirror'
|
|
52
|
+
| 'language-mirror'
|
|
53
|
+
| 'model-noticed';
|
|
41
54
|
|
|
42
55
|
/**
|
|
43
56
|
* Runtime mirror of `DefectClass`, in declaration order. The upstream issue
|
|
@@ -49,6 +62,7 @@ export const DEFECT_CLASSES: readonly DefectClass[] = [
|
|
|
49
62
|
'user-reported',
|
|
50
63
|
'council-availability-claim',
|
|
51
64
|
'language-mirror',
|
|
65
|
+
'model-noticed',
|
|
52
66
|
];
|
|
53
67
|
|
|
54
68
|
/** How far a record may travel outward. */
|
|
@@ -61,14 +75,94 @@ export interface DefectFinding {
|
|
|
61
75
|
evidence: string;
|
|
62
76
|
/** One line naming what the fix should change. */
|
|
63
77
|
suggested_surface: string;
|
|
78
|
+
/**
|
|
79
|
+
* The assets this record is ABOUT, drawn from a closed vocabulary and
|
|
80
|
+
* resolved against the tree at write time (see `partitionTargets`).
|
|
81
|
+
*
|
|
82
|
+
* `suggested_surface` is free text, so no record could ever be JOINED to
|
|
83
|
+
* the asset it describes — two records about one rule's weakness were two
|
|
84
|
+
* unrelated strings. A target is machine-addressable, which is what makes
|
|
85
|
+
* the per-target occurrence count in `targetCounts` possible at all.
|
|
86
|
+
*
|
|
87
|
+
* An EMPTY list stays legal: a real observation with no identified home is
|
|
88
|
+
* information, not an error. That is deliberately different from an
|
|
89
|
+
* UNRESOLVABLE entry, which lands in `proposes` instead.
|
|
90
|
+
*/
|
|
91
|
+
target?: string[];
|
|
92
|
+
/**
|
|
93
|
+
* Target-shaped strings that do not resolve in the tree — a rule that does
|
|
94
|
+
* not exist yet, a skill the record is proposing. Kept rather than
|
|
95
|
+
* rejected, because "this should exist" is a legitimate observation; kept
|
|
96
|
+
* SEPARATE from `target`, because a silently accepted unresolvable id is
|
|
97
|
+
* worse than free text — it looks joined and joins to nothing.
|
|
98
|
+
*/
|
|
99
|
+
proposes?: string[];
|
|
64
100
|
}
|
|
65
101
|
|
|
102
|
+
/** The four asset kinds a `target` entry may address. */
|
|
103
|
+
export const TARGET_KINDS = ['rule', 'skill', 'command', 'hook'] as const;
|
|
104
|
+
export type TargetKind = (typeof TARGET_KINDS)[number];
|
|
105
|
+
|
|
106
|
+
/** `<kind>:<id>` — closed prefix, id-shaped remainder. Never free text. */
|
|
107
|
+
export const TARGET_RE = /^(rule|skill|command|hook):([a-z0-9][a-z0-9._-]*)$/;
|
|
108
|
+
|
|
109
|
+
/** Shape only — says nothing about whether the id exists. */
|
|
110
|
+
export function isWellFormedTarget(value: string): boolean {
|
|
111
|
+
return TARGET_RE.test(value);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Split a well-formed target into its kind and id, or null. */
|
|
115
|
+
export function parseTarget(value: string): { kind: TargetKind; id: string } | null {
|
|
116
|
+
const m = TARGET_RE.exec(value);
|
|
117
|
+
return m === null ? null : { kind: m[1] as TargetKind, id: m[2] as string };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* What a record is doing now.
|
|
122
|
+
*
|
|
123
|
+
* The pair `open | released` could express neither a DECLINED record (which
|
|
124
|
+
* stayed `open` in the queue line for ever) nor a partially actioned one, so
|
|
125
|
+
* the only way a record left the queue was to be released. Six states, closed:
|
|
126
|
+
*
|
|
127
|
+
* - `open` — queued, nothing decided.
|
|
128
|
+
* - `candidate` — accepted as worth acting on, not yet acted on.
|
|
129
|
+
* - `actioned` — a fix or a decision landed.
|
|
130
|
+
* - `declined` — deliberately not acted on. Terminal, and NOT a failure.
|
|
131
|
+
* - `superseded` — another record covers it.
|
|
132
|
+
* - `parked` — waiting on a named condition; REQUIRES `parked_until`,
|
|
133
|
+
* because a park with no wake condition is a deferral
|
|
134
|
+
* wearing a condition's clothes.
|
|
135
|
+
*/
|
|
136
|
+
export type DefectStatus =
|
|
137
|
+
| 'open'
|
|
138
|
+
| 'candidate'
|
|
139
|
+
| 'actioned'
|
|
140
|
+
| 'declined'
|
|
141
|
+
| 'superseded'
|
|
142
|
+
| 'parked';
|
|
143
|
+
|
|
144
|
+
export const DEFECT_STATUSES: readonly DefectStatus[] = [
|
|
145
|
+
'open',
|
|
146
|
+
'candidate',
|
|
147
|
+
'actioned',
|
|
148
|
+
'declined',
|
|
149
|
+
'superseded',
|
|
150
|
+
'parked',
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
/** Statuses that keep a record in the queue line. */
|
|
154
|
+
export const ACTIVE_STATUSES: readonly DefectStatus[] = ['open', 'candidate'];
|
|
155
|
+
|
|
66
156
|
export interface DefectRecord extends DefectFinding {
|
|
67
157
|
fingerprint: string;
|
|
68
158
|
first_seen: string;
|
|
69
159
|
last_seen: string;
|
|
70
160
|
occurrences: number;
|
|
71
|
-
status:
|
|
161
|
+
status: DefectStatus;
|
|
162
|
+
/** MANDATORY when `status === 'parked'`: when the record comes back. */
|
|
163
|
+
parked_until?: string;
|
|
164
|
+
/** One line saying why the record reached a terminal status. */
|
|
165
|
+
resolution?: string;
|
|
72
166
|
/**
|
|
73
167
|
* Failed egress attempts from the most recent `release` call, sanitized at
|
|
74
168
|
* write time. Local-only bookkeeping: `renderReport` never includes them,
|
|
@@ -617,6 +711,38 @@ export function creationCapReached(
|
|
|
617
711
|
return recentCreations(existing, finding.source, now) >= NEW_RECORDS_PER_SOURCE_PER_WINDOW;
|
|
618
712
|
}
|
|
619
713
|
|
|
714
|
+
/**
|
|
715
|
+
* Shape validation for a stored record. Returns the problems, `[]` when valid.
|
|
716
|
+
*
|
|
717
|
+
* The one obligation that is NOT a style nit: a `parked` record without a
|
|
718
|
+
* `parked_until` is a deferral nothing can wake. Everything else here is the
|
|
719
|
+
* closed-vocabulary check the free-text era could not perform.
|
|
720
|
+
*/
|
|
721
|
+
export function validateRecord(record: DefectRecord): string[] {
|
|
722
|
+
const out: string[] = [];
|
|
723
|
+
if (!(DEFECT_STATUSES as readonly string[]).includes(record.status)) {
|
|
724
|
+
out.push(`status '${String(record.status)}' is outside the closed enum`);
|
|
725
|
+
}
|
|
726
|
+
if (record.status === 'parked' && (record.parked_until ?? '').trim() === '') {
|
|
727
|
+
out.push('status is `parked` but `parked_until` is missing — a park with no wake condition is a deferral, not a park');
|
|
728
|
+
}
|
|
729
|
+
for (const value of record.target ?? []) {
|
|
730
|
+
if (!isWellFormedTarget(value)) {
|
|
731
|
+
out.push(`target '${value}' is not \`<rule|skill|command|hook>:<id>\``);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return out;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** Union of two id lists, order-stable and de-duplicated. */
|
|
738
|
+
function _mergeIds(a: readonly string[] | undefined, b: readonly string[] | undefined): string[] {
|
|
739
|
+
const out: string[] = [];
|
|
740
|
+
for (const v of [...(a ?? []), ...(b ?? [])]) {
|
|
741
|
+
if (!out.includes(v)) out.push(v);
|
|
742
|
+
}
|
|
743
|
+
return out;
|
|
744
|
+
}
|
|
745
|
+
|
|
620
746
|
export function mergeRecord(
|
|
621
747
|
existing: DefectRecord | null,
|
|
622
748
|
finding: DefectFinding,
|
|
@@ -624,13 +750,22 @@ export function mergeRecord(
|
|
|
624
750
|
): DefectRecord {
|
|
625
751
|
const fp = fingerprint(finding.defect_class, finding.evidence);
|
|
626
752
|
if (existing !== null && existing.fingerprint === fp) {
|
|
627
|
-
|
|
753
|
+
const target = _mergeIds(existing.target, finding.target);
|
|
754
|
+
const proposes = _mergeIds(existing.proposes, finding.proposes);
|
|
755
|
+
const merged: DefectRecord = {
|
|
628
756
|
...existing,
|
|
629
757
|
last_seen: now,
|
|
630
758
|
occurrences: existing.occurrences + 1,
|
|
631
|
-
// A record
|
|
759
|
+
// A record re-reported after it left the queue is open again. The
|
|
760
|
+
// widened enum does not change this: a recurrence refutes whatever
|
|
761
|
+
// disposition was taken, which is exactly `recurring-criticism`.
|
|
632
762
|
status: 'open',
|
|
763
|
+
...(target.length > 0 ? { target } : {}),
|
|
764
|
+
...(proposes.length > 0 ? { proposes } : {}),
|
|
633
765
|
};
|
|
766
|
+
// `open` carries no wake condition; a stale one would read as a park.
|
|
767
|
+
delete merged.parked_until;
|
|
768
|
+
return merged;
|
|
634
769
|
}
|
|
635
770
|
return {
|
|
636
771
|
...finding,
|
|
@@ -12,12 +12,16 @@ import * as fs from 'node:fs';
|
|
|
12
12
|
import * as path from 'node:path';
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
|
+
ACTIVE_STATUSES,
|
|
15
16
|
creationCapReached,
|
|
16
17
|
type DefectFinding,
|
|
17
18
|
type DefectRecord,
|
|
18
19
|
type DefectSource,
|
|
19
20
|
fingerprint,
|
|
21
|
+
isWellFormedTarget,
|
|
20
22
|
mergeRecord,
|
|
23
|
+
parseTarget,
|
|
24
|
+
validateRecord,
|
|
21
25
|
} from './self_repair.js';
|
|
22
26
|
|
|
23
27
|
export const STORE_REL = path.join('agents', 'runtime', 'self-repair');
|
|
@@ -48,7 +52,16 @@ export function readRecord(root: string, fp: string): DefectRecord | null {
|
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Write one record. VALIDATES first — a `parked` record with no wake condition
|
|
57
|
+
* or a malformed target never reaches the store, because a store that accepts
|
|
58
|
+
* an unwakeable park has the same defect the park was supposed to fix.
|
|
59
|
+
*/
|
|
51
60
|
export function writeRecord(root: string, record: DefectRecord): void {
|
|
61
|
+
const problems = validateRecord(record);
|
|
62
|
+
if (problems.length > 0) {
|
|
63
|
+
throw new Error(`refusing to write an invalid record: ${problems.join('; ')}`);
|
|
64
|
+
}
|
|
52
65
|
const dir = storeDir(root);
|
|
53
66
|
fs.mkdirSync(dir, { recursive: true });
|
|
54
67
|
fs.writeFileSync(
|
|
@@ -71,7 +84,7 @@ export function listRecords(root: string): DefectRecord[] {
|
|
|
71
84
|
// The overflow counter shares the directory and the extension but is not
|
|
72
85
|
// a record. `isRecord` would reject it anyway; naming it here keeps the
|
|
73
86
|
// exclusion intentional rather than incidental.
|
|
74
|
-
if (!n.endsWith('.json') || n === OVERFLOW_FILE) {
|
|
87
|
+
if (!n.endsWith('.json') || n === OVERFLOW_FILE || n === TARGET_INDEX_FILE) {
|
|
75
88
|
continue;
|
|
76
89
|
}
|
|
77
90
|
const rec = readRecord(root, n.slice(0, -'.json'.length));
|
|
@@ -83,8 +96,16 @@ export function listRecords(root: string): DefectRecord[] {
|
|
|
83
96
|
return out;
|
|
84
97
|
}
|
|
85
98
|
|
|
99
|
+
/**
|
|
100
|
+
* The records still in the queue. Since the status enum widened past
|
|
101
|
+
* `open | released`, "still queued" is `open` OR `candidate` — a record someone
|
|
102
|
+
* accepted but has not yet acted on has not left the queue, and a `declined`,
|
|
103
|
+
* `superseded`, `actioned` or `parked` one has.
|
|
104
|
+
*/
|
|
86
105
|
export function openRecords(root: string): DefectRecord[] {
|
|
87
|
-
return listRecords(root).filter((r) =>
|
|
106
|
+
return listRecords(root).filter((r) =>
|
|
107
|
+
(ACTIVE_STATUSES as readonly string[]).includes(r.status),
|
|
108
|
+
);
|
|
88
109
|
}
|
|
89
110
|
|
|
90
111
|
/**
|
|
@@ -134,29 +155,60 @@ export function upsertFinding(
|
|
|
134
155
|
root: string,
|
|
135
156
|
finding: DefectFinding,
|
|
136
157
|
now: string,
|
|
158
|
+
// The tree the targets resolve against. Defaults to the store root, which
|
|
159
|
+
// is the production case (both are the repo). A test sandboxes the STORE in
|
|
160
|
+
// a tmp dir while still resolving `rule:scope-control` against the real
|
|
161
|
+
// tree, and folding the two roots together would make that untestable.
|
|
162
|
+
opts?: { treeRoot?: string },
|
|
137
163
|
): DefectRecord | null {
|
|
138
164
|
const fp = fingerprint(finding.defect_class, finding.evidence);
|
|
165
|
+
// Targets are resolved against the tree HERE, at the write boundary: the
|
|
166
|
+
// pure half cannot read the filesystem, and validating anywhere later would
|
|
167
|
+
// mean a dangling id had already been stored.
|
|
168
|
+
const split = partitionTargets(opts?.treeRoot ?? root, [
|
|
169
|
+
...(finding.target ?? []),
|
|
170
|
+
...(finding.proposes ?? []),
|
|
171
|
+
]);
|
|
172
|
+
const resolved: DefectFinding = {
|
|
173
|
+
...finding,
|
|
174
|
+
...(split.target.length > 0 ? { target: split.target } : {}),
|
|
175
|
+
...(split.proposes.length > 0 ? { proposes: split.proposes } : {}),
|
|
176
|
+
};
|
|
177
|
+
if (split.target.length === 0) delete resolved.target;
|
|
178
|
+
if (split.proposes.length === 0) delete resolved.proposes;
|
|
139
179
|
const existing = readRecord(root, fp);
|
|
140
180
|
// The fold path never consults the cap: a fingerprint already in the store
|
|
141
181
|
// adds no file, so capping it would silence a tracked defect for no bound.
|
|
142
|
-
if (existing === null && creationCapReached(listRecords(root),
|
|
143
|
-
bumpOverflow(root,
|
|
182
|
+
if (existing === null && creationCapReached(listRecords(root), resolved, now)) {
|
|
183
|
+
bumpOverflow(root, resolved.source, now);
|
|
144
184
|
return null;
|
|
145
185
|
}
|
|
146
|
-
const merged = mergeRecord(existing,
|
|
186
|
+
const merged = mergeRecord(existing, resolved, now);
|
|
147
187
|
writeRecord(root, merged);
|
|
148
188
|
return merged;
|
|
149
189
|
}
|
|
150
190
|
|
|
151
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* Mark a record released — a PR or issue now carries it.
|
|
193
|
+
*
|
|
194
|
+
* The stored status is `actioned`, not the retired `released`: releasing IS the
|
|
195
|
+
* action, and the widened enum has no separate slot for it. The function name
|
|
196
|
+
* is kept because the CALLER's event is still "a release happened".
|
|
197
|
+
*/
|
|
152
198
|
export function markReleased(root: string, fp: string, now: string): DefectRecord | null {
|
|
153
199
|
const rec = readRecord(root, fp);
|
|
154
200
|
if (rec === null) {
|
|
155
201
|
return null;
|
|
156
202
|
}
|
|
157
203
|
// A successful release supersedes any earlier failed attempts.
|
|
158
|
-
const next: DefectRecord = {
|
|
204
|
+
const next: DefectRecord = {
|
|
205
|
+
...rec,
|
|
206
|
+
status: 'actioned',
|
|
207
|
+
resolution: rec.resolution ?? 'released — a pull request or issue now carries this record',
|
|
208
|
+
last_seen: now,
|
|
209
|
+
};
|
|
159
210
|
delete next.release_errors;
|
|
211
|
+
delete next.parked_until;
|
|
160
212
|
writeRecord(root, next);
|
|
161
213
|
return next;
|
|
162
214
|
}
|
|
@@ -186,3 +238,217 @@ export function attachReleaseErrors(
|
|
|
186
238
|
writeRecord(root, next);
|
|
187
239
|
return next;
|
|
188
240
|
}
|
|
241
|
+
|
|
242
|
+
// target resolution
|
|
243
|
+
//
|
|
244
|
+
// `suggested_surface` is free text, so nothing could join a record to the asset
|
|
245
|
+
// it is about. A `target` entry is `<kind>:<id>` from a closed vocabulary AND it
|
|
246
|
+
// is resolved against the tree at WRITE time — an unresolvable entry is not
|
|
247
|
+
// rejected (a record proposing an asset that should exist is real information)
|
|
248
|
+
// and is not silently accepted either (a dangling id looks joined and joins to
|
|
249
|
+
// nothing). It is moved to `proposes`, where a reader can see it for what it is.
|
|
250
|
+
|
|
251
|
+
/** Where each target kind lives in the source tree. */
|
|
252
|
+
function _targetExists(root: string, kind: string, id: string): boolean {
|
|
253
|
+
if (kind === 'rule') {
|
|
254
|
+
return fs.existsSync(path.join(root, 'src', 'rules', `${id}.md`));
|
|
255
|
+
}
|
|
256
|
+
if (kind === 'skill') {
|
|
257
|
+
return fs.existsSync(path.join(root, 'src', 'skills', id, 'SKILL.md'));
|
|
258
|
+
}
|
|
259
|
+
if (kind === 'command') {
|
|
260
|
+
return _commandIds(root).has(id);
|
|
261
|
+
}
|
|
262
|
+
if (kind === 'hook') {
|
|
263
|
+
return _concernIds(root).has(id);
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Command ids, read from the pack tree: every `command.md` under
|
|
270
|
+
* `src/domains/<pack>/…` contributes the name of its containing directory, plus
|
|
271
|
+
* the `parent-child` form a clustered command is referred to by.
|
|
272
|
+
*/
|
|
273
|
+
function _commandIds(root: string): ReadonlySet<string> {
|
|
274
|
+
const out = new Set<string>();
|
|
275
|
+
const base = path.join(root, 'src', 'domains');
|
|
276
|
+
const walk = (dir: string, trail: string[]): void => {
|
|
277
|
+
let entries: fs.Dirent[];
|
|
278
|
+
try {
|
|
279
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
280
|
+
} catch {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
for (const ent of entries) {
|
|
284
|
+
if (ent.isDirectory()) {
|
|
285
|
+
walk(path.join(dir, ent.name), [...trail, ent.name]);
|
|
286
|
+
} else if (ent.name === 'command.md' && trail.length > 0) {
|
|
287
|
+
out.add(trail[trail.length - 1] as string);
|
|
288
|
+
if (trail.length > 1) {
|
|
289
|
+
out.add(trail.slice(-2).join('-'));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
walk(base, []);
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Hook-concern ids, read from the compiled manifest rather than re-parsing the
|
|
300
|
+
* YAML: `hook_manifest.json` is the artifact the dispatcher itself reads, so a
|
|
301
|
+
* concern that resolves here is a concern that actually exists at runtime.
|
|
302
|
+
*/
|
|
303
|
+
function _concernIds(root: string): ReadonlySet<string> {
|
|
304
|
+
try {
|
|
305
|
+
const raw = fs.readFileSync(
|
|
306
|
+
path.join(root, 'src', 'scripts', 'hook_manifest.json'),
|
|
307
|
+
'utf-8',
|
|
308
|
+
);
|
|
309
|
+
const parsed = JSON.parse(raw) as { manifest?: { concerns?: Record<string, unknown> } };
|
|
310
|
+
return new Set(Object.keys(parsed.manifest?.concerns ?? {}));
|
|
311
|
+
} catch {
|
|
312
|
+
return new Set<string>();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Split a caller's target list into the entries that resolve in the tree and
|
|
318
|
+
* the entries that do not. Malformed entries go to `proposes` too — a string
|
|
319
|
+
* that is not `<kind>:<id>` cannot be resolved, and dropping it would lose the
|
|
320
|
+
* observation.
|
|
321
|
+
*/
|
|
322
|
+
export function partitionTargets(
|
|
323
|
+
root: string,
|
|
324
|
+
values: readonly string[],
|
|
325
|
+
): { target: string[]; proposes: string[] } {
|
|
326
|
+
const target: string[] = [];
|
|
327
|
+
const proposes: string[] = [];
|
|
328
|
+
for (const raw of values) {
|
|
329
|
+
const value = raw.trim();
|
|
330
|
+
if (value === '') continue;
|
|
331
|
+
const parsed = isWellFormedTarget(value) ? parseTarget(value) : null;
|
|
332
|
+
if (parsed !== null && _targetExists(root, parsed.kind, parsed.id)) {
|
|
333
|
+
if (!target.includes(value)) target.push(value);
|
|
334
|
+
} else if (!proposes.includes(value)) {
|
|
335
|
+
proposes.push(value);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return { target, proposes };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// per-target occurrence index
|
|
342
|
+
//
|
|
343
|
+
// `occurrences` counts a FINGERPRINT, which is class plus normalised evidence
|
|
344
|
+
// shape — so two different manifestations of one rule's weakness are two
|
|
345
|
+
// records with one occurrence each, and the third-recurrence escalation stated
|
|
346
|
+
// in `skill-improvement-pipeline` had nothing that could ever establish a third
|
|
347
|
+
// recurrence. This is a DERIVED read over the record files: regenerable,
|
|
348
|
+
// byte-stable, and gitignored with the rest of `agents/runtime/`.
|
|
349
|
+
|
|
350
|
+
// cache-invalidation: regenerated WHOLESALE from the record files on every run
|
|
351
|
+
// (`writeTargetIndex` recomputes from scratch — no incremental or partial
|
|
352
|
+
// state), and the output is deterministic by construction, so a stale read is
|
|
353
|
+
// impossible and a version namespace would name a format nothing reads across
|
|
354
|
+
// runs. The shape change that would matter is a shape change to the records
|
|
355
|
+
// themselves, which `validateRecord` already gates.
|
|
356
|
+
export const TARGET_INDEX_FILE = '_target-index.json';
|
|
357
|
+
|
|
358
|
+
export interface TargetCount {
|
|
359
|
+
target: string;
|
|
360
|
+
/** Sum of `occurrences` across every record naming this target. */
|
|
361
|
+
occurrences: number;
|
|
362
|
+
/** Fingerprints naming it — a per-target count of DISTINCT manifestations. */
|
|
363
|
+
records: string[];
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function targetIndexPath(root: string): string {
|
|
367
|
+
return path.join(storeDir(root), TARGET_INDEX_FILE);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Aggregate occurrences by target. Pure over the store's record files. */
|
|
371
|
+
export function targetCounts(root: string): TargetCount[] {
|
|
372
|
+
const acc = new Map<string, { occurrences: number; records: string[] }>();
|
|
373
|
+
for (const rec of listRecords(root)) {
|
|
374
|
+
for (const value of rec.target ?? []) {
|
|
375
|
+
const prev = acc.get(value) ?? { occurrences: 0, records: [] };
|
|
376
|
+
prev.occurrences += Number.isFinite(rec.occurrences) ? rec.occurrences : 0;
|
|
377
|
+
if (!prev.records.includes(rec.fingerprint)) prev.records.push(rec.fingerprint);
|
|
378
|
+
acc.set(value, prev);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return [...acc.entries()]
|
|
382
|
+
.map(([target, v]) => ({
|
|
383
|
+
target,
|
|
384
|
+
occurrences: v.occurrences,
|
|
385
|
+
records: [...v.records].sort(),
|
|
386
|
+
}))
|
|
387
|
+
.sort((a, b) => (a.target < b.target ? -1 : a.target > b.target ? 1 : 0));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Write the derived index. Deterministic by construction — sorted keys, sorted
|
|
392
|
+
* fingerprints, no timestamp — so deleting it and rebuilding produces the same
|
|
393
|
+
* bytes. A timestamp here would make the artifact look fresh while saying
|
|
394
|
+
* nothing, and would destroy the one property that makes a rebuild routine.
|
|
395
|
+
*/
|
|
396
|
+
export function writeTargetIndex(root: string): TargetCount[] {
|
|
397
|
+
const counts = targetCounts(root);
|
|
398
|
+
fs.mkdirSync(storeDir(root), { recursive: true });
|
|
399
|
+
fs.writeFileSync(targetIndexPath(root), `${JSON.stringify(counts, null, 2)}\n`, 'utf-8');
|
|
400
|
+
return counts;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// status transitions
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Migrate `released` records — the retired status — to `actioned`, carrying a
|
|
407
|
+
* resolution line that says the migration is why. Idempotent: a record already
|
|
408
|
+
* on the new enum is left byte-identical, so running it twice over a store
|
|
409
|
+
* produces no second diff.
|
|
410
|
+
*
|
|
411
|
+
* Returns the fingerprints it changed.
|
|
412
|
+
*/
|
|
413
|
+
export function migrateReleasedRecords(root: string): string[] {
|
|
414
|
+
const changed: string[] = [];
|
|
415
|
+
for (const rec of listRecords(root)) {
|
|
416
|
+
if ((rec.status as string) !== 'released') continue;
|
|
417
|
+
const next: DefectRecord = {
|
|
418
|
+
...rec,
|
|
419
|
+
status: 'actioned',
|
|
420
|
+
resolution:
|
|
421
|
+
rec.resolution ??
|
|
422
|
+
'migrated from the retired `released` status; the release itself is the action recorded',
|
|
423
|
+
};
|
|
424
|
+
writeRecord(root, next);
|
|
425
|
+
changed.push(rec.fingerprint);
|
|
426
|
+
}
|
|
427
|
+
return changed.sort();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Move a record to a terminal or parked status, validating the result. */
|
|
431
|
+
export function setStatus(
|
|
432
|
+
root: string,
|
|
433
|
+
fp: string,
|
|
434
|
+
next: DefectRecord['status'],
|
|
435
|
+
now: string,
|
|
436
|
+
extra?: { parked_until?: string; resolution?: string },
|
|
437
|
+
): DefectRecord | null {
|
|
438
|
+
const rec = readRecord(root, fp);
|
|
439
|
+
if (rec === null) return null;
|
|
440
|
+
const updated: DefectRecord = {
|
|
441
|
+
...rec,
|
|
442
|
+
status: next,
|
|
443
|
+
last_seen: now,
|
|
444
|
+
...(extra?.parked_until !== undefined ? { parked_until: extra.parked_until } : {}),
|
|
445
|
+
...(extra?.resolution !== undefined ? { resolution: extra.resolution } : {}),
|
|
446
|
+
};
|
|
447
|
+
if (next !== 'parked') delete updated.parked_until;
|
|
448
|
+
const problems = validateRecord(updated);
|
|
449
|
+
if (problems.length > 0) {
|
|
450
|
+
throw new Error(`refusing to write an invalid record: ${problems.join('; ')}`);
|
|
451
|
+
}
|
|
452
|
+
writeRecord(root, updated);
|
|
453
|
+
return updated;
|
|
454
|
+
}
|
|
@@ -175,14 +175,19 @@ export function stop_means_session_end(platform: string | null | undefined): boo
|
|
|
175
175
|
* Deliberately **not** derived from the frequency lattice. That instrument
|
|
176
176
|
* models *slot presence*, and two hosts have a slot that never fires:
|
|
177
177
|
*
|
|
178
|
-
* - `cursor` —
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
178
|
+
* - `cursor` — excluded because reachability there is UNESTABLISHED, not
|
|
179
|
+
* because it is known absent. The earlier wording asserted the CLI fires only
|
|
180
|
+
* shell-execution hooks, sourced to a 2026-01 reading that carried no expiry
|
|
181
|
+
* and could not be re-verified in either direction. The safe direction is the
|
|
182
|
+
* one taken: an unverified slot must not read as covered. The lattice has no
|
|
183
|
+
* IDE/CLI dimension of its own, so it would report cursor covered; the
|
|
184
|
+
* envelope now carries a `surface` field (`_lib/surface.ts`) that records
|
|
185
|
+
* which surface a dispatch came from, and it answers `unknown` on cursor
|
|
186
|
+
* because no marker distinguishes the two.
|
|
182
187
|
* - `cowork` — slots are structurally wired but lifecycle events do not fire
|
|
183
188
|
* (`hook_manifest.yaml:346-351`).
|
|
184
|
-
* - `copilot` —
|
|
185
|
-
* from the join by declaration.
|
|
189
|
+
* - `copilot` — nothing bound by this package (`fallback_only: true` in the
|
|
190
|
+
* manifest), excluded from the join by declaration.
|
|
186
191
|
*
|
|
187
192
|
* A session on an excluded host still *registers* if a `session_start` reaches
|
|
188
193
|
* it, and simply stops being visible after its TTL. It is never reported as
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema helpers for the `node:sqlite` stores in this tree.
|
|
3
|
+
*/
|
|
4
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Add a column to an existing table when it is absent.
|
|
8
|
+
*
|
|
9
|
+
* `CREATE TABLE IF NOT EXISTS` is a no-op on a database that predates a
|
|
10
|
+
* column, so a store created by an earlier build needs the explicit add or the
|
|
11
|
+
* field is silently missing from every read.
|
|
12
|
+
*
|
|
13
|
+
* `PRAGMA table_info` rather than a catch around `ALTER TABLE`: swallowing the
|
|
14
|
+
* error would also swallow a genuine schema failure, and a store that silently
|
|
15
|
+
* fails to migrate reads exactly like one that never needed to.
|
|
16
|
+
*/
|
|
17
|
+
export function addColumnIfMissing(db: DatabaseSync, table: string, column: string, decl: string): void {
|
|
18
|
+
const cols = db.prepare(`PRAGMA table_info(${table})`).all() as { name: string }[];
|
|
19
|
+
if (cols.length === 0) return;
|
|
20
|
+
if (cols.some((c) => c.name === column)) return;
|
|
21
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${decl}`);
|
|
22
|
+
}
|