@event4u/agent-config 14.19.0 → 14.21.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 +351 -156
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +10 -0
- package/dist/agent-src/commands/agent-handoff.md +42 -41
- package/dist/agent-src/commands/analyze/inbox.md +1 -1
- package/dist/agent-src/commands/analyze/{reference-repo.md → repo.md} +247 -32
- package/dist/agent-src/commands/analyze/roadmap-repos.md +129 -0
- package/dist/agent-src/commands/analyze.md +12 -7
- package/dist/agent-src/commands/feature/plan.md +55 -16
- package/dist/agent-src/commands/feature/refactor.md +8 -3
- package/dist/agent-src/commands/humanize.md +48 -4
- package/dist/agent-src/contexts/execution/contract-decision-sheet.md +22 -1
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +11 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +35 -2
- package/dist/agent-src/contexts/execution/terminal-states.md +4 -1
- package/dist/agent-src/rules/evaluator-independence.md +4 -4
- package/dist/agent-src/rules/external-reference-deep-dive.md +2 -2
- package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
- package/dist/agent-src/skills/competitive-positioning/SKILL.md +2 -2
- package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
- package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +27 -11
- package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -1
- package/dist/agent-src/skills/markitdown/SKILL.md +2 -2
- package/dist/agent-src/skills/repomix-packer/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
- package/dist/agent-src/skills/technical-specification/SKILL.md +4 -0
- package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/features.md +5 -0
- package/dist/agent-src/templates/roadmaps.md +29 -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/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-KPBWFOPZ.js → chunk-2FTMENJL.js} +194 -37
- package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-2TAQ2K4V.js} +166 -1
- package/dist/cli-delegate/{chunk-GZTMRYCN.js → chunk-643Z7FIC.js} +79 -12
- package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
- package/dist/cli-delegate/{chunk-IDHUQIXP.js → chunk-J3RFNKG4.js} +41 -5
- 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_conformance.js +88 -110
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- 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 +1 -1
- 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 +62 -31
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -6
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -5
- package/dist/hooks/dispatch.js +3801 -2803
- package/dist/install/install.mjs +396 -168
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +809 -180
- package/dist/scripts/ai_council/content_ceiling.js +63 -0
- package/dist/scripts/ai_council/seat_policy.js +106 -0
- package/dist/scripts/ai_council/transport_resolver.js +21 -0
- package/dist/scripts/mcp_server/telemetry.js +148 -0
- package/docs/guidelines/agent-infra/naming.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/redundancy-taxonomy.md +28 -0
- package/llms.txt +14 -14
- package/package.json +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/gate-coverage.yml +55 -0
- package/src/config/gate-violation-baselines.json +18 -6
- package/src/config/release-gate-locality.yml +4 -3
- package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
- package/src/scripts/_lib/audit_field_provenance.ts +44 -3
- package/src/scripts/_lib/blocked_by_marker.ts +189 -0
- package/src/scripts/_lib/host_capability.ts +76 -1
- 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/orchestration_record.ts +23 -1
- package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
- package/src/scripts/_lib/release_highlights.ts +19 -211
- package/src/scripts/_lib/release_material.ts +27 -77
- package/src/scripts/_lib/release_position.ts +96 -0
- package/src/scripts/_lib/review_skipped_record.ts +17 -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/structured_ask.ts +117 -0
- package/src/scripts/_lib/subagent_capsule.ts +155 -3
- package/src/scripts/_lib/tty_prompt.ts +76 -0
- package/src/scripts/ai_council/api_transport.ts +163 -0
- package/src/scripts/ai_council/clients.ts +58 -63
- package/src/scripts/ai_council/config.ts +13 -0
- package/src/scripts/ai_council/content_ceiling.ts +79 -0
- package/src/scripts/ai_council/jury_aggregate.ts +178 -0
- package/src/scripts/ai_council/seat_policy.ts +137 -0
- package/src/scripts/ai_council/status_surface.ts +79 -0
- package/src/scripts/ai_council/transport_resolver.ts +59 -1
- package/src/scripts/ai_tells_rules.ts +446 -5
- package/src/scripts/ask_block_census.ts +499 -0
- package/src/scripts/bench_humanizer_eval.ts +264 -40
- package/src/scripts/bench_prose_tells_fp.ts +198 -0
- package/src/scripts/build_rule_trigger_matrix.ts +5 -0
- package/src/scripts/check_branch_work_committed.ts +234 -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 +0 -157
- package/src/scripts/council_cli.ts +18 -27
- package/src/scripts/detect_ai_tells.ts +142 -20
- package/src/scripts/handoff_context_hook.ts +54 -7
- package/src/scripts/harvest_reference_tokens.ts +698 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +31 -1
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +45 -1
- package/src/scripts/hooks/one_question_per_ask_hook.ts +191 -0
- package/src/scripts/hooks/session_eol_hook.ts +18 -7
- package/src/scripts/install-hooks.sh +52 -0
- package/src/scripts/install.ts +4 -4
- package/src/scripts/lint_agent_security.ts +316 -25
- package/src/scripts/lint_confusables.ts +42 -6
- package/src/scripts/lint_handoffs.ts +0 -132
- package/src/scripts/lint_hidden_unicode.ts +49 -7
- package/src/scripts/lint_instruction_smuggling.ts +42 -6
- package/src/scripts/lint_mcp_config_security.ts +42 -6
- package/src/scripts/lint_roadmap_blockers.ts +51 -1
- package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
- package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
- package/src/scripts/mcp_server/resources.ts +41 -0
- package/src/scripts/mcp_server/telemetry.ts +19 -4
- package/src/scripts/migrate_command_suggestions.ts +1 -1
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/probe_unblocked_ask.ts +135 -2
- package/src/scripts/release.ts +50 -95
- package/src/scripts/release_drill.ts +6 -10
- package/src/scripts/release_env.ts +1 -0
- package/src/scripts/release_publication.ts +40 -31
- package/src/scripts/self_repair_cli.ts +31 -1
- package/src/scripts/self_review_gate.ts +409 -15
- package/src/scripts/skill_scout.ts +69 -0
- package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
- package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-host MCP consent residuals (roadmap step 3.2).
|
|
3
|
+
*
|
|
4
|
+
* A RESIDUAL is what remains non-automatic after this package has installed:
|
|
5
|
+
* the step the human still has to take before the MCP server actually runs.
|
|
6
|
+
* The step's own framing is the design constraint — "a checklist of things that
|
|
7
|
+
* did work is noise, a named residual is actionable" — so this module models
|
|
8
|
+
* only what is left, and the surfaces that print it print nothing else.
|
|
9
|
+
*
|
|
10
|
+
* Provenance is carried, not assumed.
|
|
11
|
+
*
|
|
12
|
+
* Every residual says where its text came from, and the surfaces print it:
|
|
13
|
+
*
|
|
14
|
+
* - `vendor-doc` — the host's own documentation says so. Nobody here watched it
|
|
15
|
+
* happen. Vendor documentation drifts, so this is a starting value, never a
|
|
16
|
+
* fact about the machine the reader is on.
|
|
17
|
+
* - `observed` — a session on that host recorded it. Only an observation may
|
|
18
|
+
* replace a documented value, and only in that direction.
|
|
19
|
+
*
|
|
20
|
+
* An absent host is UNRECORDED, never "no residual".
|
|
21
|
+
*
|
|
22
|
+
* A host with no entry here has not been looked at. `residualFor` returns
|
|
23
|
+
* `null` and the callers say so in words, because printing nothing would make
|
|
24
|
+
* "this host needs no step" and "nobody checked this host" the same line — the
|
|
25
|
+
* exact confusion the capability manifest's own `default` provenance exists to
|
|
26
|
+
* prevent.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { describeHostCapabilities } from './host_capability.js';
|
|
30
|
+
|
|
31
|
+
/** Where a residual's text came from. Only an observation may replace a doc. */
|
|
32
|
+
export type ResidualSource = 'vendor-doc' | 'observed';
|
|
33
|
+
|
|
34
|
+
export interface ConsentResidual {
|
|
35
|
+
/** Capability-manifest host id (`claude`), not the installer tool id. */
|
|
36
|
+
hostId: string;
|
|
37
|
+
/** The step the human still has to take. One sentence, imperative. */
|
|
38
|
+
residual: string;
|
|
39
|
+
source: ResidualSource;
|
|
40
|
+
/** Why we believe it — a doc reference, or the session that observed it. */
|
|
41
|
+
cite: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Seeded residuals.
|
|
46
|
+
*
|
|
47
|
+
* DELIBERATELY SHORT. Only hosts whose residual is establishable today appear;
|
|
48
|
+
* a row invented for a host nobody checked would be the "vendor-doc residuals
|
|
49
|
+
* harden into asserted facts" risk this roadmap's own register names, arriving
|
|
50
|
+
* one step earlier than predicted. Absence here is the honest state, and the
|
|
51
|
+
* callers render it as such.
|
|
52
|
+
*/
|
|
53
|
+
const SEEDED: Readonly<Record<string, ConsentResidual>> = {
|
|
54
|
+
claude: {
|
|
55
|
+
hostId: 'claude',
|
|
56
|
+
residual:
|
|
57
|
+
'Approve the project-scoped `agent-config` server the first time the host '
|
|
58
|
+
+ 'prompts — the installer writes `.mcp.json` but does not pre-approve it.',
|
|
59
|
+
source: 'vendor-doc',
|
|
60
|
+
cite:
|
|
61
|
+
'Host documentation: a project-scope MCP server requires explicit approval, '
|
|
62
|
+
+ 'suppressible only through the user-global `enabledMcpjsonServers` key, '
|
|
63
|
+
+ 'which this package deliberately does not write '
|
|
64
|
+
+ '(blocker `mcp-user-scope-approval-consent`).',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Installer `--tools` id → capability-manifest host id, for the report. */
|
|
69
|
+
export const TOOL_TO_HOST: Readonly<Record<string, string>> = {
|
|
70
|
+
'claude-code': 'claude',
|
|
71
|
+
cursor: 'cursor',
|
|
72
|
+
'gemini-cli': 'gemini',
|
|
73
|
+
windsurf: 'windsurf',
|
|
74
|
+
cline: 'cline',
|
|
75
|
+
copilot: 'copilot',
|
|
76
|
+
augment: 'augment',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The residual for one host, or `null` when none is recorded.
|
|
81
|
+
*
|
|
82
|
+
* An OBSERVATION outranks the documented value, and in one direction only: a
|
|
83
|
+
* host whose capability row explicitly records `mcp_needs_manual_activation:
|
|
84
|
+
* false` — an observation, since the field's provenance reads `registry` only
|
|
85
|
+
* when the row wrote it — has no residual left, and that clearance is itself
|
|
86
|
+
* reported as `observed`. The reverse never happens: a documented residual is
|
|
87
|
+
* not upgraded to `observed` by anything short of a session.
|
|
88
|
+
*/
|
|
89
|
+
export function residualFor(hostId: string): ConsentResidual | null {
|
|
90
|
+
const { manifest, sources } = describeHostCapabilities(hostId);
|
|
91
|
+
if (sources.mcp_needs_manual_activation === 'registry' && !manifest.mcp_needs_manual_activation) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return SEEDED[hostId] ?? null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Was this host looked at at all? Distinguishes "cleared" from "unrecorded". */
|
|
98
|
+
export function isRecorded(hostId: string): boolean {
|
|
99
|
+
const { sources } = describeHostCapabilities(hostId);
|
|
100
|
+
return hostId in SEEDED || sources.mcp_needs_manual_activation === 'registry';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** One rendered line per host, for the doctor check and the installer report. */
|
|
104
|
+
export interface ResidualLine {
|
|
105
|
+
hostId: string;
|
|
106
|
+
/** `residual` · `cleared` · `unrecorded` — the three honest states. */
|
|
107
|
+
state: 'residual' | 'cleared' | 'unrecorded';
|
|
108
|
+
text: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Render the state of one host. Never silent, in any of the three states. */
|
|
112
|
+
export function residualLine(hostId: string): ResidualLine {
|
|
113
|
+
const r = residualFor(hostId);
|
|
114
|
+
if (r !== null) {
|
|
115
|
+
return {
|
|
116
|
+
hostId,
|
|
117
|
+
state: 'residual',
|
|
118
|
+
text: `${hostId}: ${r.residual} (source: ${r.source} — not observed here)`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (isRecorded(hostId)) {
|
|
122
|
+
return {
|
|
123
|
+
hostId,
|
|
124
|
+
state: 'cleared',
|
|
125
|
+
text: `${hostId}: nothing left to do (source: observed)`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
hostId,
|
|
130
|
+
state: 'unrecorded',
|
|
131
|
+
text: `${hostId}: no residual recorded — nobody checked this host, which is not the same as nothing to do`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The lines an installer run should print, for the tools it selected.
|
|
137
|
+
*
|
|
138
|
+
* Only hosts with a real residual OR an unrecorded state are returned: a host
|
|
139
|
+
* whose residual was observed away has nothing actionable to say, and the step
|
|
140
|
+
* asks for exactly the actionable set.
|
|
141
|
+
*/
|
|
142
|
+
export function residualReport(toolIds: Iterable<string>): ResidualLine[] {
|
|
143
|
+
const seen = new Set<string>();
|
|
144
|
+
const out: ResidualLine[] = [];
|
|
145
|
+
for (const toolId of toolIds) {
|
|
146
|
+
const hostId = TOOL_TO_HOST[toolId];
|
|
147
|
+
if (hostId === undefined || seen.has(hostId)) continue;
|
|
148
|
+
seen.add(hostId);
|
|
149
|
+
const line = residualLine(hostId);
|
|
150
|
+
if (line.state !== 'cleared') out.push(line);
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Print the residuals for one install's selected tools, or nothing.
|
|
157
|
+
*
|
|
158
|
+
* Lives here rather than inline in the installer's closing block for the same
|
|
159
|
+
* reason `registerMcpHosts` does: `install.ts` sits far over the source-size
|
|
160
|
+
* ratchet's ceiling, and the rendering belongs beside the table it renders.
|
|
161
|
+
*/
|
|
162
|
+
export function printResiduals(
|
|
163
|
+
toolIds: Iterable<string>,
|
|
164
|
+
quiet: boolean,
|
|
165
|
+
write: (s: string) => void = (s) => process.stdout.write(s),
|
|
166
|
+
): void {
|
|
167
|
+
if (quiet) return;
|
|
168
|
+
const lines = residualReport(toolIds);
|
|
169
|
+
if (lines.length === 0) return;
|
|
170
|
+
write(' Still needs you (MCP):\n');
|
|
171
|
+
for (const line of lines) write(` \u2022 ${line.text}\n`);
|
|
172
|
+
write('\n');
|
|
173
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* add one. (Mirrors `domain-safety-pii` § Surface 2 / artifact-engagement.)
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { appliedIds } from './audit_field_provenance.js';
|
|
19
20
|
import type { LookupClass } from './auto_dispatch.js';
|
|
20
21
|
import type { EvidenceBasis } from './evidence_basis.js';
|
|
21
22
|
// TYPE-ONLY on purpose. `runtime_journal.ts` imports `node:sqlite` at runtime,
|
|
@@ -104,6 +105,24 @@ export interface RecordInput {
|
|
|
104
105
|
* the rules bound in the contract.
|
|
105
106
|
*/
|
|
106
107
|
skills_applied?: string[] | null | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* Stable rule ids the run ACTUALLY carried, as an observation.
|
|
110
|
+
*
|
|
111
|
+
* Until 2026-09-07 this producer wrote the literal `['delegation-policy']`
|
|
112
|
+
* on every line, and so did `review_skipped_record.ts`. Mining the real
|
|
113
|
+
* stream with `extract_audit_patterns --min-count 2` therefore minted
|
|
114
|
+
* exactly ONE pattern at count 1074 over 1104 lines: not a regularity, just
|
|
115
|
+
* arithmetic over a constant. Every per-asset reader downstream was
|
|
116
|
+
* aggregating over the writer rather than over the work.
|
|
117
|
+
*
|
|
118
|
+
* ABSENT now means an EMPTY list, not a fabricated one. An empty list is an
|
|
119
|
+
* honest absence — the same answer `activation_receipt_producer.ts` has
|
|
120
|
+
* always given — and it is what makes the field's variation, when a caller
|
|
121
|
+
* does supply ids, evidence about the run.
|
|
122
|
+
*
|
|
123
|
+
* Ids only, never bodies. Bounded to <= 32, mirroring the contract.
|
|
124
|
+
*/
|
|
125
|
+
rules_applied?: string[] | null | undefined;
|
|
107
126
|
/**
|
|
108
127
|
* What this dispatch was supposed to produce. Drives the anti-forgery gate
|
|
109
128
|
* in `envelopeOutcome`. Absent or `unknown` disables the gate for this
|
|
@@ -253,6 +272,7 @@ const ORIGIN_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
|
253
272
|
*/
|
|
254
273
|
const MAX_APPLIED_IDS = 32;
|
|
255
274
|
|
|
275
|
+
|
|
256
276
|
/**
|
|
257
277
|
* What THIS producer's lines carry. `ids-only` and not `counts-only` because
|
|
258
278
|
* every line carries `rules_applied`, and some carry `skills_applied`.
|
|
@@ -626,7 +646,9 @@ export function buildOrchestrationLine(input: RecordInput): BuiltLine {
|
|
|
626
646
|
risk_class: risk,
|
|
627
647
|
memory: { asks: 0, hits: 0 },
|
|
628
648
|
verify: { claims: 0, first_try_passes: 0 },
|
|
629
|
-
rules_applied
|
|
649
|
+
// COMPUTED, never a producer constant. See `rules_applied` on the input
|
|
650
|
+
// interface for the measurement that retired the constant.
|
|
651
|
+
rules_applied: appliedIds(input.rules_applied),
|
|
630
652
|
// MANDATORY, never optional and never derived by the reader. This line
|
|
631
653
|
// carries `rules_applied` and may carry `skills_applied`, both of which
|
|
632
654
|
// are stable artefact ids, so the class it declares is `ids-only`
|
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* when it is stale. A second copy of any of these values is the drift seam
|
|
6
6
|
* Risk 6 warns about.
|
|
7
7
|
*/
|
|
8
|
+
import * as fs from 'node:fs';
|
|
8
9
|
import * as path from 'node:path';
|
|
9
10
|
|
|
11
|
+
import { safe_stem } from './session_register.js';
|
|
12
|
+
|
|
10
13
|
/** Where `session:recycle` writes the pending envelope (gitignored runtime state). */
|
|
11
14
|
export const RECYCLE_ENVELOPE_REL = path.join('agents', 'runtime', 'state', 'recycle-envelope.json');
|
|
12
15
|
|
|
@@ -33,3 +36,190 @@ export const RECYCLE_MAX_AGE_HOURS = 48;
|
|
|
33
36
|
* for the wrapper block and a co-pending handoff.
|
|
34
37
|
*/
|
|
35
38
|
export const RECYCLE_ENVELOPE_MAX_BYTES = 6144;
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------
|
|
41
|
+
// Per-SESSION keying (road-to-one-continuity-record Phase 2.1)
|
|
42
|
+
// ---------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/** State directory the two constants above live in. */
|
|
45
|
+
const _STATE_REL = path.join('agents', 'runtime', 'state');
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Per-SESSION record path. The two constants above name **one file per
|
|
49
|
+
* workspace**, and the session register in the same tree models concurrent
|
|
50
|
+
* sessions per checkout — so the continuity artifact and the identity model
|
|
51
|
+
* disagreed, and the artifact lost. Two sessions in one checkout overwrite
|
|
52
|
+
* each other's record and each resumes from whichever wrote last.
|
|
53
|
+
*
|
|
54
|
+
* This is the same defect `roadmap_claim_rel` (`session_register_hook.ts:105`)
|
|
55
|
+
* was repaired for, and it is deliberately repaired the same way rather than a
|
|
56
|
+
* better way: one shape for "state keyed by session", so a reader who has seen
|
|
57
|
+
* one recognises the other. `safe_stem` is imported from the register rather
|
|
58
|
+
* than re-implemented — a second sanitiser is a second traversal guard to keep
|
|
59
|
+
* correct.
|
|
60
|
+
*
|
|
61
|
+
* **No `latest` index, by decision.** A pointer file naming the newest record
|
|
62
|
+
* would restore the shared write target one layer up and re-create the same
|
|
63
|
+
* race with an extra indirection. Resolution is by register identity,
|
|
64
|
+
* workspace and branch — never by recency.
|
|
65
|
+
*
|
|
66
|
+
* When no session id is resolvable the legacy shared path is returned, so an
|
|
67
|
+
* older record keeps working and the fallback degrades to the previous
|
|
68
|
+
* behaviour instead of losing the record. The reader's ambiguity rule
|
|
69
|
+
* (`resolveContinuityRecord`) is what keeps that fallback from becoming a
|
|
70
|
+
* cross-session read.
|
|
71
|
+
*/
|
|
72
|
+
export function recycle_envelope_rel(session_id: string | null | undefined): string {
|
|
73
|
+
return _sessionKeyed(session_id, RECYCLE_ENVELOPE_REL, 'recycle-envelope', '.json');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Consumed sibling of {@link recycle_envelope_rel}, same keying. */
|
|
77
|
+
export function recycle_consumed_rel(session_id: string | null | undefined): string {
|
|
78
|
+
return _sessionKeyed(session_id, RECYCLE_CONSUMED_REL, 'recycle-envelope', '.consumed.json');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Shared builder. Containment is asserted locally rather than trusted from
|
|
83
|
+
* `safe_stem`: a guarantee living in another module's implementation is one
|
|
84
|
+
* refactor away from being untrue here, and the failure mode of being wrong is
|
|
85
|
+
* a write outside the state directory.
|
|
86
|
+
*/
|
|
87
|
+
function _sessionKeyed(
|
|
88
|
+
session_id: string | null | undefined,
|
|
89
|
+
legacy: string,
|
|
90
|
+
stem: string,
|
|
91
|
+
suffix: string,
|
|
92
|
+
): string {
|
|
93
|
+
const id = String(session_id ?? '').trim();
|
|
94
|
+
if (id === '') {
|
|
95
|
+
return legacy;
|
|
96
|
+
}
|
|
97
|
+
const rel = path.join(_STATE_REL, `${stem}-${safe_stem(id)}${suffix}`);
|
|
98
|
+
const norm = path.normalize(rel);
|
|
99
|
+
if (!norm.startsWith(`${_STATE_REL}${path.sep}`) || norm.includes('..')) {
|
|
100
|
+
return legacy;
|
|
101
|
+
}
|
|
102
|
+
return norm;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Every session-keyed record present in a workspace, plus the legacy file. */
|
|
106
|
+
export function listContinuityRecords(workspace_root: string): string[] {
|
|
107
|
+
const dir = path.join(workspace_root, _STATE_REL);
|
|
108
|
+
let names: string[];
|
|
109
|
+
try {
|
|
110
|
+
names = fs.readdirSync(dir);
|
|
111
|
+
} catch {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
return names
|
|
115
|
+
.filter(
|
|
116
|
+
(n) =>
|
|
117
|
+
n.startsWith('recycle-envelope') &&
|
|
118
|
+
n.endsWith('.json') &&
|
|
119
|
+
!n.endsWith('.consumed.json'),
|
|
120
|
+
)
|
|
121
|
+
.sort()
|
|
122
|
+
.map((n) => path.join(dir, n));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** What {@link resolveContinuityRecord} decided, and why — the reason is emitted, never dropped. */
|
|
126
|
+
export interface ContinuityResolution {
|
|
127
|
+
/** Absolute path to read, or `null` when the reader must start clean. */
|
|
128
|
+
file: string | null;
|
|
129
|
+
/** One line, always present. On `file === null` this is what the reader says out loud. */
|
|
130
|
+
reason: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Which record THIS session may read.
|
|
135
|
+
*
|
|
136
|
+
* Three cases, and the third is the one the per-session key exists for:
|
|
137
|
+
*
|
|
138
|
+
* 1. **Own id resolves.** Read the record keyed to it, and nothing else. A peer
|
|
139
|
+
* session's record is not a candidate at any point, so "neither observes the
|
|
140
|
+
* other's" holds by construction rather than by a comparison that could be
|
|
141
|
+
* skipped.
|
|
142
|
+
* 2. **No id, one candidate.** Read it. A single record in a checkout with no
|
|
143
|
+
* resolvable identity is the pre-key situation and is unchanged.
|
|
144
|
+
* 3. **No id, several candidates.** START CLEAN and say so. Picking the newest
|
|
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.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveContinuityRecord(
|
|
149
|
+
workspace_root: string,
|
|
150
|
+
session_id: string | null | undefined,
|
|
151
|
+
): ContinuityResolution {
|
|
152
|
+
const id = String(session_id ?? '').trim();
|
|
153
|
+
if (id !== '') {
|
|
154
|
+
const own = path.join(workspace_root, recycle_envelope_rel(id));
|
|
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);
|
|
160
|
+
if (candidates.length === 0) {
|
|
161
|
+
return { file: null, reason: 'no continuity record present' };
|
|
162
|
+
}
|
|
163
|
+
if (candidates.length === 1) {
|
|
164
|
+
return { file: candidates[0] as string, reason: 'one continuity record, no session id to key on' };
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
file: null,
|
|
168
|
+
reason:
|
|
169
|
+
`starting clean: ${candidates.length} continuity records in this workspace and no session id ` +
|
|
170
|
+
'to tell them apart — resuming from the newest would resume a peer session, not this one',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Is there any trace in this workspace of the session a record names as its
|
|
176
|
+
* predecessor?
|
|
177
|
+
*
|
|
178
|
+
* The normal case resolves through the consumed sibling: when this session
|
|
179
|
+
* started, its reader consumed the predecessor's record and moved it to this
|
|
180
|
+
* session's consumed path, so that file carries `session_id === predecessor`.
|
|
181
|
+
* A live record under the predecessor's own key also counts — that is the
|
|
182
|
+
* shape when the chain has not been walked yet.
|
|
183
|
+
*
|
|
184
|
+
* **A `false` here is a refusal, not a repair.** The reader discards the record
|
|
185
|
+
* and says why. The alternative — injecting a record whose stated lineage
|
|
186
|
+
* cannot be corroborated — is the guess this check exists to prevent, and it is
|
|
187
|
+
* the expensive direction: a refused resume costs one session's context, a
|
|
188
|
+
* wrong resume costs a session working from a stranger's state without knowing
|
|
189
|
+
* it.
|
|
190
|
+
*
|
|
191
|
+
* **Known false-refusal, stated rather than hidden:** the janitor prunes old
|
|
192
|
+
* state, so a long-idle chain can lose its consumed sibling and be refused
|
|
193
|
+
* although it was genuine. That is the same cost as the record having expired,
|
|
194
|
+
* which at {@link RECYCLE_MAX_AGE_HOURS} it very likely has.
|
|
195
|
+
*/
|
|
196
|
+
export function predecessorTracePresent(workspace_root: string, predecessor: string): boolean {
|
|
197
|
+
const id = String(predecessor ?? '').trim();
|
|
198
|
+
if (id === '' || id === 'none') {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
if (fs.existsSync(path.join(workspace_root, recycle_envelope_rel(id)))) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
const dir = path.join(workspace_root, _STATE_REL);
|
|
205
|
+
let names: string[];
|
|
206
|
+
try {
|
|
207
|
+
names = fs.readdirSync(dir);
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
for (const n of names) {
|
|
212
|
+
if (!n.startsWith('recycle-envelope') || !n.endsWith('.json')) continue;
|
|
213
|
+
try {
|
|
214
|
+
const raw = JSON.parse(fs.readFileSync(path.join(dir, n), 'utf-8')) as {
|
|
215
|
+
session_id?: unknown;
|
|
216
|
+
};
|
|
217
|
+
if (String(raw.session_id ?? '').trim() === id) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
// unreadable file proves nothing in either direction
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
}
|