@event4u/agent-config 14.21.0 → 14.23.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 +164 -187
- package/CREDITS.md +37 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +380 -5
- package/dist/agent-src/commands/analyze/repo.md +4 -2
- package/dist/agent-src/commands/pr/merge.md +47 -38
- package/dist/agent-src/commands/roadmap/next.md +8 -0
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +12 -0
- package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
- package/dist/agent-src/rules/notes-first-reasoning.md +36 -25
- package/dist/agent-src/rules/token-efficiency.md +26 -3
- package/dist/agent-src/scripts/_cli_entry.ts +51 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
- package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
- package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/dist/agent-src/scripts/stubs_due.ts +4 -18
- package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/dist/agent-src/skills/code-intelligence/SKILL.md +82 -1
- package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +9 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +52 -2
- package/dist/agent-src/templates/agent-settings.md +0 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -1
- package/dist/cli-delegate/_cli_entry.js +8 -0
- package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
- package/dist/cli-delegate/archival_sweep.js +8 -0
- package/dist/cli-delegate/archive_completed_roadmaps.js +542 -0
- package/dist/cli-delegate/check_update_banner.js +923 -0
- package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-2KKXJW25.js} +23 -9
- package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
- package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
- package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
- package/dist/cli-delegate/{chunk-4FS4WRXP.js → chunk-IAI3XL43.js} +15 -3
- package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
- package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-KCNTUSPI.js} +7 -1
- package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
- package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
- package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
- package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
- package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
- package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
- package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
- package/dist/cli-delegate/cmd_conformance.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +45 -13
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_session_recycle.js +4 -14
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- 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/cli-delegate/dashboard_mode.js +16 -0
- package/dist/cli-delegate/gate_budget.js +24 -0
- package/dist/cli-delegate/gate_execute.js +25 -0
- package/dist/cli-delegate/guarded_baseline.js +18 -0
- package/dist/cli-delegate/resume_probe.js +22 -0
- package/dist/cli-delegate/roadmap_gates.js +638 -0
- package/dist/cli-delegate/roadmap_set_step.js +173 -0
- package/dist/cli-delegate/stub_queue.js +47 -0
- package/dist/cli-delegate/stubs_due.js +38 -0
- package/dist/cli-delegate/ticket_bundles.js +59 -0
- package/dist/cli-delegate/update_roadmap_progress.js +60 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +13 -13
- 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 +1813 -946
- package/dist/install/claudeLayerCarriage.js +237 -0
- package/dist/install/claudePathsPlan.js +79 -0
- package/dist/install/hostLayerFingerprint.js +1 -1
- package/dist/install/install.mjs +21 -3
- package/dist/install/partitionEligibility.js +231 -117
- package/dist/install/ruleLayerPartition.js +10 -7
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +10870 -7641
- package/dist/scripts/_lib/lean_projection_mode.js +108 -0
- package/dist/scripts/_lib/router_match.js +243 -0
- package/dist/scripts/_lib/rule_injection.js +216 -0
- package/dist/scripts/condense.js +52 -45
- package/dist/scripts/project_thin_rules.js +139 -19
- package/dist/server/schemas/settings.js +16 -3
- package/docs/MIGRATION.md +105 -1
- package/docs/contracts/settings-classes.md +40 -8
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
- package/docs/guidelines/agent-infra/notes-horizon-mechanics.md +90 -0
- package/llms.txt +1 -1
- package/package.json +7 -4
- package/src/agent-src/scripts/_cli_entry.ts +51 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +120 -27
- package/src/agent-src/scripts/roadmap_gates.ts +4 -20
- package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/src/agent-src/scripts/stubs_due.ts +4 -18
- package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/src/config/agent-settings.template.yml +47 -1
- package/src/config/continuity-surface.json +516 -0
- package/src/config/estate-count-budget.json +1 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +29 -8
- package/src/config/gate-violation-baselines.json +8 -8
- package/src/config/gitignore-block.txt +19 -0
- package/src/config/hook-token-budget.json +7 -6
- package/src/config/host-injection-effect.json +73 -0
- package/src/config/packed-binary-manifest.json +33 -0
- package/src/config/preamble-payload-budget.json +112 -105
- package/src/config/publish-surface.json +4 -2
- package/src/config/routing-coverage-seed.json +9 -2
- package/src/install/claudeLayerCarriage.ts +273 -0
- package/src/install/claudePathsPlan.ts +76 -0
- package/src/install/hostLayerFingerprint.ts +1 -1
- package/src/install/partitionEligibility.ts +261 -127
- package/src/install/ruleLayerPartition.ts +19 -23
- package/src/scripts/_cli/cmd_session_recycle.ts +2 -25
- package/src/scripts/_cli/handoff_sessions.ts +14 -44
- package/src/scripts/_dispatch.bash +77 -27
- package/src/scripts/_lib/auto_dispatch.ts +2 -2
- package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
- package/src/scripts/_lib/continuity_slot.ts +295 -0
- package/src/scripts/_lib/continuity_surface.ts +296 -0
- package/src/scripts/_lib/continuity_writer.ts +253 -0
- package/src/scripts/_lib/corpus_manifest.ts +26 -9
- package/src/scripts/_lib/hook_settings.ts +86 -1
- package/src/scripts/_lib/injection_effect.ts +151 -0
- package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
- package/src/scripts/_lib/lean_projection_mode.ts +127 -0
- package/src/scripts/_lib/packed_binary_predicate.ts +262 -0
- package/src/scripts/_lib/pin_resolver.ts +13 -0
- package/src/scripts/_lib/recycle_envelope_paths.ts +109 -21
- package/src/scripts/_lib/regression_neighbourhood.ts +159 -22
- package/src/scripts/_lib/retired_status.ts +74 -0
- package/src/scripts/_lib/session_eol.ts +44 -0
- package/src/scripts/_lib/session_index_trust.ts +346 -0
- package/src/scripts/_lib/skill_catalogue.ts +156 -21
- package/src/scripts/_lib/standing_bound_ratchet.ts +154 -0
- package/src/scripts/_lib/subagent_capsule.ts +117 -8
- package/src/scripts/_lib/vendored_grammar_upstream.ts +280 -0
- package/src/scripts/ai_council/api_transport.ts +5 -0
- package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
- package/src/scripts/ai_council/prompts.ts +9 -0
- package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
- package/src/scripts/audit_mcp_tools.ts +69 -25
- package/src/scripts/build_mcp_catalog.ts +24 -1
- package/src/scripts/capture_skill_catalogue.ts +24 -11
- package/src/scripts/check_candidate_lines.ts +499 -0
- package/src/scripts/check_continuity_surface.ts +475 -0
- package/src/scripts/check_dependency_floors.ts +13 -6
- package/src/scripts/check_estate_count.ts +40 -79
- package/src/scripts/check_host_tree_parity.ts +291 -0
- package/src/scripts/check_pack_size.ts +262 -23
- package/src/scripts/check_preamble_payload_budget.ts +136 -7
- package/src/scripts/check_roadmap_trackable.ts +19 -16
- package/src/scripts/check_rule_layer_partition.ts +54 -34
- package/src/scripts/check_rule_projection_integrity.ts +102 -2
- package/src/scripts/check_single_delivery.ts +97 -31
- package/src/scripts/check_update_banner.ts +13 -0
- package/src/scripts/ci_settle.ts +205 -5
- package/src/scripts/code_graph/build.ts +217 -15
- package/src/scripts/code_graph/cli.ts +334 -15
- package/src/scripts/code_graph/detect.ts +38 -0
- package/src/scripts/code_graph/extract.ts +11 -0
- package/src/scripts/code_graph/grammar_probe.ts +43 -0
- package/src/scripts/code_graph/loader.ts +88 -14
- package/src/scripts/code_graph/query.ts +144 -11
- package/src/scripts/code_graph/resolution_tiers.ts +238 -0
- package/src/scripts/code_graph/sqlite_store.ts +262 -6
- package/src/scripts/code_graph/types.ts +117 -3
- package/src/scripts/code_graph/validate.ts +26 -1
- package/src/scripts/code_graph/verbs.ts +590 -0
- package/src/scripts/condense.ts +54 -42
- package/src/scripts/corpus_manifest.ts +5 -5
- package/src/scripts/council_cli.ts +11 -12
- package/src/scripts/count_intent_disagreement.ts +426 -0
- package/src/scripts/handoff_context_hook.ts +118 -1
- package/src/scripts/hook_effect_doctor.ts +82 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +87 -12
- package/src/scripts/hooks/block_speaking_inbox_dir.ts +182 -16
- package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
- package/src/scripts/hooks/code_graph_context_hook.ts +201 -0
- package/src/scripts/hooks/concern_registry.ts +4 -2
- package/src/scripts/hooks/host_semantics.ts +1 -1
- package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
- package/src/scripts/hooks/push_settle_hook.ts +3 -2
- package/src/scripts/hooks/rule_inject_hook.ts +108 -21
- package/src/scripts/hooks/session_eol_hook.ts +120 -2
- package/src/scripts/hooks/skill_route_hook.ts +11 -4
- package/src/scripts/hot_context_hook.ts +62 -7
- package/src/scripts/inbox_source_census.ts +322 -0
- package/src/scripts/install-hooks.sh +82 -0
- package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
- package/src/scripts/lint_plan_risk_register.ts +13 -17
- package/src/scripts/lint_roadmap_blockers.ts +164 -1
- package/src/scripts/mcp_server/consumer_tool_catalog.json +152 -2
- package/src/scripts/mcp_server/graph_tools.ts +294 -0
- package/src/scripts/mcp_server/path_util.ts +31 -0
- package/src/scripts/mcp_server/suggest_skill.ts +72 -0
- package/src/scripts/mcp_server/tool_catalog_source.ts +7 -1
- package/src/scripts/mcp_server/tools.ts +5 -69
- package/src/scripts/media/lib/adapter-contract.md +7 -2
- package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
- package/src/scripts/model_rule_injection.ts +61 -4
- package/src/scripts/project_thin_rules.ts +149 -16
- package/src/scripts/rdp_candidates_delta.ts +276 -0
- package/src/scripts/rdp_quality_eval.ts +217 -19
- package/src/scripts/release_mix_taxonomy.json +3 -2
- package/src/scripts/report_host_injection_effect.ts +189 -0
- package/src/scripts/report_layer_overlap.ts +41 -0
- package/src/scripts/report_skill_menu_census.ts +341 -0
- package/src/scripts/report_standing_payload_by_host.ts +650 -0
- package/src/scripts/schemas/agent-settings.schema.json +7 -1
- package/src/scripts/session_memory_index.ts +81 -8
- package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
- package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
- package/src/server/schemas/settings.ts +23 -4
- package/src/vendor/grammars/README.md +86 -0
- package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
- package/src/scripts/_lib/carrier_status.ts +0 -118
- package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
- package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The continuity-record slot — capacity policy and state machine.
|
|
3
|
+
*
|
|
4
|
+
* `road-to-continuity-writer-activation` step 1.1. Until this module existed
|
|
5
|
+
* the slot had a producer (`session:recycle`), a consumer
|
|
6
|
+
* (`handoff_context_hook`) and no documented conflict ownership: the write was
|
|
7
|
+
* an unguarded `atomic_write_json`, so a second publish replaced whatever sat
|
|
8
|
+
* there and nobody had decided whether that was correct. With a MANUAL producer
|
|
9
|
+
* the question was theoretical — a human ran the verb once. Automatic
|
|
10
|
+
* production makes it routine, which is why the policy is settled here BEFORE
|
|
11
|
+
* any automatic writer exists.
|
|
12
|
+
*
|
|
13
|
+
* The full contract, including why the two rejected alternatives are rejected
|
|
14
|
+
* on tree evidence rather than on preference, is
|
|
15
|
+
* `docs/contracts/continuity-record-slot.md`. This module is that document in
|
|
16
|
+
* code; neither is the source of truth for the other's reasoning, and both are
|
|
17
|
+
* checked by `tests/scripts/_lib_continuity_slot.test.ts`.
|
|
18
|
+
*
|
|
19
|
+
* The policy in one line:
|
|
20
|
+
*
|
|
21
|
+
* SUPERSEDE OWN · REFUSE FOREIGN · QUARANTINE UNUSABLE · NEVER GO BACKWARDS.
|
|
22
|
+
*
|
|
23
|
+
* Why not create-if-absent.
|
|
24
|
+
*
|
|
25
|
+
* Create-if-absent is the policy the roadmap's own Risk 2 describes as a silent
|
|
26
|
+
* data loss: "the successor resumes from something plausible and older". It is
|
|
27
|
+
* rejected. Within one session the resident and the incoming record share a
|
|
28
|
+
* key, a workspace and a predecessor — the incoming one is the same lineage at
|
|
29
|
+
* a later moment, so keeping the older is a pure loss with no compensating
|
|
30
|
+
* safety.
|
|
31
|
+
*
|
|
32
|
+
* Why not a bounded multi-record queue.
|
|
33
|
+
*
|
|
34
|
+
* Because the reader forbids the resolution such a queue would need. Picking
|
|
35
|
+
* one of several records requires recency, and `recycle_envelope_paths.ts`
|
|
36
|
+
* carries a recorded decision against exactly that ("No `latest` index, by
|
|
37
|
+
* decision" — a pointer to the newest "would restore the shared write target
|
|
38
|
+
* one layer up"). `resolveContinuityRecord` implements it: several candidates
|
|
39
|
+
* and no session id START CLEAN rather than pick. A queue would either
|
|
40
|
+
* contradict that lock or be unreadable.
|
|
41
|
+
*
|
|
42
|
+
* Where `consuming` lives.
|
|
43
|
+
*
|
|
44
|
+
* Nowhere on disk, and that is the answer rather than a gap. Consumption is a
|
|
45
|
+
* single `fs.renameSync` from the authoritative name to the consumed name
|
|
46
|
+
* (`handoff_context_hook.ts:199`), and publication is a single `fs.renameSync`
|
|
47
|
+
* from a pid-suffixed temp name to the authoritative name
|
|
48
|
+
* (`state_io.ts:_publish_text_locked`). A rename within one directory is
|
|
49
|
+
* atomic on every filesystem this package supports, so an interruption lands
|
|
50
|
+
* strictly before or strictly after it: the authoritative name holds either the
|
|
51
|
+
* previous complete record or the new complete record, never a partial one.
|
|
52
|
+
* `consuming` is therefore a transition, not a state, and no reader has to
|
|
53
|
+
* tolerate a half-written record.
|
|
54
|
+
*/
|
|
55
|
+
import * as fs from 'node:fs';
|
|
56
|
+
import * as path from 'node:path';
|
|
57
|
+
|
|
58
|
+
import {
|
|
59
|
+
recycle_consumed_rel,
|
|
60
|
+
recycle_envelope_rel,
|
|
61
|
+
recycle_quarantine_rel,
|
|
62
|
+
RECYCLE_ENVELOPE_MAX_BYTES,
|
|
63
|
+
RECYCLE_MAX_AGE_HOURS,
|
|
64
|
+
} from './recycle_envelope_paths.js';
|
|
65
|
+
import { validateRecycleEnvelope } from './subagent_capsule.js';
|
|
66
|
+
import { update_json_under_lock } from '../hooks/state_io.js';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The slot's states.
|
|
70
|
+
*
|
|
71
|
+
* Five are observable on disk. `consuming` is declared because the roadmap step
|
|
72
|
+
* names it and a reader looking for it deserves to find the reason it is not a
|
|
73
|
+
* disk state rather than to conclude it was forgotten — see the module header.
|
|
74
|
+
*/
|
|
75
|
+
export const SLOT_STATES = [
|
|
76
|
+
'absent',
|
|
77
|
+
'published',
|
|
78
|
+
'consuming',
|
|
79
|
+
'consumed',
|
|
80
|
+
'quarantined',
|
|
81
|
+
'conflicting',
|
|
82
|
+
] as const;
|
|
83
|
+
export type SlotState = (typeof SLOT_STATES)[number];
|
|
84
|
+
|
|
85
|
+
/** What a publish attempt did, and why. The reason is always present. */
|
|
86
|
+
export interface PublishOutcome {
|
|
87
|
+
/** `published` on a write; otherwise the state that refused it. */
|
|
88
|
+
state: SlotState;
|
|
89
|
+
/** Did the authoritative record change as a result of this call? */
|
|
90
|
+
wrote: boolean;
|
|
91
|
+
/** One line, always present — emitted by callers, never dropped. */
|
|
92
|
+
reason: string;
|
|
93
|
+
/** Absolute path of the record moved aside, when one was. */
|
|
94
|
+
quarantined?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** What {@link inspectSlot} found, without changing anything. */
|
|
98
|
+
export interface SlotInspection {
|
|
99
|
+
state: SlotState;
|
|
100
|
+
reason: string;
|
|
101
|
+
/** The authoritative path this session's records live at. */
|
|
102
|
+
target: string;
|
|
103
|
+
/** Parsed resident record, when one is present AND parseable. */
|
|
104
|
+
resident?: Record<string, unknown>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Hours a record stays usable — the reader's own bound, not a second one. */
|
|
108
|
+
const MAX_AGE_MS = RECYCLE_MAX_AGE_HOURS * 3600 * 1000;
|
|
109
|
+
|
|
110
|
+
function _parse(file: string): Record<string, unknown> | null {
|
|
111
|
+
try {
|
|
112
|
+
const decoded: unknown = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
113
|
+
if (typeof decoded === 'object' && decoded !== null && !Array.isArray(decoded)) {
|
|
114
|
+
return decoded as Record<string, unknown>;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Milliseconds since a record's `written_at`, or `null` when it has none. */
|
|
123
|
+
function _ageMs(record: Record<string, unknown>, now: Date): number | null {
|
|
124
|
+
const raw = record['written_at'];
|
|
125
|
+
if (typeof raw !== 'string') return null;
|
|
126
|
+
const at = Date.parse(raw);
|
|
127
|
+
if (Number.isNaN(at)) return null;
|
|
128
|
+
return now.getTime() - at;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Read the slot for `session_id` without touching it.
|
|
133
|
+
*
|
|
134
|
+
* `conflicting` here is a statement about the RESIDENT relative to the caller's
|
|
135
|
+
* identity, which is why the session id is required rather than optional: a
|
|
136
|
+
* record is foreign only with respect to somebody.
|
|
137
|
+
*/
|
|
138
|
+
export function inspectSlot(
|
|
139
|
+
workspace_root: string,
|
|
140
|
+
session_id: string | null | undefined,
|
|
141
|
+
now: Date = new Date(),
|
|
142
|
+
): SlotInspection {
|
|
143
|
+
const target = path.join(workspace_root, recycle_envelope_rel(session_id));
|
|
144
|
+
if (!fs.existsSync(target)) {
|
|
145
|
+
const consumed = path.join(workspace_root, recycle_consumed_rel(session_id));
|
|
146
|
+
if (fs.existsSync(consumed)) {
|
|
147
|
+
return { state: 'consumed', reason: 'the record for this session was consumed', target };
|
|
148
|
+
}
|
|
149
|
+
const quarantined = path.join(workspace_root, recycle_quarantine_rel(session_id));
|
|
150
|
+
if (fs.existsSync(quarantined)) {
|
|
151
|
+
return {
|
|
152
|
+
state: 'quarantined',
|
|
153
|
+
reason: 'the slot is free; an unusable record sits in quarantine',
|
|
154
|
+
target,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return { state: 'absent', reason: 'no record in this slot', target };
|
|
158
|
+
}
|
|
159
|
+
const resident = _parse(target);
|
|
160
|
+
if (resident === null) {
|
|
161
|
+
return { state: 'quarantined', reason: 'the resident record does not parse as an object', target };
|
|
162
|
+
}
|
|
163
|
+
const violations = validateRecycleEnvelope(resident);
|
|
164
|
+
if (violations.length > 0) {
|
|
165
|
+
return {
|
|
166
|
+
state: 'quarantined',
|
|
167
|
+
reason: `the resident record fails validation (${violations.length} violation(s))`,
|
|
168
|
+
target,
|
|
169
|
+
resident,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const age = _ageMs(resident, now);
|
|
173
|
+
if (age === null || age > MAX_AGE_MS) {
|
|
174
|
+
return {
|
|
175
|
+
state: 'quarantined',
|
|
176
|
+
reason:
|
|
177
|
+
age === null
|
|
178
|
+
? 'the resident record carries no parseable written_at'
|
|
179
|
+
: `the resident record is older than ${RECYCLE_MAX_AGE_HOURS} h`,
|
|
180
|
+
target,
|
|
181
|
+
resident,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const owner = String(resident['session_id'] ?? '').trim();
|
|
185
|
+
const mine = String(session_id ?? '').trim();
|
|
186
|
+
if (owner !== '' && mine !== '' && owner !== mine) {
|
|
187
|
+
return {
|
|
188
|
+
state: 'conflicting',
|
|
189
|
+
reason: `the slot holds an unconsumed record belonging to session ${owner}`,
|
|
190
|
+
target,
|
|
191
|
+
resident,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return { state: 'published', reason: 'this session has an unconsumed record', target, resident };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Publish `record` into this session's slot under the capacity policy.
|
|
199
|
+
*
|
|
200
|
+
* Never throws for a policy reason: a refusal is a return value, because this
|
|
201
|
+
* runs on the Stop path where an exception is a worse outcome than a missed
|
|
202
|
+
* record. A genuine I/O failure is reported as `wrote: false` with the reason,
|
|
203
|
+
* for the same reason.
|
|
204
|
+
*
|
|
205
|
+
* The write itself goes through {@link update_json_under_lock} rather than
|
|
206
|
+
* `atomic_write_json`, and the difference is the whole point: the locked form
|
|
207
|
+
* gives a read-modify-write, so the resident is inspected and the decision
|
|
208
|
+
* taken INSIDE the exclusion that stops a peer from publishing between the
|
|
209
|
+
* inspection and the write. `atomic_write_json` — the path
|
|
210
|
+
* `session:recycle` still uses — has no such window and cannot express a
|
|
211
|
+
* refusal.
|
|
212
|
+
*/
|
|
213
|
+
export function publishContinuityRecord(
|
|
214
|
+
workspace_root: string,
|
|
215
|
+
session_id: string | null | undefined,
|
|
216
|
+
record: Record<string, unknown>,
|
|
217
|
+
now: Date = new Date(),
|
|
218
|
+
): PublishOutcome {
|
|
219
|
+
const violations = validateRecycleEnvelope(record);
|
|
220
|
+
if (violations.length > 0) {
|
|
221
|
+
return {
|
|
222
|
+
state: 'absent',
|
|
223
|
+
wrote: false,
|
|
224
|
+
reason: `refused — the record to publish fails validation: ${violations[0] as string}`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const bytes = Buffer.byteLength(JSON.stringify(record, null, 2), 'utf-8');
|
|
228
|
+
if (bytes > RECYCLE_ENVELOPE_MAX_BYTES) {
|
|
229
|
+
return {
|
|
230
|
+
state: 'absent',
|
|
231
|
+
wrote: false,
|
|
232
|
+
reason: `refused — ${bytes} bytes exceeds the ${RECYCLE_ENVELOPE_MAX_BYTES} byte slot cap`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const target = path.join(workspace_root, recycle_envelope_rel(session_id));
|
|
237
|
+
const before = inspectSlot(workspace_root, session_id, now);
|
|
238
|
+
|
|
239
|
+
// A foreign, still-usable record is not this session's to move or replace.
|
|
240
|
+
// Refusing costs this session's resume; superseding costs another session's,
|
|
241
|
+
// and that one has no way to find out.
|
|
242
|
+
if (before.state === 'conflicting') {
|
|
243
|
+
return { state: 'conflicting', wrote: false, reason: `refused — ${before.reason}` };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Monotonicity. A delayed or retried publisher must not roll the slot back
|
|
247
|
+
// to an earlier moment of the same session.
|
|
248
|
+
if (before.state === 'published' && before.resident !== undefined) {
|
|
249
|
+
const residentAt = Date.parse(String(before.resident['written_at'] ?? ''));
|
|
250
|
+
const incomingAt = Date.parse(String(record['written_at'] ?? ''));
|
|
251
|
+
if (!Number.isNaN(residentAt) && !Number.isNaN(incomingAt) && incomingAt < residentAt) {
|
|
252
|
+
return {
|
|
253
|
+
state: 'published',
|
|
254
|
+
wrote: false,
|
|
255
|
+
reason: 'refused — the resident record is newer than the one offered',
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let quarantined: string | undefined;
|
|
261
|
+
if (before.state === 'quarantined' && fs.existsSync(target)) {
|
|
262
|
+
// Move aside, never delete. See `recycle_quarantine_rel`.
|
|
263
|
+
const dest = path.join(workspace_root, recycle_quarantine_rel(session_id));
|
|
264
|
+
try {
|
|
265
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
266
|
+
fs.renameSync(target, dest);
|
|
267
|
+
quarantined = dest;
|
|
268
|
+
} catch (exc) {
|
|
269
|
+
return {
|
|
270
|
+
state: 'quarantined',
|
|
271
|
+
wrote: false,
|
|
272
|
+
reason: `refused — could not quarantine the unusable resident: ${String(exc)}`,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const result = update_json_under_lock<Record<string, unknown>>(target, () => record);
|
|
278
|
+
if (result !== 'written') {
|
|
279
|
+
return {
|
|
280
|
+
state: before.state,
|
|
281
|
+
wrote: false,
|
|
282
|
+
reason: `refused — the slot write did not complete (${result})`,
|
|
283
|
+
...(quarantined !== undefined ? { quarantined } : {}),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
state: 'published',
|
|
288
|
+
wrote: true,
|
|
289
|
+
reason:
|
|
290
|
+
before.state === 'published'
|
|
291
|
+
? 'superseded this session’s earlier record'
|
|
292
|
+
: `published into a ${before.state} slot`,
|
|
293
|
+
...(quarantined !== undefined ? { quarantined } : {}),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Continuity-surface inventory — the behavioural half of the ratchet.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS
|
|
5
|
+
* ---------------
|
|
6
|
+
* `road-to-one-continuity-record` measured its own success as five numbers —
|
|
7
|
+
* public continuity commands, session resume pickers, persistent continuity
|
|
8
|
+
* artefacts, continuity schemas, normal-path manual actions — and its council
|
|
9
|
+
* of 2026-09-07 named the defect in measuring them by name:
|
|
10
|
+
*
|
|
11
|
+
* "Any persisted state written for later session/run recovery, verification,
|
|
12
|
+
* or context restoration must be listed, whether or not it is labeled
|
|
13
|
+
* continuity."
|
|
14
|
+
*
|
|
15
|
+
* A count keyed on the WORD `continuity` is gameable by renaming. So the
|
|
16
|
+
* artefact axis is DISCOVERED behaviourally — every distinct leaf under
|
|
17
|
+
* `agents/runtime/state/` that any source file names — and the inventory then
|
|
18
|
+
* has to dispose of each one as `counted` or `excluded` WITH A REASON. A leaf
|
|
19
|
+
* nothing disposes of is a hard failure, which is what makes a newly added
|
|
20
|
+
* persisted-state surface impossible to slip past the count silently.
|
|
21
|
+
*
|
|
22
|
+
* The exclusions are the load-bearing half, not the leftovers. `checkpoints/`
|
|
23
|
+
* is the worked example the council named: it is kept (D2 — run-integrity
|
|
24
|
+
* evidence is keyed by run id, consumed by `run:supervise`, and a disagreement
|
|
25
|
+
* there is an integrity alarm rather than a degraded resume), so it is excluded
|
|
26
|
+
* from the continuity axis, and that exclusion is PRINTED on every run rather
|
|
27
|
+
* than buried in a predicate.
|
|
28
|
+
*
|
|
29
|
+
* WHAT THIS MODULE IS NOT
|
|
30
|
+
* -----------------------
|
|
31
|
+
* It does not decide policy. It reads a committed inventory, checks that the
|
|
32
|
+
* inventory still describes the tree (every `locus` resolves; every discovered
|
|
33
|
+
* leaf is disposed of), and derives the five numbers from the dispositions.
|
|
34
|
+
* The ratchet that forbids growth lives in `check_continuity_surface.ts`, which
|
|
35
|
+
* runs these same functions over a materialised base tree.
|
|
36
|
+
*/
|
|
37
|
+
import * as fs from 'node:fs';
|
|
38
|
+
import * as path from 'node:path';
|
|
39
|
+
|
|
40
|
+
/** Repo-relative POSIX path of the committed inventory. */
|
|
41
|
+
export const INVENTORY_POSIX = 'src/config/continuity-surface.json';
|
|
42
|
+
|
|
43
|
+
/** Source roots the behavioural discovery walks. */
|
|
44
|
+
export const DISCOVERY_ROOTS: readonly string[] = [
|
|
45
|
+
'src/scripts',
|
|
46
|
+
'src/agent-src/scripts',
|
|
47
|
+
'src/cli',
|
|
48
|
+
'src/shared',
|
|
49
|
+
'src/server',
|
|
50
|
+
'src/install',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The five axes, in the order the parent roadmap's goal states them. The order
|
|
55
|
+
* is part of the contract: the goal reads `0 / 0 / 1 / 1 / 0` positionally, and
|
|
56
|
+
* a reader comparing a run against that line needs the same positions.
|
|
57
|
+
*/
|
|
58
|
+
export const AXES = [
|
|
59
|
+
'public_continuity_commands',
|
|
60
|
+
'session_resume_pickers',
|
|
61
|
+
'persistent_continuity_artefacts',
|
|
62
|
+
'continuity_schemas',
|
|
63
|
+
'normal_path_manual_actions',
|
|
64
|
+
] as const;
|
|
65
|
+
|
|
66
|
+
export type Axis = (typeof AXES)[number];
|
|
67
|
+
|
|
68
|
+
/** The target the parent roadmap's goal states, by axis. */
|
|
69
|
+
export const TARGETS: Readonly<Record<Axis, number>> = {
|
|
70
|
+
public_continuity_commands: 0,
|
|
71
|
+
session_resume_pickers: 0,
|
|
72
|
+
persistent_continuity_artefacts: 1,
|
|
73
|
+
continuity_schemas: 1,
|
|
74
|
+
normal_path_manual_actions: 0,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export interface InventoryRow {
|
|
78
|
+
/** Stable identifier. For a discovered artefact this is the state leaf. */
|
|
79
|
+
id: string;
|
|
80
|
+
/** Which axis this row belongs to. */
|
|
81
|
+
axis: Axis;
|
|
82
|
+
/** `counted` contributes to the axis number; `excluded` does not. */
|
|
83
|
+
disposition: 'counted' | 'excluded';
|
|
84
|
+
/**
|
|
85
|
+
* Repo-relative `file` or `file:line` the row is anchored to. It must
|
|
86
|
+
* resolve, which is what makes a retirement MEASURED: deleting the surface
|
|
87
|
+
* without deleting or re-disposing its row reddens.
|
|
88
|
+
*/
|
|
89
|
+
locus: string;
|
|
90
|
+
/**
|
|
91
|
+
* Why. Mandatory on every row, not only on exclusions — a `counted` row
|
|
92
|
+
* whose reason nobody wrote is a number without a meaning.
|
|
93
|
+
*/
|
|
94
|
+
reason: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface Inventory {
|
|
98
|
+
rows: readonly InventoryRow[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A leaf the discovery found, with the first source position that names it. */
|
|
102
|
+
export interface DiscoveredLeaf {
|
|
103
|
+
leaf: string;
|
|
104
|
+
locus: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const LEAF_RE =
|
|
108
|
+
/agents\/runtime\/state\/([A-Za-z0-9_.-]+)|['"]runtime['"],\s*['"]state['"],\s*['"]([A-Za-z0-9_.-]+)['"]/g;
|
|
109
|
+
|
|
110
|
+
const SOURCE_EXT = /\.(ts|js|mjs|sh|bash)$/;
|
|
111
|
+
|
|
112
|
+
function _walk(dir: string, out: string[]): void {
|
|
113
|
+
let entries: fs.Dirent[];
|
|
114
|
+
try {
|
|
115
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
116
|
+
} catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const full = path.join(dir, entry.name);
|
|
121
|
+
if (entry.isDirectory()) {
|
|
122
|
+
_walk(full, out);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!SOURCE_EXT.test(entry.name)) continue;
|
|
126
|
+
// Test files name state paths in fixtures; a fixture is not a surface.
|
|
127
|
+
if (entry.name.endsWith('.test.ts')) continue;
|
|
128
|
+
out.push(full);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Every distinct `agents/runtime/state/<leaf>` any source file names, with the
|
|
134
|
+
* first `file:line` that names it. Sorted, so two runs over the same tree
|
|
135
|
+
* produce byte-identical output and a diff of the count is a diff of the tree.
|
|
136
|
+
*/
|
|
137
|
+
export function discoverStateLeaves(root: string): DiscoveredLeaf[] {
|
|
138
|
+
const files: string[] = [];
|
|
139
|
+
for (const rel of DISCOVERY_ROOTS) {
|
|
140
|
+
_walk(path.join(root, rel), files);
|
|
141
|
+
}
|
|
142
|
+
files.sort();
|
|
143
|
+
const found = new Map<string, string>();
|
|
144
|
+
for (const file of files) {
|
|
145
|
+
let text: string;
|
|
146
|
+
try {
|
|
147
|
+
text = fs.readFileSync(file, 'utf-8');
|
|
148
|
+
} catch {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const rel = path.relative(root, file).split(path.sep).join('/');
|
|
152
|
+
const lines = text.split('\n');
|
|
153
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
154
|
+
const line = lines[i] ?? '';
|
|
155
|
+
LEAF_RE.lastIndex = 0;
|
|
156
|
+
let match: RegExpExecArray | null;
|
|
157
|
+
while ((match = LEAF_RE.exec(line)) !== null) {
|
|
158
|
+
const raw = match[1] ?? match[2] ?? '';
|
|
159
|
+
// A trailing dot is string concatenation in the source
|
|
160
|
+
// (`'...json' + '.tmp'`), never part of the leaf name.
|
|
161
|
+
const leaf = raw.replace(/\.+$/, '');
|
|
162
|
+
if (leaf === '' || leaf === '.') continue;
|
|
163
|
+
if (!found.has(leaf)) found.set(leaf, `${rel}:${String(i + 1)}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return [...found.keys()].sort().map((leaf) => ({ leaf, locus: found.get(leaf) ?? '' }));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function readInventory(root: string): Inventory {
|
|
171
|
+
const file = path.join(root, INVENTORY_POSIX);
|
|
172
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf-8')) as { rows?: unknown };
|
|
173
|
+
const rows = Array.isArray(parsed.rows) ? (parsed.rows as InventoryRow[]) : [];
|
|
174
|
+
return { rows };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Does a `file` or `file:line` locus still resolve in this tree? */
|
|
178
|
+
export function locusResolves(root: string, locus: string): boolean {
|
|
179
|
+
const match = /^(.*?):(\d+)$/.exec(locus);
|
|
180
|
+
const rel = match ? (match[1] ?? '') : locus;
|
|
181
|
+
const target = path.join(root, rel);
|
|
182
|
+
let stat: fs.Stats;
|
|
183
|
+
try {
|
|
184
|
+
stat = fs.statSync(target);
|
|
185
|
+
} catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
if (!match) return true;
|
|
189
|
+
if (!stat.isFile()) return false;
|
|
190
|
+
// A line anchor must still BE a line. Content is deliberately not compared:
|
|
191
|
+
// a locus that pins prose would rot on every reflow and turn the inventory
|
|
192
|
+
// into a line-number chore, which is how anchor checks get deleted.
|
|
193
|
+
const line = Number(match[2]);
|
|
194
|
+
let text: string;
|
|
195
|
+
try {
|
|
196
|
+
text = fs.readFileSync(target, 'utf-8');
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
return line >= 1 && line <= text.split('\n').length;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface SurfaceCounts {
|
|
204
|
+
public_continuity_commands: number;
|
|
205
|
+
session_resume_pickers: number;
|
|
206
|
+
persistent_continuity_artefacts: number;
|
|
207
|
+
continuity_schemas: number;
|
|
208
|
+
normal_path_manual_actions: number;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function countAxes(inv: Inventory): SurfaceCounts {
|
|
212
|
+
const counts = {
|
|
213
|
+
public_continuity_commands: 0,
|
|
214
|
+
session_resume_pickers: 0,
|
|
215
|
+
persistent_continuity_artefacts: 0,
|
|
216
|
+
continuity_schemas: 0,
|
|
217
|
+
normal_path_manual_actions: 0,
|
|
218
|
+
};
|
|
219
|
+
for (const row of inv.rows) {
|
|
220
|
+
if (row.disposition !== 'counted') continue;
|
|
221
|
+
if (!AXES.includes(row.axis)) continue;
|
|
222
|
+
counts[row.axis] += 1;
|
|
223
|
+
}
|
|
224
|
+
return counts;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface InventoryFinding {
|
|
228
|
+
kind: 'undisposed-leaf' | 'dead-locus' | 'bad-row' | 'duplicate-row';
|
|
229
|
+
detail: string;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Validate the inventory against the tree.
|
|
234
|
+
*
|
|
235
|
+
* Three failure classes, and each exists because a specific way of faking a
|
|
236
|
+
* lower number would otherwise work:
|
|
237
|
+
*
|
|
238
|
+
* - `undisposed-leaf` — a persisted-state leaf the source names and the
|
|
239
|
+
* inventory does not mention. This is the add-a-surface-quietly route.
|
|
240
|
+
* - `dead-locus` — a row anchored to something that no longer exists. This
|
|
241
|
+
* is the claim-a-retirement-that-did-not-happen route in reverse: it forces
|
|
242
|
+
* the row to be deleted (a real subtraction) rather than left standing.
|
|
243
|
+
* - `bad-row` / `duplicate-row` — shape defects that would make either of the
|
|
244
|
+
* above unenforceable.
|
|
245
|
+
*/
|
|
246
|
+
export function validateInventory(
|
|
247
|
+
root: string,
|
|
248
|
+
inv: Inventory,
|
|
249
|
+
discovered: readonly DiscoveredLeaf[],
|
|
250
|
+
): InventoryFinding[] {
|
|
251
|
+
const findings: InventoryFinding[] = [];
|
|
252
|
+
const seen = new Set<string>();
|
|
253
|
+
for (const row of inv.rows) {
|
|
254
|
+
const id = typeof row.id === 'string' ? row.id : '';
|
|
255
|
+
if (id === '') {
|
|
256
|
+
findings.push({ kind: 'bad-row', detail: 'a row carries no `id`' });
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (seen.has(id)) {
|
|
260
|
+
findings.push({ kind: 'duplicate-row', detail: `two rows share the id \`${id}\`` });
|
|
261
|
+
}
|
|
262
|
+
seen.add(id);
|
|
263
|
+
if (!AXES.includes(row.axis)) {
|
|
264
|
+
findings.push({ kind: 'bad-row', detail: `\`${id}\`: unknown axis \`${String(row.axis)}\`` });
|
|
265
|
+
}
|
|
266
|
+
if (row.disposition !== 'counted' && row.disposition !== 'excluded') {
|
|
267
|
+
findings.push({
|
|
268
|
+
kind: 'bad-row',
|
|
269
|
+
detail: `\`${id}\`: disposition must be \`counted\` or \`excluded\`, got \`${String(row.disposition)}\``,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (typeof row.reason !== 'string' || row.reason.trim().length < 20) {
|
|
273
|
+
findings.push({
|
|
274
|
+
kind: 'bad-row',
|
|
275
|
+
detail: `\`${id}\`: every row needs a reason of substance — a number without a stated meaning is not an inventory`,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (typeof row.locus !== 'string' || row.locus.trim() === '') {
|
|
279
|
+
findings.push({ kind: 'bad-row', detail: `\`${id}\`: no \`locus\`` });
|
|
280
|
+
} else if (!locusResolves(root, row.locus)) {
|
|
281
|
+
findings.push({
|
|
282
|
+
kind: 'dead-locus',
|
|
283
|
+
detail: `\`${id}\`: locus \`${row.locus}\` does not resolve — if the surface was retired, DELETE the row (that is the subtraction); if it moved, re-anchor it`,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
for (const item of discovered) {
|
|
288
|
+
if (!seen.has(item.leaf)) {
|
|
289
|
+
findings.push({
|
|
290
|
+
kind: 'undisposed-leaf',
|
|
291
|
+
detail: `\`${item.leaf}\` (${item.locus}) is persisted state under agents/runtime/state/ that the inventory does not dispose of — add a row with \`disposition: counted\` (it is continuity state) or \`disposition: excluded\` with the reason it is not`,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return findings;
|
|
296
|
+
}
|