@event4u/agent-config 14.18.0 → 14.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +120 -129
- package/README.md +71 -149
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +44 -43
- package/dist/agent-src/commands/review/changes.md +19 -19
- package/dist/agent-src/commands/review.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +37 -4
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +2 -2
- package/dist/agent-src/rules/guidelines.md +1 -1
- package/dist/agent-src/rules/session-canary.md +2 -2
- package/dist/agent-src/rules/token-efficiency.md +1 -1
- package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
- package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
- package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
- package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
- package/dist/agent-src/skills/design-review/SKILL.md +1 -1
- package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
- package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
- package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
- package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
- package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
- package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +17 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +14 -1
- package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
- package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
- package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
- package/dist/cli/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- package/dist/cli/mcp/dispatch.js +10 -0
- package/dist/cli/mcp/stdio.js +33 -2
- package/dist/cli/mcp/telemetry.js +227 -0
- package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-C6IPZRDA.js} +484 -353
- package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-NUMQHLS5.js} +304 -136
- package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-PI2GU3DP.js} +163 -1
- package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
- package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
- package/dist/cli-delegate/cmd_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +489 -121
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +42 -12
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_session_recycle.js +59 -902
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +34 -33
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +4455 -3025
- package/dist/install/install.mjs +975 -689
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +2087 -1134
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/dist/scripts/mcp_server/telemetry.js +148 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/naming.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
- package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
- package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
- package/docs/guidelines/design-antipatterns.md +87 -64
- package/docs/guidelines/redundancy-taxonomy.md +28 -0
- package/package.json +1 -1
- package/src/agent-src/README.md +1 -1
- package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
- package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
- package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
- package/src/config/assurance-capability-registry.json +9 -0
- package/src/config/ci-local-parity.yml +6 -0
- package/src/config/gate-coverage.yml +225 -0
- package/src/config/gate-violation-baselines.json +18 -6
- package/src/config/release-gate-locality.yml +12 -0
- package/src/scripts/_cli/cmd_doctor.ts +2 -2
- package/src/scripts/_cli/cmd_handoff.ts +1 -1
- package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
- package/src/scripts/_cli/cmd_update.ts +1 -1
- package/src/scripts/_cli/cmd_versions.ts +1 -1
- package/src/scripts/_lib/adr_frontmatter.ts +44 -0
- package/src/scripts/_lib/audit_field_provenance.ts +44 -3
- package/src/scripts/_lib/consequence_objects.ts +152 -0
- package/src/scripts/_lib/exec_evidence.ts +7 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
- package/src/scripts/_lib/host_capability.ts +38 -0
- package/src/scripts/_lib/host_permission_checks.ts +251 -0
- package/src/scripts/_lib/install_reach_checks.ts +33 -1
- package/src/scripts/_lib/mcp_bridge.ts +222 -10
- package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
- package/src/scripts/_lib/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- package/src/scripts/_lib/orchestration_record.ts +23 -1
- package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
- package/src/scripts/_lib/release_highlights.ts +93 -0
- package/src/scripts/_lib/release_material.ts +64 -1
- package/src/scripts/_lib/release_position.ts +96 -0
- package/src/scripts/_lib/review_skipped_record.ts +17 -1
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
- package/src/scripts/_lib/security_lint.ts +179 -7
- package/src/scripts/_lib/self_repair.ts +141 -6
- package/src/scripts/_lib/self_repair_store.ts +273 -7
- package/src/scripts/_lib/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/subagent_capsule.ts +155 -3
- package/src/scripts/_lib/surface.ts +86 -0
- package/src/scripts/_lib/ui_authority.ts +116 -0
- package/src/scripts/adr/regenerate_index.ts +54 -1
- package/src/scripts/adr_cite_check.ts +71 -15
- package/src/scripts/ai_council/clients.ts +1 -1
- package/src/scripts/autonomy_friction_corpus.ts +229 -0
- package/src/scripts/build_proof.ts +15 -2
- package/src/scripts/build_rule_trigger_matrix.ts +5 -0
- package/src/scripts/check_beta_review_markers.ts +141 -17
- package/src/scripts/check_enforcement_coverage.ts +1 -1
- package/src/scripts/check_enforcement_denominator.ts +121 -11
- package/src/scripts/check_finding_dispositions.ts +66 -9
- package/src/scripts/check_gate_coverage.ts +96 -0
- package/src/scripts/check_mcp_doc_drift.ts +217 -0
- package/src/scripts/check_pack_size.ts +321 -3
- package/src/scripts/check_release_highlights.ts +74 -1
- package/src/scripts/check_skill_activation_claim.ts +179 -0
- package/src/scripts/check_trigger_evals.ts +1 -1
- package/src/scripts/conformance_scan.ts +44 -3
- package/src/scripts/explain_run.ts +1 -1
- package/src/scripts/generate_capability_matrix.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +75 -1
- package/src/scripts/handoff_context_hook.ts +54 -7
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +14 -11
- package/src/scripts/hooks/augment-dispatcher.sh +1 -1
- package/src/scripts/hooks/category_a.ts +291 -0
- package/src/scripts/hooks/cline-dispatcher.sh +1 -1
- package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
- package/src/scripts/hooks/dispatch_hook.ts +17 -22
- package/src/scripts/hooks/git_command_classifier.ts +156 -1
- package/src/scripts/hooks/host_lowering.ts +215 -0
- package/src/scripts/hooks/host_lowering.yaml +185 -0
- package/src/scripts/hooks/host_semantics.ts +105 -37
- package/src/scripts/hooks/journal_record_hook.ts +10 -0
- package/src/scripts/hooks/permission_gate.ts +117 -0
- package/src/scripts/hooks/session_eol_hook.ts +18 -7
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install.ts +57 -86
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/lint_agent_security.ts +316 -25
- package/src/scripts/lint_confusables.ts +42 -6
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_handoffs.ts +0 -132
- package/src/scripts/lint_hidden_unicode.ts +49 -7
- package/src/scripts/lint_hook_manifest.ts +87 -3
- package/src/scripts/lint_instruction_smuggling.ts +42 -6
- package/src/scripts/lint_mcp_config_security.ts +42 -6
- package/src/scripts/lint_motion_authority_drift.ts +731 -0
- package/src/scripts/lint_readme_size.ts +158 -12
- package/src/scripts/lint_reasoning_fixtures.ts +216 -0
- package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
- package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
- package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/mcp_server/resources.ts +41 -0
- package/src/scripts/mcp_server/telemetry.ts +19 -4
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/release.ts +24 -24
- package/src/scripts/release_publication.ts +6 -2
- package/src/scripts/report_skill_activation.ts +319 -7
- package/src/scripts/schemas/review-findings.schema.json +5 -0
- package/src/scripts/schemas/skill.schema.json +2 -2
- package/src/scripts/self_repair_cli.ts +31 -1
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/skill_scout.ts +69 -0
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
- package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
- package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
|
@@ -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
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - the repo generator (`condense.py generate_claude_skills`), which
|
|
12
12
|
* builds the package's own `.claude/skills/` tree, and
|
|
13
13
|
* - the consumer install finalizer
|
|
14
|
-
* (`
|
|
14
|
+
* (the installer's `finalize_claude_model_tiers`), which rewrites the
|
|
15
15
|
* installed `.claude/skills/` tree so Claude Code performs the
|
|
16
16
|
* per-turn model switch on a consumer with `model.auto_switch: auto`.
|
|
17
17
|
*
|
|
@@ -275,7 +275,7 @@ export function slot_frequency(platform: string, slot: Slot): Frequency {
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
/**
|
|
278
|
-
* Platforms
|
|
278
|
+
* Platforms this package binds nothing on.
|
|
279
279
|
*
|
|
280
280
|
* Copilot declares `fallback_only: true` in the manifest — concerns route
|
|
281
281
|
* through rule-only fallback and the dispatcher no-ops. Every hook-declared
|
|
@@ -294,7 +294,7 @@ export function is_hook_capable(platform_block: Record<string, unknown>): boolea
|
|
|
294
294
|
export interface PlatformBinding {
|
|
295
295
|
/** Platform id → slot → concern ids bound in that slot. */
|
|
296
296
|
slots: Map<string, Map<Slot, string[]>>;
|
|
297
|
-
/** Platform ids
|
|
297
|
+
/** Platform ids declared `fallback_only` — this package binds nothing there. */
|
|
298
298
|
fallback_only: Set<string>;
|
|
299
299
|
}
|
|
300
300
|
|
|
@@ -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
|
+
}
|
|
@@ -29,6 +29,10 @@ import {
|
|
|
29
29
|
CURATED_HEAD_INSTRUCTION,
|
|
30
30
|
MIX_RESPONSE_MARKER,
|
|
31
31
|
MIX_RESPONSE_PLACEHOLDERS,
|
|
32
|
+
PROMISE_OUTCOMES,
|
|
33
|
+
PROMISE_PHRASE,
|
|
34
|
+
PROMISE_READBACK_MARKER,
|
|
35
|
+
extract_changelog_section,
|
|
32
36
|
} from './release_material.js';
|
|
33
37
|
|
|
34
38
|
/** The five curated labels, in the order an operator reads them. */
|
|
@@ -546,6 +550,95 @@ export function mix_response_blockers(
|
|
|
546
550
|
return [];
|
|
547
551
|
}
|
|
548
552
|
|
|
553
|
+
/**
|
|
554
|
+
* The promise the PREVIOUS release head made, or null when it made none.
|
|
555
|
+
*
|
|
556
|
+
* Scoped to that section's governance-response block, not to its whole body: a
|
|
557
|
+
* product line that happens to contain the phrase is not a promise the project
|
|
558
|
+
* made about the next cycle.
|
|
559
|
+
*/
|
|
560
|
+
export function previous_promise(changelogText: string, previousVersion: string): string | null {
|
|
561
|
+
const section = extract_changelog_section(changelogText, previousVersion);
|
|
562
|
+
if (section === null) return null;
|
|
563
|
+
const idx = section.body.indexOf(MIX_RESPONSE_MARKER);
|
|
564
|
+
if (idx === -1) return null;
|
|
565
|
+
const block = mix_response_block(section.body, idx);
|
|
566
|
+
return block.includes(PROMISE_PHRASE) ? block : null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* The ONE predicate for "did this head answer the previous head's promise?".
|
|
571
|
+
*
|
|
572
|
+
* The defect: `_lib/release_material.ts` generates a `Next cycle ships …` line
|
|
573
|
+
* and nothing has ever read the previous one back, so an unmet promise can be
|
|
574
|
+
* restated indefinitely at zero cost — the debt is booked and never called. A
|
|
575
|
+
* promise nobody reads is not a commitment, it is a sentence.
|
|
576
|
+
*
|
|
577
|
+
* Three outcomes are accepted and one of them must be chosen, which is the
|
|
578
|
+
* whole mechanism: it does not require the promise to have been KEPT, and no
|
|
579
|
+
* ratio, deadline or count is enforced here. Refusing an unkept promise would
|
|
580
|
+
* make the honest answer the expensive one and teach an author to promise less
|
|
581
|
+
* rather than to answer truthfully.
|
|
582
|
+
*
|
|
583
|
+
* Length floor and placeholder scan reuse the governance-response discipline
|
|
584
|
+
* for the same reason it exists there: a bare marker, or the writer's own
|
|
585
|
+
* scaffolding, is not an answer a human wrote.
|
|
586
|
+
*/
|
|
587
|
+
export function promise_readback_blockers(
|
|
588
|
+
sectionBody: string,
|
|
589
|
+
version: string,
|
|
590
|
+
previousVersion: string,
|
|
591
|
+
promise: string | null,
|
|
592
|
+
where = '`main`',
|
|
593
|
+
): string[] {
|
|
594
|
+
if (promise === null) return [];
|
|
595
|
+
const idx = sectionBody.indexOf(PROMISE_READBACK_MARKER);
|
|
596
|
+
if (idx === -1) {
|
|
597
|
+
return [
|
|
598
|
+
`the ${version} section does not answer the ${previousVersion} head's ` +
|
|
599
|
+
`\`${PROMISE_PHRASE}\` promise. Add one \`> ${PROMISE_READBACK_MARKER}\` line ` +
|
|
600
|
+
`immediately under the curated head on ${where}, naming the promise and whether ` +
|
|
601
|
+
`it ${PROMISE_OUTCOMES.join(', ')} (the last one with its reason).`,
|
|
602
|
+
];
|
|
603
|
+
}
|
|
604
|
+
const block = mix_response_block(sectionBody, idx);
|
|
605
|
+
const left = MIX_RESPONSE_PLACEHOLDERS.filter((t) => block.includes(t));
|
|
606
|
+
if (left.length > 0) {
|
|
607
|
+
return [
|
|
608
|
+
`the ${version} previous-cycle answer still carries the writer's placeholder(s) ` +
|
|
609
|
+
`(${left.map((t) => `\`${t}\``).join(', ')}). \`CHANGELOG.md\` is published to npm. ` +
|
|
610
|
+
`Write the outcome on ${where} and re-run.`,
|
|
611
|
+
];
|
|
612
|
+
}
|
|
613
|
+
const lower = block.toLowerCase();
|
|
614
|
+
const named = PROMISE_OUTCOMES.filter((o) => lower.includes(o));
|
|
615
|
+
if (named.length === 0) {
|
|
616
|
+
return [
|
|
617
|
+
`the ${version} previous-cycle answer names no outcome. It has to say which of ` +
|
|
618
|
+
`${PROMISE_OUTCOMES.join(', ')} applies to the ${previousVersion} promise — an ` +
|
|
619
|
+
'answer that restates the promise without resolving it is the failure this reads ' +
|
|
620
|
+
'back for.',
|
|
621
|
+
];
|
|
622
|
+
}
|
|
623
|
+
if (readback_answer(block).length < MIX_RESPONSE_MIN_CHARS) {
|
|
624
|
+
return [
|
|
625
|
+
`the ${version} previous-cycle answer is shorter than a written answer ` +
|
|
626
|
+
`(${String(MIX_RESPONSE_MIN_CHARS)} characters after the marker). Name what was ` +
|
|
627
|
+
`promised and what became of it on ${where}, not the outcome word alone.`,
|
|
628
|
+
];
|
|
629
|
+
}
|
|
630
|
+
return [];
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** What the author wrote under the read-back marker, marker and quoting removed. */
|
|
634
|
+
export function readback_answer(block: string): string {
|
|
635
|
+
return block
|
|
636
|
+
.replace(PROMISE_READBACK_MARKER, '')
|
|
637
|
+
.replace(/^[>\s.]+|[>\s.]+$/gu, '')
|
|
638
|
+
.replace(/[>\s]+/gu, ' ')
|
|
639
|
+
.trim();
|
|
640
|
+
}
|
|
641
|
+
|
|
549
642
|
/**
|
|
550
643
|
* The whole response block: the marker's line plus every blockquote line under it.
|
|
551
644
|
*
|
|
@@ -351,6 +351,69 @@ export const MIX_RESPONSE_PLACEHOLDERS: readonly string[] = [
|
|
|
351
351
|
export function render_mix_response(level: string): string[] {
|
|
352
352
|
return [
|
|
353
353
|
`> ${MIX_RESPONSE_MARKER} ${level}.`,
|
|
354
|
-
`>
|
|
354
|
+
`> ${PROMISE_PHRASE} ${MIX_RESPONSE_PLACEHOLDER}, tracked in <roadmap or issue>.`,
|
|
355
355
|
];
|
|
356
356
|
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* The words that turn a governance response into a PROMISE about a later cycle.
|
|
360
|
+
*
|
|
361
|
+
* Named rather than inlined because two sides now read it: the writer emits it,
|
|
362
|
+
* and the next release reads the previous section for it to decide whether a
|
|
363
|
+
* read-back is owed. A promise nothing reads back costs nothing to restate, so
|
|
364
|
+
* it can be restated for as many cycles as there are cycles — the defect this
|
|
365
|
+
* constant exists to make addressable.
|
|
366
|
+
*/
|
|
367
|
+
export const PROMISE_PHRASE = 'Next cycle ships';
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* The marker under which a release head answers the PREVIOUS head's promise.
|
|
371
|
+
*
|
|
372
|
+
* A named sentinel, on the same discipline as `MIX_RESPONSE_MARKER` and
|
|
373
|
+
* `DERIVED_MARKER`: a shape match over prose would accept a sentence that
|
|
374
|
+
* happens to mention last cycle and reject a reworded answer.
|
|
375
|
+
*/
|
|
376
|
+
export const PROMISE_READBACK_MARKER = '**Previous cycle:**';
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* The three outcomes a read-back may report, and the reason there are exactly
|
|
380
|
+
* three.
|
|
381
|
+
*
|
|
382
|
+
* `shipped` and `did not ship` are the honest halves of the fact. `withdrawn`
|
|
383
|
+
* exists because a promise the project decided not to keep is a legitimate
|
|
384
|
+
* third state — but only WITH a reason, which is why the answer also has to
|
|
385
|
+
* clear a written-length floor rather than being one of these three words on
|
|
386
|
+
* its own. Without the third state the mechanism would push an author toward
|
|
387
|
+
* restating a dead promise as still-outstanding, which is the failure it is
|
|
388
|
+
* built to stop.
|
|
389
|
+
*/
|
|
390
|
+
export const PROMISE_OUTCOMES: readonly string[] = ['shipped', 'did not ship', 'withdrawn'];
|
|
391
|
+
|
|
392
|
+
/** Every `## X.Y.Z` release version in the file, in document order (newest first). */
|
|
393
|
+
export function changelog_versions(text: string): string[] {
|
|
394
|
+
const out: string[] = [];
|
|
395
|
+
for (const m of text.matchAll(/^##\s+\[?(\d+\.\d+\.\d+)\b/gmu)) {
|
|
396
|
+
out.push(m[1] as string);
|
|
397
|
+
}
|
|
398
|
+
return out;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* The release published immediately before `version`, or null when there is none.
|
|
403
|
+
*
|
|
404
|
+
* Read from the CHANGELOG rather than from git tags on purpose: the promise
|
|
405
|
+
* being read back is a property of the SECTION, and a tag can exist for a
|
|
406
|
+
* version whose section was archived into `docs/archive/` under an era split —
|
|
407
|
+
* in which case there is no previous promise reachable in this file and the
|
|
408
|
+
* honest answer is that none is owed.
|
|
409
|
+
*
|
|
410
|
+
* `version` itself need not be present: a section still under authoring is
|
|
411
|
+
* named `[Unreleased]`, so the caller passes the version it is releasing and
|
|
412
|
+
* gets the newest RELEASED version that is not it.
|
|
413
|
+
*/
|
|
414
|
+
export function previous_changelog_version(text: string, version: string): string | null {
|
|
415
|
+
const versions = changelog_versions(text);
|
|
416
|
+
const idx = versions.indexOf(version);
|
|
417
|
+
if (idx !== -1) return versions[idx + 1] ?? null;
|
|
418
|
+
return versions[0] ?? null;
|
|
419
|
+
}
|