@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
|
@@ -42,3 +42,130 @@ export function writesThinFiles(mode: LeanProjectionMode): boolean {
|
|
|
42
42
|
export function deliversBodies(mode: LeanProjectionMode): boolean {
|
|
43
43
|
return mode === 'delivery';
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The hosts a thinning mode may actually thin.
|
|
48
|
+
*
|
|
49
|
+
* Deliberately NOT "every host id the package knows". `condense`'s `TOOL_DIRS`
|
|
50
|
+
* writes a per-rule tree for exactly three hosts, so those are the only ids for
|
|
51
|
+
* which `lean_projection.hosts` can mean anything: naming `windsurf` there would
|
|
52
|
+
* be a request to thin a single concatenated file that has no per-rule stub
|
|
53
|
+
* shape, and naming `codex` a request to thin a tree the installer writes and
|
|
54
|
+
* `condense` never touches. Accepting such an id silently would leave the
|
|
55
|
+
* operator believing a host was scoped when nothing reads the entry.
|
|
56
|
+
*/
|
|
57
|
+
export const THINNABLE_HOSTS: readonly string[] = ['claude-code', 'cursor', 'cline'];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The shipped default host set for a thinning mode.
|
|
61
|
+
*
|
|
62
|
+
* One host, and it is the one host that both binds `pre_tool_use` and honours a
|
|
63
|
+
* deny (`docs/enforcement-by-host.md:18-28`) — i.e. the only host where the
|
|
64
|
+
* delivery concern's verdict is acted on. Widening this is a decision, never a
|
|
65
|
+
* default: see `resolveLeanProjectionHosts`, which never widens implicitly.
|
|
66
|
+
*/
|
|
67
|
+
export const DEFAULT_LEAN_PROJECTION_HOSTS: readonly string[] = ['claude-code'];
|
|
68
|
+
|
|
69
|
+
/** Why an id in a configured `hosts:` list was not kept. */
|
|
70
|
+
export type DroppedHostReason = 'unknown' | 'not-thinnable';
|
|
71
|
+
|
|
72
|
+
export interface LeanProjectionHosts {
|
|
73
|
+
/** The ids that survived, sorted and de-duplicated. */
|
|
74
|
+
readonly hosts: readonly string[];
|
|
75
|
+
/** Ids that were dropped, with the reason — reported, never silently discarded. */
|
|
76
|
+
readonly dropped: ReadonlyArray<{ readonly id: string; readonly reason: DroppedHostReason }>;
|
|
77
|
+
/** True when nothing was configured and the default was applied. */
|
|
78
|
+
readonly usedDefault: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolve a configured `lean_projection.hosts` list.
|
|
83
|
+
*
|
|
84
|
+
* Three properties, each of which is a failure this repository has seen in a
|
|
85
|
+
* neighbouring config surface:
|
|
86
|
+
*
|
|
87
|
+
* 1. **Absent means the default, never "all".** An empty or missing list
|
|
88
|
+
* resolves to `DEFAULT_LEAN_PROJECTION_HOSTS`. A key nobody set must not
|
|
89
|
+
* thin every host — that is D1, the defect this axis exists to repair.
|
|
90
|
+
* 2. **The set never widens implicitly.** Every id is checked against
|
|
91
|
+
* `THINNABLE_HOSTS`; anything else is dropped. A typo cannot enrol a host,
|
|
92
|
+
* and neither can a real host id that has no per-rule tree.
|
|
93
|
+
* 3. **A drop is reported, not swallowed.** The caller gets the ids and the
|
|
94
|
+
* reason so it can print them. Silently ignoring an entry an operator wrote
|
|
95
|
+
* is how a setting comes to mean nothing while looking configured.
|
|
96
|
+
*
|
|
97
|
+
* A configured list that resolves to NOTHING keeps `usedDefault: false` and an
|
|
98
|
+
* empty `hosts` — that is "thin no host", which is a legitimate and safe answer
|
|
99
|
+
* and must not fall back to the default. Falling back there would turn a
|
|
100
|
+
* fully-typo'd list into a Claude Code flip the operator never asked for.
|
|
101
|
+
*
|
|
102
|
+
* WHICH FILES REACH THIS AT ALL, stated because R2 finding 6 read the drop path
|
|
103
|
+
* as unreachable: `agent-settings.schema.json` and the wizard's Zod schema both
|
|
104
|
+
* constrain the items to an `enum`, so an out-of-vocabulary id written through
|
|
105
|
+
* either surface is a hard validation FAILURE and never arrives here. What
|
|
106
|
+
* arrives here is a HAND-EDITED `.agent-settings.yml` — the file
|
|
107
|
+
* `condense._lean_projection_settings` and `hooks/rule_inject_hook.gateOpen`
|
|
108
|
+
* both read directly, with no validation step in between. The two layers are
|
|
109
|
+
* defence in depth, not one contract stated twice: the schema is the authoring
|
|
110
|
+
* gate, this function is the read-time gate, and the drop wording below exists
|
|
111
|
+
* for the file the schema never saw.
|
|
112
|
+
*/
|
|
113
|
+
export function resolveLeanProjectionHosts(raw: unknown): LeanProjectionHosts {
|
|
114
|
+
const known = new Set(THINNABLE_HOSTS);
|
|
115
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
116
|
+
return { hosts: [...DEFAULT_LEAN_PROJECTION_HOSTS], dropped: [], usedDefault: true };
|
|
117
|
+
}
|
|
118
|
+
const hosts: string[] = [];
|
|
119
|
+
const dropped: Array<{ id: string; reason: DroppedHostReason }> = [];
|
|
120
|
+
for (const entry of raw) {
|
|
121
|
+
const id = typeof entry === 'string' ? entry.trim().toLowerCase() : '';
|
|
122
|
+
if (id === '') {
|
|
123
|
+
dropped.push({ id: String(entry), reason: 'unknown' });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (!known.has(id)) {
|
|
127
|
+
// `not-thinnable` is reserved for an id this package genuinely knows
|
|
128
|
+
// as a host but which owns no per-rule tree. Everything else is a
|
|
129
|
+
// typo or an invention, and the two deserve different wording
|
|
130
|
+
// because the operator's next action differs: fix the spelling, or
|
|
131
|
+
// learn that the host cannot be thinned at all.
|
|
132
|
+
dropped.push({ id, reason: KNOWN_NON_THINNABLE_HOSTS.has(id) ? 'not-thinnable' : 'unknown' });
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!hosts.includes(id)) hosts.push(id);
|
|
136
|
+
}
|
|
137
|
+
hosts.sort();
|
|
138
|
+
return { hosts, dropped, usedDefault: false };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Host ids the package ships surfaces for that own no per-rule rule tree.
|
|
143
|
+
*
|
|
144
|
+
* Only used to word a drop precisely; it grants nothing. Sourced from
|
|
145
|
+
* `condense._ALL_TOOLS` plus the two hosts outside it that the host table
|
|
146
|
+
* carries (`codex`, `cowork`).
|
|
147
|
+
*/
|
|
148
|
+
const KNOWN_NON_THINNABLE_HOSTS: ReadonlySet<string> = new Set([
|
|
149
|
+
'claude-desktop', 'augment', 'copilot', 'windsurf', 'gemini', 'codex', 'cowork',
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
/** One line per dropped id, for a caller that prints warnings. Empty when nothing was dropped. */
|
|
153
|
+
export function describeDroppedHosts(res: LeanProjectionHosts): string[] {
|
|
154
|
+
return res.dropped.map(({ id, reason }) =>
|
|
155
|
+
reason === 'not-thinnable'
|
|
156
|
+
? `lean_projection.hosts: "${id}" is a known host but has no per-rule rule tree — dropped`
|
|
157
|
+
: `lean_projection.hosts: "${id}" is not a thinnable host id — dropped`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Does the projector write stubs for this host, under this mode and host set?
|
|
163
|
+
*
|
|
164
|
+
* The single predicate `condense` calls. Mode is checked FIRST and on purpose:
|
|
165
|
+
* with `mode` unset the answer is `false` for every host regardless of what
|
|
166
|
+
* `hosts` says, so a `hosts:` list left behind after a rollback to `eager-all`
|
|
167
|
+
* thins nothing.
|
|
168
|
+
*/
|
|
169
|
+
export function thinsHost(mode: LeanProjectionMode, hosts: readonly string[], hostId: string): boolean {
|
|
170
|
+
return writesThinFiles(mode) && hosts.includes(hostId);
|
|
171
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary-payload eligibility — the mechanically verified predicate that
|
|
3
|
+
* reopened the `binary` class.
|
|
4
|
+
*
|
|
5
|
+
* WHAT THIS IS, SAID PLAINLY.
|
|
6
|
+
*
|
|
7
|
+
* `f573d628c` closed `binary` as a hard zero on 2026-09-07. This REOPENS it,
|
|
8
|
+
* 24 hours later, and that IS A WEAKENING OF A CONTROL. An AI council
|
|
9
|
+
* (2026-09-08, 2 seats, unanimous, deep tier) authorised it, and one seat
|
|
10
|
+
* explicitly refused the framing that this is merely "correcting an
|
|
11
|
+
* over-generalisation" — that distinction is cosmetic. The control was closed
|
|
12
|
+
* for the wrong reason, so reopening it is a weakening: justified, and still a
|
|
13
|
+
* weakening. It is written down here rather than softened because a control
|
|
14
|
+
* that gets quietly relaxed is worse than one that never existed.
|
|
15
|
+
*
|
|
16
|
+
* WHY THE CLOSURE WAS WRONG.
|
|
17
|
+
*
|
|
18
|
+
* Decision A over-generalised. Its reasoning was "the three observed binaries
|
|
19
|
+
* are unused, therefore no binary can ever be required" — an invalid
|
|
20
|
+
* generalisation from an observed sample to a universal rule. The vendored
|
|
21
|
+
* tree-sitter grammars are the direct counterexample: bytes the production path
|
|
22
|
+
* loads and the engine requires. A textual placeholder was lossless for three
|
|
23
|
+
* PNGs nothing decoded; for a grammar it does not degrade a fixture, it breaks
|
|
24
|
+
* the engine.
|
|
25
|
+
*
|
|
26
|
+
* WHAT REPLACES THE HARD ZERO — AND WHAT DOES NOT.
|
|
27
|
+
*
|
|
28
|
+
* Not a prose exception. A binary is admitted only when EVERY condition below
|
|
29
|
+
* is verified mechanically, on the bytes that are actually in the tarball:
|
|
30
|
+
*
|
|
31
|
+
* 1. exactly one manifest entry for the path
|
|
32
|
+
* 2. no wildcard or directory grant anywhere in the manifest
|
|
33
|
+
* 3. the packed tarball really contains the path
|
|
34
|
+
* 4. sha256(bytes) equals the manifest hash
|
|
35
|
+
* 5. size equals the manifest size
|
|
36
|
+
* 6. kind is `tree-sitter-wasm-grammar`
|
|
37
|
+
* 7. WASM magic and version are valid
|
|
38
|
+
* 8. the PRODUCTION loader reports the manifest's grammar_id
|
|
39
|
+
* 9. the PRODUCTION loader reports the manifest's ABI
|
|
40
|
+
* 10. a defined invalid-header mutation of the bytes is REJECTED
|
|
41
|
+
* 11. a defined unsupported-ABI expectation is REJECTED
|
|
42
|
+
*
|
|
43
|
+
* 8 and 9 go through `probeGrammarFile`, the engine's own loader, deliberately:
|
|
44
|
+
* a second implementation inside the gate could accept bytes the engine
|
|
45
|
+
* rejects, which is exactly the gap that would let a broken grammar ship while
|
|
46
|
+
* the gate called it valid.
|
|
47
|
+
*
|
|
48
|
+
* 10 and 11 run on every invocation, not in a separate test, because a
|
|
49
|
+
* predicate whose refusals nobody exercises is a predicate nobody has checked.
|
|
50
|
+
*
|
|
51
|
+
* `archive` IS NOT REOPENED.
|
|
52
|
+
*
|
|
53
|
+
* This collision produced evidence about grammars and none whatsoever about
|
|
54
|
+
* archives. `archive` stays a hard zero with no manifest path, and this module
|
|
55
|
+
* offers none.
|
|
56
|
+
*
|
|
57
|
+
* ELIGIBILITY NEVER TOUCHES THE MEASUREMENT.
|
|
58
|
+
*
|
|
59
|
+
* The gate reports `observed` as the true count and byte total on every run.
|
|
60
|
+
* Compliance is `observed <= allowed`, where `allowed` is the number of entries
|
|
61
|
+
* that passed every condition above — never `observed` minus something excused.
|
|
62
|
+
* A file that fails one condition lowers `allowed`; it never disappears from
|
|
63
|
+
* `observed`.
|
|
64
|
+
*
|
|
65
|
+
* THE THREAT BOUNDARY — WHO THIS REFUSES, AND WHO IT DOES NOT.
|
|
66
|
+
*
|
|
67
|
+
* Written down because the eleven conditions read as a security control and are
|
|
68
|
+
* only half of one, and a reader could not previously tell that boundary from
|
|
69
|
+
* an oversight. The `release/14.22.0` self-review finding `d1696732ac28` is
|
|
70
|
+
* exactly that misreading: it reached for a signature this repository has no
|
|
71
|
+
* infrastructure to provide (`grep -rniE 'cosign|sigstore|minisign|slsa'`
|
|
72
|
+
* returns nothing in this tree), because nothing here said where the line was.
|
|
73
|
+
*
|
|
74
|
+
* REFUSED. A byte that drifts from what the manifest says — a corrupted copy, a
|
|
75
|
+
* truncated checkout, a refresh that updated the file and forgot the row, a
|
|
76
|
+
* grammar whose ABI moved under a version bump, a second row quietly widening a
|
|
77
|
+
* path. Each of those is one side of the pair changing while the other does
|
|
78
|
+
* not, and every condition above is a re-derivation from the bytes rather than
|
|
79
|
+
* a restatement of the row.
|
|
80
|
+
*
|
|
81
|
+
* NOT REFUSED, DELIBERATELY. An attacker who edits the manifest and the bytes in
|
|
82
|
+
* the SAME COMMIT. Every condition here reads the manifest, so a same-commit
|
|
83
|
+
* pair is internally consistent and passes all eleven. That case is out of
|
|
84
|
+
* scope for this module and is held by review instead: the manifest is a small
|
|
85
|
+
* JSON file whose diff a human reads, and a hash changing next to a binary
|
|
86
|
+
* changing is the most visible shape a diff has.
|
|
87
|
+
*
|
|
88
|
+
* WHAT CLOSES IT, AND WHERE. `_lib/vendored_grammar_upstream.ts` compares the
|
|
89
|
+
* vendored bytes against the `tree-sitter-wasms` release pinned in
|
|
90
|
+
* `package-lock.json` and installed by `npm ci` — a source outside this
|
|
91
|
+
* repository's own diff — and refuses rather than passes when that comparison
|
|
92
|
+
* cannot be made. It is exercised by `tests/scripts/vendored_grammar_upstream.test.ts`,
|
|
93
|
+
* which is where a reader auditing "what enforces these bytes" should go next.
|
|
94
|
+
* Its own docblock states what it may not claim: it is not independent of all
|
|
95
|
+
* repository state, and it does not verify the grammars against the registry
|
|
96
|
+
* independent of local build state.
|
|
97
|
+
*
|
|
98
|
+
* STILL OPEN, AND OWNED BY REVIEW. A `package-lock.json` row that points at a
|
|
99
|
+
* hostile package. No check in this tree defends that; the lock is
|
|
100
|
+
* human-readable text declaring dependencies, and reviewing it is the control.
|
|
101
|
+
*/
|
|
102
|
+
import { createHash } from 'node:crypto';
|
|
103
|
+
import * as fs from 'node:fs';
|
|
104
|
+
import * as path from 'node:path';
|
|
105
|
+
|
|
106
|
+
/** One manifest row. Path-exact; `why` is documentation, never a condition. */
|
|
107
|
+
export interface PackedBinaryEntry {
|
|
108
|
+
readonly path: string;
|
|
109
|
+
readonly sha256: string;
|
|
110
|
+
readonly size: number;
|
|
111
|
+
readonly kind: string;
|
|
112
|
+
readonly grammar_id: string;
|
|
113
|
+
readonly abi: number;
|
|
114
|
+
readonly why?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface PackedBinaryManifest {
|
|
118
|
+
readonly schema_version: number;
|
|
119
|
+
readonly entries: readonly PackedBinaryEntry[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The one kind this predicate knows how to verify. */
|
|
123
|
+
export const VERIFIABLE_KIND = 'tree-sitter-wasm-grammar';
|
|
124
|
+
|
|
125
|
+
/** What the production loader reported for a file. */
|
|
126
|
+
export interface GrammarProbe {
|
|
127
|
+
readonly abi: number;
|
|
128
|
+
readonly grammar_id: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A per-file verdict, with the failing condition named. */
|
|
132
|
+
export interface EligibilityVerdict {
|
|
133
|
+
readonly path: string;
|
|
134
|
+
readonly eligible: boolean;
|
|
135
|
+
/** Every condition that failed, in predicate order. Empty iff eligible. */
|
|
136
|
+
readonly failures: readonly string[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** WASM preamble: `\0asm` followed by little-endian version 1. */
|
|
140
|
+
const WASM_MAGIC = [0x00, 0x61, 0x73, 0x6d] as const;
|
|
141
|
+
const WASM_VERSION = [0x01, 0x00, 0x00, 0x00] as const;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Condition 7, on its own so the header mutation in condition 10 has something
|
|
145
|
+
* specific to defeat.
|
|
146
|
+
*/
|
|
147
|
+
export function wasmMagicAndVersionValid(bytes: Uint8Array): boolean {
|
|
148
|
+
if (bytes.length < 8) return false;
|
|
149
|
+
for (let i = 0; i < 4; i += 1) if (bytes[i] !== WASM_MAGIC[i]) return false;
|
|
150
|
+
for (let i = 0; i < 4; i += 1) if (bytes[4 + i] !== WASM_VERSION[i]) return false;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The DEFINED invalid-header mutation for condition 10.
|
|
156
|
+
*
|
|
157
|
+
* One byte of the magic, flipped. Deliberately NOT `corrupt(bytes)`: arbitrary
|
|
158
|
+
* corruption can land in a custom section and leave a still-valid module, or
|
|
159
|
+
* hit padding and change nothing, so a test built on it can pass while proving
|
|
160
|
+
* nothing. This targets a property the predicate actually validates.
|
|
161
|
+
*/
|
|
162
|
+
export function definedInvalidHeaderMutation(bytes: Uint8Array): Uint8Array {
|
|
163
|
+
const out = Uint8Array.from(bytes);
|
|
164
|
+
if (out.length > 1) out[1] = (out[1] as number) ^ 0xff;
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* The DEFINED unsupported-ABI expectation for condition 11.
|
|
170
|
+
*
|
|
171
|
+
* The ABI is not a byte at a known offset — it is what the loader reports after
|
|
172
|
+
* parsing the module — so this mutates the EXPECTATION rather than the bytes:
|
|
173
|
+
* an entry claiming an ABI the grammar does not have must be refused. Same
|
|
174
|
+
* validated property, targeted from the side where it can be stated exactly.
|
|
175
|
+
*/
|
|
176
|
+
export function definedUnsupportedAbiEntry(entry: PackedBinaryEntry): PackedBinaryEntry {
|
|
177
|
+
return { ...entry, abi: entry.abi + 1 };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Condition 2, evaluated over the whole manifest rather than per file. */
|
|
181
|
+
export function wildcardGrants(manifest: PackedBinaryManifest): string[] {
|
|
182
|
+
return manifest.entries
|
|
183
|
+
.filter((e) => e.path.includes('*') || e.path.includes('?') || e.path.endsWith('/'))
|
|
184
|
+
.map((e) => e.path);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Paths carrying more than one row — condition 1's uniqueness half. */
|
|
188
|
+
export function duplicatePaths(manifest: PackedBinaryManifest): string[] {
|
|
189
|
+
const seen = new Map<string, number>();
|
|
190
|
+
for (const e of manifest.entries) seen.set(e.path, (seen.get(e.path) ?? 0) + 1);
|
|
191
|
+
return [...seen.entries()].filter(([, n]) => n > 1).map(([p]) => p);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Evaluate every condition for one packed file.
|
|
196
|
+
*
|
|
197
|
+
* `probe` is injected so the caller decides how the production loader is
|
|
198
|
+
* reached — in the gate it is a child process, in a test it can be a stub that
|
|
199
|
+
* returns a wrong ABI. `null` means the loader could not answer, which fails
|
|
200
|
+
* conditions 8 and 9 rather than passing them by default.
|
|
201
|
+
*/
|
|
202
|
+
export function evaluateEntry(options: {
|
|
203
|
+
readonly filePath: string;
|
|
204
|
+
readonly packedPaths: ReadonlySet<string>;
|
|
205
|
+
readonly manifest: PackedBinaryManifest;
|
|
206
|
+
readonly bytes: Uint8Array | null;
|
|
207
|
+
readonly probe: GrammarProbe | null;
|
|
208
|
+
}): EligibilityVerdict {
|
|
209
|
+
const { filePath, packedPaths, manifest, bytes, probe } = options;
|
|
210
|
+
const failures: string[] = [];
|
|
211
|
+
|
|
212
|
+
const rows = manifest.entries.filter((e) => e.path === filePath);
|
|
213
|
+
if (rows.length === 0) failures.push('no manifest entry for this path');
|
|
214
|
+
if (rows.length > 1) failures.push('more than one manifest entry for this path');
|
|
215
|
+
const wild = wildcardGrants(manifest);
|
|
216
|
+
if (wild.length > 0) failures.push(`manifest carries wildcard or directory grants: ${wild.join(', ')}`);
|
|
217
|
+
if (!packedPaths.has(filePath)) failures.push('path is not in the packed tarball');
|
|
218
|
+
|
|
219
|
+
const entry = rows[0];
|
|
220
|
+
if (entry === undefined) return { path: filePath, eligible: false, failures };
|
|
221
|
+
|
|
222
|
+
if (bytes === null) {
|
|
223
|
+
failures.push('bytes could not be read');
|
|
224
|
+
return { path: filePath, eligible: false, failures };
|
|
225
|
+
}
|
|
226
|
+
const digest = createHash('sha256').update(bytes).digest('hex');
|
|
227
|
+
if (digest !== entry.sha256) failures.push(`sha256 mismatch: ${digest} != ${entry.sha256}`);
|
|
228
|
+
if (bytes.length !== entry.size) failures.push(`size mismatch: ${String(bytes.length)} != ${String(entry.size)}`);
|
|
229
|
+
if (entry.kind !== VERIFIABLE_KIND) failures.push(`kind is not ${VERIFIABLE_KIND}: ${entry.kind}`);
|
|
230
|
+
if (!wasmMagicAndVersionValid(bytes)) failures.push('WASM magic or version invalid');
|
|
231
|
+
|
|
232
|
+
if (probe === null) {
|
|
233
|
+
failures.push('the production loader could not read this file');
|
|
234
|
+
} else {
|
|
235
|
+
if (probe.grammar_id !== entry.grammar_id) {
|
|
236
|
+
failures.push(`grammar_id mismatch: loader says ${probe.grammar_id}, manifest says ${entry.grammar_id}`);
|
|
237
|
+
}
|
|
238
|
+
if (probe.abi !== entry.abi) {
|
|
239
|
+
failures.push(`ABI mismatch: loader says ${String(probe.abi)}, manifest says ${String(entry.abi)}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Conditions 10 and 11 — the predicate refusing what it must refuse. Run
|
|
244
|
+
// here, on every invocation, so a refusal path is never merely asserted in
|
|
245
|
+
// a test somebody can skip.
|
|
246
|
+
if (wasmMagicAndVersionValid(definedInvalidHeaderMutation(bytes))) {
|
|
247
|
+
failures.push('self-check failed: a defined invalid-header mutation was NOT rejected');
|
|
248
|
+
}
|
|
249
|
+
if (probe !== null && probe.abi === definedUnsupportedAbiEntry(entry).abi) {
|
|
250
|
+
failures.push('self-check failed: a defined unsupported-ABI expectation was NOT rejected');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return { path: filePath, eligible: failures.length === 0, failures };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Read and shape-check the manifest. Throws rather than degrading to empty. */
|
|
257
|
+
export function loadManifest(repoRoot: string): PackedBinaryManifest {
|
|
258
|
+
const p = path.join(repoRoot, 'src', 'config', 'packed-binary-manifest.json');
|
|
259
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf-8')) as PackedBinaryManifest;
|
|
260
|
+
if (!Array.isArray(raw.entries)) throw new Error(`packed-binary-manifest: entries must be an array (${p})`);
|
|
261
|
+
return raw;
|
|
262
|
+
}
|
|
@@ -253,10 +253,23 @@ function _parse_cli(argv: string[]): { cwd: string; installed: string; forward:
|
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
// Script entry — mirrors the Python `if __name__ == "__main__"` block.
|
|
256
|
+
// `build:delegate-aux` bundles this file with `--splitting`, which can move the
|
|
257
|
+
// module body into a shared chunk: `import.meta.url` then names the CHUNK, the
|
|
258
|
+
// URL comparison below never matches, and the entry silently no-ops. Inside that
|
|
259
|
+
// bundle the invoked file name is the reliable signal (same pattern as
|
|
260
|
+
// src/scripts/_cli/cmd_migrate.ts). Defined by that build only.
|
|
261
|
+
declare const __AGENT_CONFIG_CLI_DELEGATE__: boolean | undefined;
|
|
256
262
|
function _isCliEntry(): boolean {
|
|
257
263
|
if (process.argv[1] === undefined) {
|
|
258
264
|
return false;
|
|
259
265
|
}
|
|
266
|
+
if (typeof __AGENT_CONFIG_CLI_DELEGATE__ !== 'undefined' && __AGENT_CONFIG_CLI_DELEGATE__) {
|
|
267
|
+
// A miss falls THROUGH to the URL comparison below, which is the
|
|
268
|
+
// symlinked-invocation case that fallback exists for.
|
|
269
|
+
if (path.basename(process.argv[1], '.js') === 'pin_resolver') {
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
260
273
|
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
261
274
|
if (import.meta.url === argvUrl) {
|
|
262
275
|
return true;
|
|
@@ -73,11 +73,49 @@ export function recycle_envelope_rel(session_id: string | null | undefined): str
|
|
|
73
73
|
return _sessionKeyed(session_id, RECYCLE_ENVELOPE_REL, 'recycle-envelope', '.json');
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* THE KEYING CONTRACT, in one place because its absence is what allowed the
|
|
78
|
+
* 2026-09-09 defect: the record is keyed by the **producing session** — the one
|
|
79
|
+
* that ran `session:recycle` — and it is READ by the **consuming session**,
|
|
80
|
+
* which is a different session with a different id. Those two facts were each
|
|
81
|
+
* true in their own module and were never written down together, so the
|
|
82
|
+
* producer and the consumer agreed on a PATH and disagreed about whose id
|
|
83
|
+
* filled it. {@link resolveContinuityRecord} is where that is now reconciled:
|
|
84
|
+
* it admits the unique record that is NOT the reader's own.
|
|
85
|
+
*/
|
|
86
|
+
|
|
76
87
|
/** Consumed sibling of {@link recycle_envelope_rel}, same keying. */
|
|
77
88
|
export function recycle_consumed_rel(session_id: string | null | undefined): string {
|
|
78
89
|
return _sessionKeyed(session_id, RECYCLE_CONSUMED_REL, 'recycle-envelope', '.consumed.json');
|
|
79
90
|
}
|
|
80
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Where an UNUSABLE resident record is moved aside so the slot can be
|
|
94
|
+
* published into — never deleted.
|
|
95
|
+
*
|
|
96
|
+
* `road-to-continuity-writer-activation` step 1.1. The reader already discards
|
|
97
|
+
* a malformed, schema-invalid or expired record loudly
|
|
98
|
+
* ({@link resolveContinuityRecord}'s callers), so nothing is lost by taking it
|
|
99
|
+
* out of the authoritative name. What WOULD be lost by `unlink` is the evidence
|
|
100
|
+
* of why a resume did not happen, and that is the only artefact anyone
|
|
101
|
+
* debugging a missed resume has. Quarantine is one file per session and is
|
|
102
|
+
* pruned by the same janitor as the rest of `agents/runtime/state/`.
|
|
103
|
+
*
|
|
104
|
+
* Deliberately NOT the consumed name: `.consumed.json` is a claim that a
|
|
105
|
+
* successor read the record, and `predecessorTracePresent` reads exactly that
|
|
106
|
+
* file to corroborate a lineage. Writing a never-consumed record there would
|
|
107
|
+
* manufacture a predecessor trace, which is the one thing that check exists to
|
|
108
|
+
* refuse.
|
|
109
|
+
*/
|
|
110
|
+
export function recycle_quarantine_rel(session_id: string | null | undefined): string {
|
|
111
|
+
return _sessionKeyed(
|
|
112
|
+
session_id,
|
|
113
|
+
path.join(_STATE_REL, 'recycle-envelope.quarantined.json'),
|
|
114
|
+
'recycle-envelope',
|
|
115
|
+
'.quarantined.json',
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
81
119
|
/**
|
|
82
120
|
* Shared builder. Containment is asserted locally rather than trusted from
|
|
83
121
|
* `safe_stem`: a guarantee living in another module's implementation is one
|
|
@@ -133,44 +171,94 @@ export interface ContinuityResolution {
|
|
|
133
171
|
/**
|
|
134
172
|
* Which record THIS session may read.
|
|
135
173
|
*
|
|
136
|
-
*
|
|
174
|
+
* **A resume reads the PREDECESSOR's record, and this function used to look for
|
|
175
|
+
* the reader's own.** Corrected 2026-09-09 after measuring it: the producer
|
|
176
|
+
* writes under the PRODUCING session's id and the consumer arrives with its
|
|
177
|
+
* OWN, so the two never coincided and every successor was told `absent`. The
|
|
178
|
+
* mechanism resolved only for a caller with no session id at all — the one
|
|
179
|
+
* shape no host produces, and the only shape the 19 pre-existing consumer tests
|
|
180
|
+
* exercised. `road-to-a-recycle-envelope-that-is-consumed` carries the
|
|
181
|
+
* five-fixture table; AI council 2026-09-09, 2/2, chose the rule below.
|
|
182
|
+
*
|
|
183
|
+
* Four cases, and the second is the resume:
|
|
184
|
+
*
|
|
185
|
+
* 1. **Own record.** NEVER read. A session re-reading its own state is a loop,
|
|
186
|
+
* not a resume, and a second `session_start` for one session (resume, host
|
|
187
|
+
* reconnect) made it reachable. Excluded by construction here rather than
|
|
188
|
+
* guarded downstream.
|
|
189
|
+
* 2. **Exactly one record that is not the reader's own.** Read it. That is the
|
|
190
|
+
* predecessor.
|
|
191
|
+
* 3. **Several such records.** START CLEAN and say so — the existing refusal,
|
|
192
|
+
* unchanged and now reachable from both branches. Picking the newest is
|
|
193
|
+
* precisely the recency resolution the no-index rule below forbids.
|
|
194
|
+
* 4. **None.** `absent`.
|
|
137
195
|
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* is precisely the recency resolution the no-index rule forbids: it would
|
|
146
|
-
* hand this session a stranger's state and look like a successful resume.
|
|
196
|
+
* **Why this is not recency, stated because the rule it must not become is one
|
|
197
|
+
* line away:** nothing is sorted and nothing is compared by time. Uniqueness
|
|
198
|
+
* admits; ambiguity refuses. The peer-isolation intent the per-session key was
|
|
199
|
+
* introduced for therefore moves from the KEY to the REFUSAL — a live peer's
|
|
200
|
+
* record is no longer excluded by its filename, so the branch gate in
|
|
201
|
+
* `consume_recycle_envelope` is what keeps an unrelated session from resuming
|
|
202
|
+
* it. That gate is part of this rule, not a decoration on it.
|
|
147
203
|
*/
|
|
148
204
|
export function resolveContinuityRecord(
|
|
149
205
|
workspace_root: string,
|
|
150
206
|
session_id: string | null | undefined,
|
|
151
207
|
): ContinuityResolution {
|
|
152
208
|
const id = String(session_id ?? '').trim();
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return fs.existsSync(own)
|
|
156
|
-
? { file: own, reason: `continuity record for this session (${safe_stem(id)})` }
|
|
157
|
-
: { file: null, reason: `no continuity record for this session (${safe_stem(id)})` };
|
|
158
|
-
}
|
|
159
|
-
const candidates = listContinuityRecords(workspace_root);
|
|
209
|
+
const own = id === '' ? null : path.join(workspace_root, recycle_envelope_rel(id));
|
|
210
|
+
const candidates = listContinuityRecords(workspace_root).filter((f) => f !== own);
|
|
160
211
|
if (candidates.length === 0) {
|
|
161
|
-
return {
|
|
212
|
+
return {
|
|
213
|
+
file: null,
|
|
214
|
+
reason:
|
|
215
|
+
id === ''
|
|
216
|
+
? 'no continuity record present'
|
|
217
|
+
: `no predecessor record in this workspace (own: ${safe_stem(id)})`,
|
|
218
|
+
};
|
|
162
219
|
}
|
|
163
220
|
if (candidates.length === 1) {
|
|
164
|
-
return {
|
|
221
|
+
return {
|
|
222
|
+
file: candidates[0] as string,
|
|
223
|
+
reason:
|
|
224
|
+
id === ''
|
|
225
|
+
? 'one continuity record, no session id to key on'
|
|
226
|
+
: `predecessor record, one candidate (own: ${safe_stem(id)})`,
|
|
227
|
+
};
|
|
165
228
|
}
|
|
166
229
|
return {
|
|
167
230
|
file: null,
|
|
168
231
|
reason:
|
|
169
|
-
`starting clean: ${candidates.length} continuity records in this workspace and no
|
|
170
|
-
'
|
|
232
|
+
`starting clean: ${candidates.length} continuity records in this workspace and no way to tell ` +
|
|
233
|
+
'which is the predecessor — resuming from the newest would resume a peer session, not this one',
|
|
171
234
|
};
|
|
172
235
|
}
|
|
173
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Which session this one continues — read, never composed.
|
|
239
|
+
*
|
|
240
|
+
* The chain is derivable without any new artifact: when this session started,
|
|
241
|
+
* its reader consumed the predecessor's record and MOVED it to this session's
|
|
242
|
+
* consumed path (`recycle_consumed_rel`). So the file sitting there is, by
|
|
243
|
+
* construction, the record this session resumed from, and its `session_id` is
|
|
244
|
+
* the predecessor.
|
|
245
|
+
*
|
|
246
|
+
* Returns the explicit string `none` when there is nothing there. `none` is a
|
|
247
|
+
* CLAIM — "this session starts a chain" — and is deliberately not an empty
|
|
248
|
+
* value: a reader must be able to tell "no predecessor" from "nobody wrote the
|
|
249
|
+
* field", because only the second one is a reason to go looking.
|
|
250
|
+
*/
|
|
251
|
+
export function resolvePredecessor(projectRoot: string, sessionId: string | null): string {
|
|
252
|
+
try {
|
|
253
|
+
const consumed = path.join(projectRoot, recycle_consumed_rel(sessionId));
|
|
254
|
+
const raw = JSON.parse(fs.readFileSync(consumed, 'utf-8')) as { session_id?: unknown };
|
|
255
|
+
const prior = String(raw.session_id ?? '').trim();
|
|
256
|
+
return prior === '' ? 'none' : prior;
|
|
257
|
+
} catch {
|
|
258
|
+
return 'none';
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
174
262
|
/**
|
|
175
263
|
* Is there any trace in this workspace of the session a record names as its
|
|
176
264
|
* predecessor?
|