@event4u/agent-config 14.6.0 → 14.8.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +255 -145
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +264 -10
- package/dist/agent-src/commands/analyze.md +1 -1
- package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
- package/dist/agent-src/commands/evals/roadmap.json +17 -1
- package/dist/agent-src/commands/pr/create.md +14 -2
- package/dist/agent-src/commands/pr/merge.md +379 -0
- package/dist/agent-src/commands/roadmap/process-full.md +272 -32
- package/dist/agent-src/commands/roadmap.md +1 -1
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +95 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +289 -106
- package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
- package/dist/agent-src/rules/context-hygiene.md +2 -0
- package/dist/agent-src/rules/decision-revisit-gate.md +67 -32
- package/dist/agent-src/rules/fix-what-you-see.md +108 -0
- package/dist/agent-src/rules/no-roadmap-references.md +0 -5
- package/dist/agent-src/rules/recurring-criticism.md +110 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +20 -1
- package/dist/agent-src/rules/rule-type-governance.md +0 -2
- package/dist/agent-src/rules/self-repair-loop.md +17 -0
- package/dist/agent-src/rules/skill-quality.md +0 -2
- package/dist/agent-src/rules/source-confidentiality.md +37 -7
- package/dist/agent-src/scripts/archival_sweep.ts +117 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +26 -7
- package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
- package/dist/agent-src/scripts/gate_budget.ts +282 -0
- package/dist/agent-src/scripts/gate_execute.ts +72 -31
- package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
- package/dist/agent-src/scripts/update_roadmap_progress.ts +296 -53
- package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
- package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
- package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
- package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
- package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
- package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
- package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
- package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
- package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
- package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
- package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
- package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
- package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
- package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
- package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
- package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
- package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
- package/dist/agent-src/skills/security/SKILL.md +8 -0
- package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
- package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
- package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
- package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +65 -4
- package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
- package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
- package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
- package/dist/cli/registry.js +2 -1
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
- package/dist/cli-delegate/{chunk-P6LQDIWK.js → chunk-7PRE6ZV2.js} +338 -0
- package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
- package/dist/cli-delegate/chunk-E4XIZDIB.js +402 -0
- package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-EEJUMCDZ.js} +16 -16
- package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
- package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-H4VIVD4C.js} +385 -284
- package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
- package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-LPPWO5CE.js} +12 -8
- package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
- package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-N7CDDGI3.js} +4 -23
- package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-PMEKGDED.js} +74 -167
- package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
- package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
- package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
- package/dist/cli-delegate/cmd_conformance.js +466 -716
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +10 -10
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +10 -9
- package/dist/cli-delegate/cmd_handoff.js +10 -9
- package/dist/cli-delegate/cmd_memory_get.js +2 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +1 -1
- package/dist/cli-delegate/cmd_preflight.js +5 -5
- package/dist/cli-delegate/cmd_prune.js +11 -10
- package/dist/cli-delegate/cmd_route_audit.js +6 -5
- package/dist/cli-delegate/cmd_route_explain.js +3 -3
- package/dist/cli-delegate/cmd_session_recycle.js +840 -30
- package/dist/cli-delegate/cmd_settings_get.js +3 -3
- package/dist/cli-delegate/cmd_settings_migrate.js +10 -9
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +11 -10
- package/dist/cli-delegate/cmd_uninstall.js +13 -11
- package/dist/cli-delegate/cmd_update.js +4 -4
- package/dist/cli-delegate/cmd_upgrade.js +4 -4
- package/dist/cli-delegate/cmd_validate.js +11 -10
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +111 -37
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +5 -5
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +15 -12
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +11 -8
- package/dist/hooks/dispatch.js +3165 -2192
- package/dist/install/claudePathsPlan.js +187 -0
- package/dist/install/claudePathsPlan.js.map +1 -0
- package/dist/install/globalRuleLayers.js +169 -0
- package/dist/install/globalRuleLayers.js.map +1 -0
- package/dist/install/hostLayerFingerprint.js +136 -0
- package/dist/install/hostLayerFingerprint.js.map +1 -0
- package/dist/install/install.mjs +553 -425
- package/dist/install/partitionEligibility.js +432 -0
- package/dist/install/partitionEligibility.js.map +1 -0
- package/dist/install/ruleLayerPartition.js +157 -0
- package/dist/install/ruleLayerPartition.js.map +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1863 -1234
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_src.js +41 -0
- package/dist/scripts/_lib/agent_src.js.map +1 -1
- package/dist/scripts/_lib/install_layout.js +83 -0
- package/dist/scripts/_lib/install_layout.js.map +1 -0
- package/dist/scripts/_lib/installed_lock.js +444 -0
- package/dist/scripts/_lib/installed_lock.js.map +1 -0
- package/dist/scripts/_lib/prune_empty_dirs.js +85 -0
- package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
- package/dist/scripts/condense.js +63 -251
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +4 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/contracts/settings-classes.md +12 -6
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
- package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
- package/docs/guidelines/agent-infra/false-green.md +47 -1
- package/docs/guidelines/agent-infra/memory-access.md +17 -0
- package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/archival_sweep.ts +117 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +26 -7
- package/src/agent-src/scripts/dashboard_mode.ts +212 -0
- package/src/agent-src/scripts/gate_budget.ts +282 -0
- package/src/agent-src/scripts/gate_execute.ts +72 -31
- package/src/agent-src/scripts/roadmap_gates.ts +74 -3
- package/src/agent-src/scripts/update_roadmap_progress.ts +296 -53
- package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
- package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
- package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
- package/src/config/agent-settings.template.yml +27 -2
- package/src/config/ci-local-parity.yml +39 -0
- package/src/config/estate-count-budget.json +612 -3
- package/src/config/evaluator-budgets.json +3 -3
- package/src/config/gate-coverage.yml +220 -12
- package/src/config/gate-violation-baselines.json +73 -63
- package/src/config/gitignore-block.txt +15 -2
- package/src/config/hook-latency-budget.json +5 -0
- package/src/config/rule-activation-census.json +14 -6
- package/src/config/rule-stub-ceilings.json +34 -7
- package/src/install/claudePathsPlan.ts +196 -0
- package/src/install/globalRuleLayers.ts +197 -0
- package/src/install/hostLayerFingerprint.ts +145 -0
- package/src/install/partitionEligibility.ts +502 -0
- package/src/install/ruleLayerPartition.ts +212 -0
- package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
- package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
- package/src/scripts/_dispatch.bash +17 -1
- package/src/scripts/_lib/ac_heading.ts +30 -0
- package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
- package/src/scripts/_lib/adr_frontmatter.ts +459 -0
- package/src/scripts/_lib/agent_src.ts +42 -0
- package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
- package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
- package/src/scripts/_lib/envelope_grounding.ts +49 -9
- package/src/scripts/_lib/installed_lock.ts +50 -5
- package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
- package/src/scripts/_lib/md_section.ts +129 -0
- package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
- package/src/scripts/_lib/scope_guard.sh +26 -3
- package/src/scripts/_lib/self_repair.ts +19 -0
- package/src/scripts/_lib/self_repair_class_b.ts +171 -0
- package/src/scripts/adr/evidence_census.ts +899 -0
- package/src/scripts/adr/regenerate_index.ts +12 -32
- package/src/scripts/adr_cite_check.ts +540 -39
- package/src/scripts/ai_council/cli_help.ts +55 -0
- package/src/scripts/audit_adr_coverage.ts +103 -17
- package/src/scripts/before_complete_hook.ts +153 -28
- package/src/scripts/bench_ab_v2_complexity.ts +27 -7
- package/src/scripts/bench_hook_latency.ts +204 -0
- package/src/scripts/build_archive_index.ts +34 -0
- package/src/scripts/check_adr_frontmatter.ts +574 -39
- package/src/scripts/check_hook_bundle_content.ts +219 -0
- package/src/scripts/check_new_adr_evidence.ts +529 -0
- package/src/scripts/check_references.ts +78 -0
- package/src/scripts/check_release_published.ts +32 -11
- package/src/scripts/check_rule_layer_partition.ts +531 -0
- package/src/scripts/check_single_delivery.ts +194 -17
- package/src/scripts/check_standing_rule_delivery.ts +8 -2
- package/src/scripts/ci_settle.ts +170 -0
- package/src/scripts/condense.ts +79 -259
- package/src/scripts/council_cli.ts +3 -3
- package/src/scripts/dispatch_r2_reviewer.ts +6 -14
- package/src/scripts/external_sources_denylist.json +4 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +77 -14
- package/src/scripts/hooks/block_no_verify.ts +61 -10
- package/src/scripts/hooks/block_unauthorized_git.ts +21 -1
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/dispatch_hook.ts +154 -187
- package/src/scripts/hooks/dispatch_issues.ts +38 -19
- package/src/scripts/hooks/fallback_yaml.ts +66 -0
- package/src/scripts/hooks/host_semantics.ts +21 -0
- package/src/scripts/hooks/injection_budget.ts +13 -3
- package/src/scripts/hooks/py_json_dumps.ts +103 -0
- package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
- package/src/scripts/hooks/run_continuation_hook.ts +8 -5
- package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
- package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
- package/src/scripts/hooks/state_io.ts +870 -21
- package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
- package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
- package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
- package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
- package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
- package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
- package/src/scripts/injection_scan_hook.ts +137 -5
- package/src/scripts/install.ts +16 -11
- package/src/scripts/language_mirror_hook.ts +561 -25
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_hook_manifest.ts +49 -0
- package/src/scripts/lint_plan_risk_register.ts +4 -3
- package/src/scripts/lint_provenance_vocabulary.ts +472 -4
- package/src/scripts/lint_skill_router_head.ts +4 -4
- package/src/scripts/mcp_server/tools.ts +2 -2
- package/src/scripts/new_roadmap.ts +136 -0
- package/src/scripts/pack_mcp_content.ts +20 -5
- package/src/scripts/release.ts +37 -38
- package/src/scripts/report_layer_overlap.ts +6 -0
- package/src/scripts/routing_doctor.ts +5 -1
- package/src/scripts/self_repair_hook.ts +37 -5
- package/src/scripts/session_register_hook.ts +11 -0
- package/src/scripts/skill_usage_collect.ts +11 -1
- package/src/scripts/skill_usage_report.ts +278 -8
- package/src/scripts/sync_pr_branch.ts +170 -20
- package/src/server/schemas/settings.ts +8 -0
- package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
- package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
- package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
- package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
- package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-MMSGVZKV.js} +3 -3
|
@@ -4,6 +4,25 @@ Templates for roadmap files stored in `agents/roadmaps/` or `{module_root}/{Modu
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## Start from the skeleton, not from this page
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
./scripts-run src/scripts/new_roadmap <slug> # lightweight
|
|
11
|
+
./scripts-run src/scripts/new_roadmap <slug> --structural
|
|
12
|
+
./scripts-run src/scripts/new_roadmap <slug> --stdout # print, do not write
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
It emits a file that passes every roadmap gate **unedited** — the complexity
|
|
16
|
+
enum, the acceptance heading in the exact form the extractor matches, the
|
|
17
|
+
risk-review marker, a legal risk type, and an anchor that resolves. Those four
|
|
18
|
+
conventions live in four different gates, and each one costs a full gate round
|
|
19
|
+
to discover by failing it. Measured 2026-08-20 across 88 roadmaps: **six
|
|
20
|
+
different invented `complexity:` values** were in the tree, and **10 of 22**
|
|
21
|
+
roadmaps wrote an acceptance heading the extractor cannot see.
|
|
22
|
+
|
|
23
|
+
The rules below still govern the content. The skeleton only removes the part
|
|
24
|
+
that was never a judgement call.
|
|
25
|
+
|
|
7
26
|
## Rules for Roadmaps
|
|
8
27
|
|
|
9
28
|
1. **Be precise and concise.** Lightweight default: ≤ 600 lines (see rule 15). Only a declared `complexity: structural` roadmap may exceed that, capped at 1000 lines — if larger, split into multiple files.
|
|
@@ -89,8 +108,8 @@ Templates for roadmap files stored in `agents/roadmaps/` or `{module_root}/{Modu
|
|
|
89
108
|
- `interactive` — every gate fires as authored by its owning rule.
|
|
90
109
|
**Not what an absent field means.** When the field is absent the mode is
|
|
91
110
|
DERIVED from the invocation form (`roadmap-process-loop § 3a`):
|
|
92
|
-
`process-full`
|
|
93
|
-
|
|
111
|
+
`process-full` derives `autonomous`; `/roadmap:next` and `process-phase`
|
|
112
|
+
derive `phase-checkpoints`; `process-step` runs without a contract. The
|
|
94
113
|
contract screen is still shown once and still carries "run interactive
|
|
95
114
|
instead", so declaring nothing costs a keystroke, never control.
|
|
96
115
|
- `phase-checkpoints` — the run halts at each phase boundary with a
|
|
@@ -110,6 +129,24 @@ Templates for roadmap files stored in `agents/roadmaps/` or `{module_root}/{Modu
|
|
|
110
129
|
lint. Follow-up roadmaps (rule 17) inherit the parent's mode as the
|
|
111
130
|
*suggested* option during creation but the question is always asked
|
|
112
131
|
again.
|
|
132
|
+
|
|
133
|
+
**Companion field, same block — `depends:`.** Optional, and only
|
|
134
|
+
meaningful for a set run:
|
|
135
|
+
|
|
136
|
+
```yaml
|
|
137
|
+
depends: [road-to-other-thing] # slugs, not paths
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
A declared edge is authoritative and is never overridden by the
|
|
141
|
+
file-overlap heuristic the set contract also computes
|
|
142
|
+
([`roadmap-process-loop § 3d`](../contexts/execution/roadmap-process-loop.md)).
|
|
143
|
+
The union of declared and inferred edges feeds exactly two decisions —
|
|
144
|
+
the order members run in, and whether two members may share a lane —
|
|
145
|
+
and nothing else. Declaring an edge that is not real costs serial
|
|
146
|
+
execution; missing a real one costs a collision, so the heuristic
|
|
147
|
+
resolves toward serial and a declaration is the cheap way to be right.
|
|
148
|
+
Absent means no declared dependency, never “independent”: the overlap
|
|
149
|
+
heuristic still runs.
|
|
113
150
|
19. **Source-derived roadmaps carry a gap-table + provenance; internally
|
|
114
151
|
originated roadmaps carry neither.** This rule fires **only** when a
|
|
115
152
|
roadmap originates from an external input (a suggestion, a competitive
|
|
@@ -203,16 +240,40 @@ Templates for roadmap files stored in `agents/roadmaps/` or `{module_root}/{Modu
|
|
|
203
240
|
| `0` | auto-run — deterministic, free, reversible | the agent runs `Run:`; the output IS the unblock |
|
|
204
241
|
| `1` | budget-preauthorized — billable but reversible | the agent runs `Run:` under a standing budget and logs a receipt |
|
|
205
242
|
| `2` | consent-once — a real preference call, reversible | one line with the recommendation and a default |
|
|
206
|
-
| `3` | human-only —
|
|
243
|
+
| `3` | human-only — the human IS the content: externally impossible for the agent, or an explicitly excluded action | a person, because nobody else can |
|
|
244
|
+
|
|
245
|
+
**CAPABILITY BEFORE ROLE (ADR-237).** `3` means *a human is the
|
|
246
|
+
content of this gate*, never *a human usually does this*. Before
|
|
247
|
+
authoring `3`, ask one question: **can the agent execute this at all**
|
|
248
|
+
— through the filesystem, git, `gh`, an API, a CLI, a tool, a model, a
|
|
249
|
+
council? If yes, it is **not** class 3, whoever conventionally
|
|
250
|
+
performs it. A branch to create, a push, a PR to open, a repository or
|
|
251
|
+
branch setting the agent can change, a workflow to start, CI to
|
|
252
|
+
re-run, a merge base to update, conflicts, failing tests, local
|
|
253
|
+
configuration, a paid call inside the run's budget — all of these are
|
|
254
|
+
**work**, and authoring `3` on any of them is a defect a
|
|
255
|
+
`process-full` run repairs rather than obeys.
|
|
256
|
+
|
|
257
|
+
What legitimately remains `3`: a credential that does not exist and
|
|
258
|
+
the agent cannot create · a purchase beyond the delegated budget ·
|
|
259
|
+
physical hardware access · another person or organisation must act · a
|
|
260
|
+
wait that is factually mandatory and cannot be simulated or verified ·
|
|
261
|
+
an action on the Hard Floor's EXCLUDED list (production-trunk merge,
|
|
262
|
+
deploy, prod data / secrets / IAM / DNS, an irreversible external
|
|
263
|
+
action).
|
|
207
264
|
|
|
208
265
|
Three properties are load-bearing. **`Class:` is optional and its
|
|
209
266
|
absence means 3**, so nothing becomes executable because an author
|
|
210
|
-
forgot a field
|
|
267
|
+
forgot a field — but an absent class is a *default*, not a finding,
|
|
268
|
+
and the capability question above still decides whether the step is
|
|
269
|
+
really blocked. **A class-0 or class-1 entry MUST carry `Run:`** —
|
|
211
270
|
`lint_roadmap_blockers` fails otherwise, because a gate that claims
|
|
212
271
|
to be runnable without naming the command reads as actionable and is
|
|
213
272
|
not. And **class is authored, never inferred**: promoting a gate is
|
|
214
273
|
a reviewed edit, which is what keeps a Hard-Floor gate
|
|
215
274
|
(`non-destructive-by-default`) out of reach of a runtime judgment.
|
|
275
|
+
Repairing a `3` that fails the capability test is such an edit — it is
|
|
276
|
+
recorded at the blocker with the reason, not applied silently.
|
|
216
277
|
|
|
217
278
|
Only the leading token of `Class:` is read, so
|
|
218
279
|
`- **Class:** 1 — budget-preauthorized` is valid; the taxonomy name
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The detached Class-A sender (road-to-org-telemetry Phase 2, step 2.1).
|
|
4
|
+
*
|
|
5
|
+
* Spawned by `spawn_detached_sender` and never by a human. It is the ONLY
|
|
6
|
+
* code in this tree that makes an outbound telemetry call, which is why it is
|
|
7
|
+
* one small file with no imports beyond node builtins: the egress surface
|
|
8
|
+
* should be readable end to end in one screen by anyone auditing what leaves
|
|
9
|
+
* a machine.
|
|
10
|
+
*
|
|
11
|
+
* PLAIN JS, DELIBERATELY. Its siblings here are TypeScript resolved by `tsx`
|
|
12
|
+
* at import time. A detached child inherits no such resolution, so a `.ts`
|
|
13
|
+
* sender would depend on finding a `tsx` binary in a consumer install at
|
|
14
|
+
* session teardown. `process.execPath` — plain node — exists by construction.
|
|
15
|
+
* The cost of that choice is that this file cannot import the sibling
|
|
16
|
+
* modules, so the drain algorithm lives HERE and nowhere else; the TypeScript
|
|
17
|
+
* side owns enqueue and spawn only, and there is no second copy to drift.
|
|
18
|
+
*
|
|
19
|
+
* CLAIM-BY-RENAME, NOT READ-THEN-TRUNCATE. `rename` is atomic within a
|
|
20
|
+
* filesystem, so a session that appends a record while this process is
|
|
21
|
+
* sending writes to a fresh spool rather than into bytes about to be deleted.
|
|
22
|
+
* Read-then-truncate loses exactly those records, silently, and only under
|
|
23
|
+
* concurrency — the failure shape that never shows up in a single-session
|
|
24
|
+
* test.
|
|
25
|
+
*
|
|
26
|
+
* FAILURE KEEPS THE DATA. A non-2xx, a timeout, a DNS failure, a thrown
|
|
27
|
+
* anything: the claimed lines go back in front of the spool and the next
|
|
28
|
+
* session's flush tries again. "Local retention for the next flush" is the
|
|
29
|
+
* step's own wording and this is it. Exit code is always 0 — nothing reads it
|
|
30
|
+
* (`stdio: 'ignore'`), and a non-zero from a detached child is a log line
|
|
31
|
+
* nobody will ever see.
|
|
32
|
+
*/
|
|
33
|
+
import * as fs from 'node:fs';
|
|
34
|
+
|
|
35
|
+
const DEFAULT_TIMEOUT_MS = 1000;
|
|
36
|
+
|
|
37
|
+
function parse_args(argv) {
|
|
38
|
+
const out = { spool: '', endpoint: '', timeout_ms: DEFAULT_TIMEOUT_MS };
|
|
39
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
40
|
+
const a = argv[i];
|
|
41
|
+
if (a === '--spool') {
|
|
42
|
+
out.spool = argv[i + 1] ?? '';
|
|
43
|
+
i += 1;
|
|
44
|
+
} else if (a === '--endpoint') {
|
|
45
|
+
out.endpoint = argv[i + 1] ?? '';
|
|
46
|
+
i += 1;
|
|
47
|
+
} else if (a === '--timeout') {
|
|
48
|
+
const n = Number.parseInt(argv[i + 1] ?? '', 10);
|
|
49
|
+
if (Number.isFinite(n) && n > 0) out.timeout_ms = n;
|
|
50
|
+
i += 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Take the spool by rename. Returns the claim path, or `null` when there is
|
|
58
|
+
* nothing to send — including the race where a sibling sender claimed it
|
|
59
|
+
* first, which is a no-op rather than an error.
|
|
60
|
+
*/
|
|
61
|
+
export function claim_spool(spool_path) {
|
|
62
|
+
let size = 0;
|
|
63
|
+
try {
|
|
64
|
+
size = fs.statSync(spool_path).size;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (size === 0) return null;
|
|
69
|
+
const claim = `${spool_path}.sending.${process.pid}.${Date.now()}`;
|
|
70
|
+
try {
|
|
71
|
+
fs.renameSync(spool_path, claim);
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return claim;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Put the claimed lines back in FRONT of whatever arrived meanwhile. */
|
|
79
|
+
export function restore_claim(claim_path, spool_path) {
|
|
80
|
+
try {
|
|
81
|
+
const claimed = fs.readFileSync(claim_path, 'utf-8');
|
|
82
|
+
let current = '';
|
|
83
|
+
try {
|
|
84
|
+
current = fs.readFileSync(spool_path, 'utf-8');
|
|
85
|
+
} catch {
|
|
86
|
+
current = '';
|
|
87
|
+
}
|
|
88
|
+
fs.writeFileSync(spool_path, claimed + current, { encoding: 'utf-8' });
|
|
89
|
+
fs.unlinkSync(claim_path);
|
|
90
|
+
return true;
|
|
91
|
+
} catch {
|
|
92
|
+
// The claim file survives under its own name; the next sender's
|
|
93
|
+
// `claim_spool` will not see it, so it is recoverable by hand rather
|
|
94
|
+
// than lost. Better than a partial write over the live spool.
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* POST the batch as NDJSON. Returns true only on a 2xx.
|
|
101
|
+
*
|
|
102
|
+
* The body is the spooled bytes verbatim — every line is a Class-A record
|
|
103
|
+
* whose fields are all structural (see `remote.ts`), so there is no
|
|
104
|
+
* serialisation step here that could introduce a field the record type does
|
|
105
|
+
* not have.
|
|
106
|
+
*/
|
|
107
|
+
export async function post_batch(endpoint, body, timeout_ms) {
|
|
108
|
+
try {
|
|
109
|
+
const res = await fetch(endpoint, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: { 'content-type': 'application/x-ndjson' },
|
|
112
|
+
body,
|
|
113
|
+
signal: AbortSignal.timeout(timeout_ms),
|
|
114
|
+
});
|
|
115
|
+
return res.ok === true;
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function run(args) {
|
|
122
|
+
if (!args.spool || !args.endpoint) return 'no-op';
|
|
123
|
+
const claim = claim_spool(args.spool);
|
|
124
|
+
if (claim === null) return 'no-op';
|
|
125
|
+
|
|
126
|
+
let body = '';
|
|
127
|
+
try {
|
|
128
|
+
body = fs.readFileSync(claim, 'utf-8');
|
|
129
|
+
} catch {
|
|
130
|
+
return 'read-failed';
|
|
131
|
+
}
|
|
132
|
+
if (body.length === 0) {
|
|
133
|
+
try {
|
|
134
|
+
fs.unlinkSync(claim);
|
|
135
|
+
} catch {
|
|
136
|
+
/* nothing to clean */
|
|
137
|
+
}
|
|
138
|
+
return 'no-op';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const ok = await post_batch(args.endpoint, body, args.timeout_ms);
|
|
142
|
+
if (ok) {
|
|
143
|
+
try {
|
|
144
|
+
fs.unlinkSync(claim);
|
|
145
|
+
} catch {
|
|
146
|
+
/* sent; a stale claim is harmless */
|
|
147
|
+
}
|
|
148
|
+
return 'sent';
|
|
149
|
+
}
|
|
150
|
+
restore_claim(claim, args.spool);
|
|
151
|
+
return 'retained';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const _entry = process.argv[1] ?? '';
|
|
155
|
+
const _invoked_directly = _entry !== '' && import.meta.url === new URL(`file://${_entry}`).href;
|
|
156
|
+
|
|
157
|
+
if (_invoked_directly) {
|
|
158
|
+
run(parse_args(process.argv.slice(2)))
|
|
159
|
+
.then(() => process.exit(0))
|
|
160
|
+
.catch(() => process.exit(0));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export { parse_args };
|
|
@@ -35,8 +35,14 @@ import { check_id_redaction, py_json_dumps_compact_sorted } from './engagement.j
|
|
|
35
35
|
|
|
36
36
|
export const CLASS_A_SCHEMA_VERSION = 1;
|
|
37
37
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Record classes. `usage` is Phase 1; `self-repair` is Phase 5 step 5.1.
|
|
40
|
+
*
|
|
41
|
+
* Both are Class A — structural fields only — and both travel the same log,
|
|
42
|
+
* spool and transport, discriminated by this field. `skill_usage_report`
|
|
43
|
+
* filters on it so a defect report can never be counted as an invocation.
|
|
44
|
+
*/
|
|
45
|
+
export const ALLOWED_RECORD_CLASSES = ['usage', 'self-repair'] as const;
|
|
40
46
|
|
|
41
47
|
/**
|
|
42
48
|
* Discipline-profile values, the knob that actually decides which rule
|
|
@@ -279,6 +285,122 @@ export function build_class_a_record(input: BuildClassAInput): ClassARecord {
|
|
|
279
285
|
};
|
|
280
286
|
}
|
|
281
287
|
|
|
288
|
+
/**
|
|
289
|
+
* One Class-A SELF-REPAIR record (Phase 5, step 5.1).
|
|
290
|
+
*
|
|
291
|
+
* The shadow of a queued defect: it says a defect of a named class occurred,
|
|
292
|
+
* on which host, at which package version, under which declared profile, for
|
|
293
|
+
* which pseudonymous user. It says nothing about WHAT happened.
|
|
294
|
+
*
|
|
295
|
+
* A SEPARATE TYPE, NOT A WIDENED `ClassARecord`, AND THAT IS THE WHOLE POINT.
|
|
296
|
+
* `DefectFinding` carries `evidence` (a quoted span of the offending text) and
|
|
297
|
+
* `suggested_surface` (a free sentence). Those are the Class-B payload and
|
|
298
|
+
* they ship only on explicit per-case approval. Adding either to a
|
|
299
|
+
* telemetry record type — even optionally, even "sanitized" — would give this
|
|
300
|
+
* type a field capable of holding project content, and the privacy property
|
|
301
|
+
* this whole surface rests on is that no such field exists. There is then no
|
|
302
|
+
* scrubber to fail. Keeping the automatic class in its own interface is how
|
|
303
|
+
* that stays checkable by reading the type instead of auditing every writer.
|
|
304
|
+
*
|
|
305
|
+
* WHAT IS DELIBERATELY ABSENT: an enumerated active-rule / active-skill list.
|
|
306
|
+
* The roadmap Context asks Class A to carry "the active rule and skill
|
|
307
|
+
* snapshot" so that "this rule was loaded in six of seven reports of this
|
|
308
|
+
* class" localizes an artefact. No producer for that exists on a hook path:
|
|
309
|
+
* `match_prompt` needs the compiled router, which nothing in the hook surface
|
|
310
|
+
* loads today and which is not established as present in a consumer install.
|
|
311
|
+
* `discipline_profile` is recorded instead — the knob that decides which rule
|
|
312
|
+
* surfaces load at all — and it is a profile-level snapshot, NOT the
|
|
313
|
+
* per-artefact attribution key. That attribution claim stays unavailable, and
|
|
314
|
+
* saying so here is cheaper than a reader inferring it from a field name.
|
|
315
|
+
*/
|
|
316
|
+
export interface ClassADefectRecord {
|
|
317
|
+
schema_version: number;
|
|
318
|
+
record_class: string;
|
|
319
|
+
ts_bucket: string;
|
|
320
|
+
/** Detector / intake class — a closed vocabulary owned by the caller. */
|
|
321
|
+
defect_class: string;
|
|
322
|
+
/** Which intake produced it — also a closed vocabulary. */
|
|
323
|
+
defect_source: string;
|
|
324
|
+
/** How many times this fingerprint has been seen. A counter, not content. */
|
|
325
|
+
occurrences: number;
|
|
326
|
+
host: string | null;
|
|
327
|
+
package_version: string | null;
|
|
328
|
+
discipline_profile: string | null;
|
|
329
|
+
org_id: string;
|
|
330
|
+
user_hash: string;
|
|
331
|
+
session_hash: string | null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export interface BuildClassADefectInput {
|
|
335
|
+
defect_class: string;
|
|
336
|
+
defect_source: string;
|
|
337
|
+
occurrences: number;
|
|
338
|
+
host: string | null;
|
|
339
|
+
org_id: string;
|
|
340
|
+
salt: string;
|
|
341
|
+
hostname: string;
|
|
342
|
+
username: string;
|
|
343
|
+
session_id: string;
|
|
344
|
+
package_version?: string | null;
|
|
345
|
+
discipline_profile?: string | null;
|
|
346
|
+
now?: Date;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Build one validated Class-A self-repair record. Pure, and throws rather
|
|
351
|
+
* than returning a partial record — same contract as its usage sibling.
|
|
352
|
+
*
|
|
353
|
+
* `defect_class` and `defect_source` go through the same id-redaction floor as
|
|
354
|
+
* every other id here, so a caller that ever passed a path, an extension or
|
|
355
|
+
* an over-long free string gets an exception instead of an egress.
|
|
356
|
+
*/
|
|
357
|
+
export function build_class_a_defect_record(
|
|
358
|
+
input: BuildClassADefectInput,
|
|
359
|
+
): ClassADefectRecord {
|
|
360
|
+
check_id_redaction('defect_class', input.defect_class);
|
|
361
|
+
check_id_redaction('defect_source', input.defect_source);
|
|
362
|
+
check_id_redaction('org_id', input.org_id);
|
|
363
|
+
|
|
364
|
+
const host = input.host ?? null;
|
|
365
|
+
if (host !== null) {
|
|
366
|
+
check_id_redaction('host', host);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const discipline_profile = input.discipline_profile ?? null;
|
|
370
|
+
if (
|
|
371
|
+
discipline_profile !== null
|
|
372
|
+
&& !(ALLOWED_DISCIPLINE_PROFILES as readonly string[]).includes(discipline_profile)
|
|
373
|
+
) {
|
|
374
|
+
throw new ClassARecordError(
|
|
375
|
+
`discipline_profile must be one of ${ALLOWED_DISCIPLINE_PROFILES.join(', ')} or null`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const package_version = input.package_version ?? null;
|
|
380
|
+
if (package_version !== null) {
|
|
381
|
+
check_id_redaction('package_version', package_version);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (!Number.isInteger(input.occurrences) || input.occurrences < 1) {
|
|
385
|
+
throw new ClassARecordError('occurrences must be a positive integer');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
schema_version: CLASS_A_SCHEMA_VERSION,
|
|
390
|
+
record_class: 'self-repair',
|
|
391
|
+
ts_bucket: hour_bucket(input.now ?? new Date()),
|
|
392
|
+
defect_class: input.defect_class,
|
|
393
|
+
defect_source: input.defect_source,
|
|
394
|
+
occurrences: input.occurrences,
|
|
395
|
+
host,
|
|
396
|
+
package_version,
|
|
397
|
+
discipline_profile,
|
|
398
|
+
org_id: input.org_id,
|
|
399
|
+
user_hash: derive_user_hash(input.salt, input.hostname, input.username),
|
|
400
|
+
session_hash: derive_session_hash(input.salt, input.session_id),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
282
404
|
// ── Retention — the growth budget for the local record log ──────────────
|
|
283
405
|
//
|
|
284
406
|
// Phase 1 shipped this file as append-only with no cap and no pruning, and
|
|
@@ -544,6 +666,20 @@ export function enforce_retention(
|
|
|
544
666
|
return { kept: kept_lines.length, dropped_by_age, dropped_by_size };
|
|
545
667
|
}
|
|
546
668
|
|
|
669
|
+
/** Append one line to one bounded JSONL file. Both targets share it. */
|
|
670
|
+
function _append_and_bound(
|
|
671
|
+
target: string,
|
|
672
|
+
payload: string,
|
|
673
|
+
policy: RetentionPolicy,
|
|
674
|
+
now: Date,
|
|
675
|
+
): void {
|
|
676
|
+
fs.mkdirSync(path.dirname(target) || '.', { recursive: true });
|
|
677
|
+
fs.appendFileSync(target, payload, { encoding: 'utf-8' });
|
|
678
|
+
if (retention_due(target, policy, now)) {
|
|
679
|
+
enforce_retention(target, policy, now);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
547
683
|
/**
|
|
548
684
|
* Append one record as a JSONL line, then enforce the retention policy.
|
|
549
685
|
*
|
|
@@ -555,17 +691,34 @@ export function enforce_retention(
|
|
|
555
691
|
* which makes the common append one extra `stat` plus one bounded read. It
|
|
556
692
|
* lives here rather than in a separate sweep because this is the only writer:
|
|
557
693
|
* a policy a caller has to remember to run is a policy that stops running.
|
|
694
|
+
*
|
|
695
|
+
* THE SPOOL IS WRITTEN HERE, IN THE SAME CALL (Phase 2, step 2.1). When
|
|
696
|
+
* `spool_path` is given, the SAME bytes are appended to the outbound spool
|
|
697
|
+
* under the SAME retention policy, and the session-end flush then only has
|
|
698
|
+
* to drain a file. The alternative — a flush that works out for itself which
|
|
699
|
+
* logged records are still unsent — needs a byte watermark into a file that
|
|
700
|
+
* `enforce_retention` rewrites in place, and a watermark over a compacting
|
|
701
|
+
* file is a silent-corruption pair: the offset survives the prune, the
|
|
702
|
+
* records it points at do not. Enqueue-at-write has no watermark to
|
|
703
|
+
* invalidate and spools each record exactly once, by the only writer.
|
|
704
|
+
*
|
|
705
|
+
* The spool inherits the log's growth budget rather than getting its own,
|
|
706
|
+
* which is what answers Phase 0's unmeasured "queue growth bound across a
|
|
707
|
+
* multi-day outage": a sink that is down for days cannot push the spool past
|
|
708
|
+
* `max_bytes`. The cost of that bound is that the oldest UNSENT records are
|
|
709
|
+
* dropped rather than kept — the same trade the log already makes, stated
|
|
710
|
+
* here rather than discovered later in a truncated file.
|
|
558
711
|
*/
|
|
559
712
|
export function append_class_a_record(
|
|
560
713
|
log_path: string,
|
|
561
|
-
record: ClassARecord,
|
|
714
|
+
record: ClassARecord | ClassADefectRecord,
|
|
562
715
|
policy: RetentionPolicy = DEFAULT_RETENTION_POLICY,
|
|
563
716
|
now: Date = new Date(),
|
|
717
|
+
spool_path: string | null = null,
|
|
564
718
|
): void {
|
|
565
719
|
const payload = `${py_json_dumps_compact_sorted(record as unknown as Record<string, unknown>)}\n`;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
enforce_retention(log_path, policy, now);
|
|
720
|
+
_append_and_bound(log_path, payload, policy, now);
|
|
721
|
+
if (spool_path !== null && spool_path !== '') {
|
|
722
|
+
_append_and_bound(spool_path, payload, policy, now);
|
|
570
723
|
}
|
|
571
724
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Class-A transport — the enqueue half (road-to-org-telemetry Phase 2, step 2.1).
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS IS NOT AN INLINE `fetch` AT SESSION END. Phase 0's second spike
|
|
5
|
+
* pre-registered "added latency at or below one second at p95" for an inline
|
|
6
|
+
* flush and measured it FAILING: a healthy sink costs 0.4 ms p95 and a
|
|
7
|
+
* refused connection 0.3 ms, but a BLACKHOLE — a socket that accepts and
|
|
8
|
+
* never answers, the shape a wedged sink actually takes — costs 1002 ms p95
|
|
9
|
+
* against a 1000 ms bar, because the timeout IS the cost. The step's own text
|
|
10
|
+
* defers to that result ("per the second spike's result"), and the result is:
|
|
11
|
+
* session end ENQUEUES ONLY, a detached sender flushes, and local retention
|
|
12
|
+
* carries what did not go out. The same spike measured that fallback at
|
|
13
|
+
* 20.5 ms p95 against the same blackhole — dominated by process spawn — and
|
|
14
|
+
* that is the number this module must not regress.
|
|
15
|
+
*
|
|
16
|
+
* SO THE SPOOL IS WRITTEN BY THE APPENDER, NOT BY THE FLUSH. A session-end
|
|
17
|
+
* flush that had to work out which records were already sent would need a
|
|
18
|
+
* byte watermark into the log, and the log is rewritten in place by
|
|
19
|
+
* `enforce_retention` — a watermark and a compacting file are a
|
|
20
|
+
* silent-corruption pair (offsets survive the prune; the records they point
|
|
21
|
+
* at do not). Enqueue-at-write has no watermark to invalidate: a record is
|
|
22
|
+
* spooled exactly once, by the only writer, in the same call that logs it.
|
|
23
|
+
*
|
|
24
|
+
* TWO PROPERTIES PHASE 0 SAID PHASE 2 MUST MEASURE, AND WHERE THEY LANDED.
|
|
25
|
+
* (1) Does a detached child survive teardown of the session process group —
|
|
26
|
+
* measured, see `agents/evidence/eval-findings/org-telemetry-p2-transport.md`.
|
|
27
|
+
* (2) The queue's growth bound across a multi-day outage — answered by
|
|
28
|
+
* construction here: the spool carries the SAME `RetentionPolicy` as the log,
|
|
29
|
+
* enforced by the same code, so a sink that is down for days cannot grow it
|
|
30
|
+
* past `max_bytes`. That bound is a decision to drop the oldest unsent
|
|
31
|
+
* records rather than to keep all of them, and it is stated rather than left
|
|
32
|
+
* for an operator to find in a truncated file.
|
|
33
|
+
*
|
|
34
|
+
* NO SINK EXISTS YET. `sink-choice` is transferred (a private repository is
|
|
35
|
+
* the council's preferred architecture, but creating it is an org-admin
|
|
36
|
+
* action outside this repository), so nothing in this tree can name a real
|
|
37
|
+
* endpoint. What ships here is the transport an org's own endpoint value
|
|
38
|
+
* activates; with no endpoint the settings never resolve `active` and not a
|
|
39
|
+
* byte moves.
|
|
40
|
+
*/
|
|
41
|
+
import { spawn } from 'node:child_process';
|
|
42
|
+
import * as fs from 'node:fs';
|
|
43
|
+
import * as path from 'node:path';
|
|
44
|
+
|
|
45
|
+
import {
|
|
46
|
+
DEFAULT_RETENTION_POLICY,
|
|
47
|
+
enforce_retention,
|
|
48
|
+
retention_due,
|
|
49
|
+
type RetentionPolicy,
|
|
50
|
+
} from './remote.js';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The outbound-flush timeout, in ms.
|
|
54
|
+
*
|
|
55
|
+
* The step says "at or below one second"; this is the bar itself rather than
|
|
56
|
+
* a value under it, because the spike measured the timeout as the dominant
|
|
57
|
+
* cost only for an INLINE flush. In the detached child the wait costs the
|
|
58
|
+
* session nothing, so shortening it would only turn recoverable slow sinks
|
|
59
|
+
* into dropped batches.
|
|
60
|
+
*/
|
|
61
|
+
export const DEFAULT_FLUSH_TIMEOUT_MS = 1000;
|
|
62
|
+
|
|
63
|
+
/** `never` means no transport at all — no spool is written. */
|
|
64
|
+
export const FLUSH_NEVER = 'never';
|
|
65
|
+
|
|
66
|
+
/** The one flush policy that spools and sends. */
|
|
67
|
+
export const FLUSH_SESSION_END = 'session-end';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The spool that sits beside a record log.
|
|
71
|
+
*
|
|
72
|
+
* Derived rather than configured: a second path key would let an install
|
|
73
|
+
* point the spool somewhere the appender does not write, and the failure —
|
|
74
|
+
* records logged locally, nothing ever sent — is invisible. `.jsonl` is
|
|
75
|
+
* replaced rather than appended to so the spool is not itself picked up by a
|
|
76
|
+
* reader globbing `*.jsonl` for records.
|
|
77
|
+
*/
|
|
78
|
+
export function spool_path_for(log_path: string): string {
|
|
79
|
+
const dir = path.dirname(log_path);
|
|
80
|
+
const base = path.basename(log_path).replace(/\.jsonl$/u, '');
|
|
81
|
+
return path.join(dir, `${base}.spool.jsonl`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Append one already-serialised record line to the spool and bound it.
|
|
86
|
+
*
|
|
87
|
+
* The line is passed in rather than re-serialised so the spooled bytes are
|
|
88
|
+
* byte-identical to the logged bytes: a sink and a local inspection of the
|
|
89
|
+
* same record must not be able to disagree.
|
|
90
|
+
*/
|
|
91
|
+
export function enqueue_line(
|
|
92
|
+
spool_path: string,
|
|
93
|
+
line: string,
|
|
94
|
+
policy: RetentionPolicy = DEFAULT_RETENTION_POLICY,
|
|
95
|
+
now: Date = new Date(),
|
|
96
|
+
): void {
|
|
97
|
+
fs.mkdirSync(path.dirname(spool_path) || '.', { recursive: true });
|
|
98
|
+
fs.appendFileSync(spool_path, line, { encoding: 'utf-8' });
|
|
99
|
+
if (retention_due(spool_path, policy, now)) {
|
|
100
|
+
enforce_retention(spool_path, policy, now);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface SpawnSenderInput {
|
|
105
|
+
/** Absolute path to `flush_sender.mjs`. */
|
|
106
|
+
readonly sender_script: string;
|
|
107
|
+
readonly spool_path: string;
|
|
108
|
+
readonly endpoint: string;
|
|
109
|
+
readonly timeout_ms?: number;
|
|
110
|
+
/** Node binary. Defaults to the one running this process. */
|
|
111
|
+
readonly node_path?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Spawn the detached sender and return immediately.
|
|
116
|
+
*
|
|
117
|
+
* `detached: true` + `stdio: 'ignore'` + `unref()` is the exact shape the
|
|
118
|
+
* spike measured at 20.5 ms p95, and all three are load-bearing: without
|
|
119
|
+
* `detached` the child dies with the session's process group, without
|
|
120
|
+
* `ignore` the parent holds pipe fds open, and without `unref` the parent's
|
|
121
|
+
* event loop waits for the child — which would reintroduce the very latency
|
|
122
|
+
* the spool exists to remove.
|
|
123
|
+
*
|
|
124
|
+
* The sender is a PLAIN-NODE `.mjs` on purpose. Its siblings in this
|
|
125
|
+
* directory are TypeScript resolved by `tsx` at import time; a detached child
|
|
126
|
+
* has no such resolution to inherit, so a `.ts` sender would depend on a
|
|
127
|
+
* `tsx` binary being findable in a consumer install at teardown — the least
|
|
128
|
+
* observable moment there is. `process.execPath` is the node already running
|
|
129
|
+
* this hook, so it exists by construction.
|
|
130
|
+
*
|
|
131
|
+
* Returns `true` when the child was spawned. Never throws: a transport that
|
|
132
|
+
* can fail a session is worse than a transport that loses a batch, and the
|
|
133
|
+
* batch is not lost — it stays in the spool for the next flush.
|
|
134
|
+
*/
|
|
135
|
+
export function spawn_detached_sender(input: SpawnSenderInput): boolean {
|
|
136
|
+
try {
|
|
137
|
+
const child = spawn(
|
|
138
|
+
input.node_path ?? process.execPath,
|
|
139
|
+
[
|
|
140
|
+
input.sender_script,
|
|
141
|
+
'--spool',
|
|
142
|
+
input.spool_path,
|
|
143
|
+
'--endpoint',
|
|
144
|
+
input.endpoint,
|
|
145
|
+
'--timeout',
|
|
146
|
+
String(input.timeout_ms ?? DEFAULT_FLUSH_TIMEOUT_MS),
|
|
147
|
+
],
|
|
148
|
+
{ detached: true, stdio: 'ignore' },
|
|
149
|
+
);
|
|
150
|
+
child.unref();
|
|
151
|
+
return true;
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Is there anything to send?
|
|
159
|
+
*
|
|
160
|
+
* Checked before the spawn so an idle session pays a `stat` rather than a
|
|
161
|
+
* process. Most sessions invoke no skill at all.
|
|
162
|
+
*/
|
|
163
|
+
export function spool_has_work(spool_path: string): boolean {
|
|
164
|
+
try {
|
|
165
|
+
return fs.statSync(spool_path).size > 0;
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
package/dist/cli/registry.js
CHANGED
|
@@ -59,11 +59,12 @@ export const REGISTRY = [
|
|
|
59
59
|
{ name: 'sessions:list', disposition: 'delegate', synopsis: 'List live agent sessions plus unmerged branches held by other worktrees; --json, --branches.' },
|
|
60
60
|
{ name: 'sessions:claim', disposition: 'delegate', synopsis: 'Claim a roadmap for this session so other sessions skip it; --release clears it.' },
|
|
61
61
|
{ name: 'session:recycle', disposition: 'delegate', synopsis: 'Validate + write the main-session recycle envelope (--verify validates only, --project names the repo); the successor resumes from it after /clear.' },
|
|
62
|
-
{ name: 'roadmap:progress', disposition: 'delegate', synopsis: 'Regenerate agents/roadmaps-progress.md from open roadmaps.' },
|
|
62
|
+
{ name: 'roadmap:progress', disposition: 'delegate', synopsis: 'Regenerate agents/roadmaps-progress.md from open roadmaps; archives completed ones (--no-archive to skip).' },
|
|
63
63
|
{ name: 'roadmap:progress-check', disposition: 'delegate', synopsis: 'Fail if agents/roadmaps-progress.md is stale (for CI).' },
|
|
64
64
|
{ name: 'roadmap:archive', disposition: 'delegate', synopsis: 'Archive completed roadmaps (PR-gate sweep).' },
|
|
65
65
|
{ name: 'gates', disposition: 'delegate', synopsis: 'Open decisions that need you, as actions — owner-filtered roadmap blockers; --all, --json, --reply, --pending (staged requires_confirmation actions).' },
|
|
66
66
|
{ name: 'capabilities:index', disposition: 'delegate', synopsis: 'Regenerate CAPABILITIES.yaml (the package coverage index); --check for CI.' },
|
|
67
|
+
{ name: 'adr:effective', disposition: 'delegate', synopsis: "Effective state of one ADR: status, Decision verbatim, clauses its own amendments superseded, active amendments, axes, trigger state. Authorizes nothing." },
|
|
67
68
|
{ name: 'affected', disposition: 'delegate', synopsis: 'Artefacts related to <artefact> via the discovery relation-graph (BFS).' },
|
|
68
69
|
{ name: 'graph-explain', disposition: 'delegate', synopsis: 'Seed on a <concept>, expand 2 hops over the discovery relation-graph with a node budget.' },
|
|
69
70
|
{ name: 'benchmark', disposition: 'delegate', synopsis: 'Report context-token reduction vs the full always-loaded projection.' },
|