@event4u/agent-config 14.13.0 → 14.14.1
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 +138 -376
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +36 -0
- package/dist/agent-src/commands/pr/create.md +16 -1
- package/dist/agent-src/contexts/execution/terminal-states.md +25 -4
- package/dist/agent-src/rules/code-comment-discipline.md +6 -0
- package/dist/agent-src/rules/missing-skill-recovery.md +2 -2
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
- package/dist/agent-src/scripts/guarded_baseline.ts +22 -7
- package/dist/agent-src/scripts/stubs_due.ts +118 -2
- package/dist/agent-src/scripts/update_roadmap_progress.ts +8 -8
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli-delegate/{chunk-ILMUPLGW.js → chunk-7RXIKF62.js} +159 -86
- package/dist/cli-delegate/cmd_conformance.js +39 -2
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +7 -7
- 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 +1696 -812
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +297 -115
- package/docs/guidelines/agent-infra/guarded-baseline.md +1 -1
- package/docs/guidelines/code-clarity.md +18 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
- package/src/agent-src/scripts/guarded_baseline.ts +22 -7
- package/src/agent-src/scripts/stubs_due.ts +118 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +8 -8
- package/src/config/assurance-capability-registry.json +1 -1
- package/src/config/estate-count-budget.json +20 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +211 -1
- package/src/config/gate-reachability-exemptions.json +1 -0
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/preamble-payload-budget.json +19 -11
- package/src/config/rule-stub-ceilings.json +14 -7
- package/src/config/web-launch-readiness.json +4 -2
- package/src/scripts/_cli/_doctor_emit.ts +71 -0
- package/src/scripts/_cli/_doctor_strict.ts +163 -0
- package/src/scripts/_cli/cmd_doctor.ts +16 -37
- package/src/scripts/_dispatch.bash +11 -4
- package/src/scripts/_lib/activation_ladder.ts +47 -3
- package/src/scripts/_lib/activation_receipt_producer.ts +341 -0
- package/src/scripts/_lib/adr_frontmatter.ts +26 -0
- package/src/scripts/_lib/branch_convergence.ts +186 -0
- package/src/scripts/_lib/candidate_pair_delta.ts +349 -0
- package/src/scripts/_lib/carrier_status.ts +118 -0
- package/src/scripts/_lib/cascade_stage_enumeration.ts +92 -0
- package/src/scripts/_lib/context_observation.ts +132 -0
- package/src/scripts/_lib/continuation_ladder.ts +188 -0
- package/src/scripts/_lib/corpus_manifest.ts +726 -0
- package/src/scripts/_lib/delivery_arm_experiment.ts +18 -2
- package/src/scripts/_lib/estate_carrier_cases.ts +72 -0
- package/src/scripts/_lib/evaluation_cascade.ts +452 -0
- package/src/scripts/_lib/evolution_roi.ts +562 -0
- package/src/scripts/_lib/experiment_binding.ts +95 -0
- package/src/scripts/_lib/harness_evolution_guards.ts +10 -0
- package/src/scripts/_lib/ignored_blocker.ts +8 -1
- package/src/scripts/_lib/lexical_shortlist.ts +215 -0
- package/src/scripts/_lib/llm_candidate_proposer.ts +461 -0
- package/src/scripts/_lib/llm_proposer_transport.ts +153 -0
- package/src/scripts/_lib/outcome_envelope.ts +6 -1
- package/src/scripts/_lib/outcome_vocabularies.ts +135 -5
- package/src/scripts/_lib/pathology_archive.ts +393 -0
- package/src/scripts/_lib/promotion_capability.ts +315 -0
- package/src/scripts/_lib/promotion_evidence.ts +414 -0
- package/src/scripts/_lib/promotion_review.ts +294 -0
- package/src/scripts/_lib/prompt_shape.ts +92 -0
- package/src/scripts/_lib/release_highlights.ts +79 -1
- package/src/scripts/_lib/release_material.ts +125 -2
- package/src/scripts/_lib/repeated_failure.ts +19 -2
- package/src/scripts/_lib/routing_corpus.ts +307 -0
- package/src/scripts/_lib/routing_index_input.ts +104 -0
- package/src/scripts/_lib/rule_injection.ts +22 -1
- package/src/scripts/_lib/runtime_journal.ts +11 -4
- package/src/scripts/_lib/semantic_noop.ts +167 -0
- package/src/scripts/activation_receipt.ts +258 -0
- package/src/scripts/ai_council/cross_exam.ts +128 -0
- package/src/scripts/ai_council/explain_route.ts +157 -0
- package/src/scripts/ai_council/information_gain.ts +293 -0
- package/src/scripts/ai_council/leakage_corpus.ts +385 -0
- package/src/scripts/ai_council/leakage_patterns.ts +453 -0
- package/src/scripts/ai_council/minority_retention.ts +171 -0
- package/src/scripts/ai_council/recouncil_savings.ts +299 -0
- package/src/scripts/ai_council/replay_route.ts +223 -0
- package/src/scripts/ai_council/reviewer_assignment.ts +279 -0
- package/src/scripts/ai_council/routing_training_row.ts +197 -0
- package/src/scripts/ai_council/synthesis_strategy.ts +159 -0
- package/src/scripts/check_adr_frontmatter.ts +4 -9
- package/src/scripts/check_agent_artifact_location.ts +337 -0
- package/src/scripts/check_artefact_count_messaging.ts +74 -8
- package/src/scripts/check_enforcement_coverage.ts +40 -1
- package/src/scripts/check_estate_count.ts +205 -24
- package/src/scripts/check_release_highlights.ts +113 -14
- package/src/scripts/check_roadmap_trackable.ts +17 -2
- package/src/scripts/check_web_launch_readiness.ts +310 -20
- package/src/scripts/corpus_manifest.ts +235 -0
- package/src/scripts/evolution_lab.ts +234 -9
- package/src/scripts/generate_capability_matrix.ts +16 -9
- package/src/scripts/grade_target_readiness.ts +221 -2
- package/src/scripts/hook_effect_doctor.ts +47 -4
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +28 -6
- package/src/scripts/hooks/comment_discipline_hook.ts +146 -0
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/run_continuation_hook.ts +78 -88
- package/src/scripts/hooks/session_eol_hook.ts +12 -1
- package/src/scripts/hooks/turn_end_gate_hook.ts +9 -1
- package/src/scripts/install-hooks.sh +18 -0
- package/src/scripts/language_mirror_hook.ts +14 -2
- package/src/scripts/lint_carrier_integrity.ts +820 -0
- package/src/scripts/lint_code_comments.ts +748 -0
- package/src/scripts/lint_plan_risk_register.ts +39 -19
- package/src/scripts/lint_promotion_paths.ts +779 -0
- package/src/scripts/llm_propose.ts +256 -0
- package/src/scripts/measure_delivery_sets.ts +361 -0
- package/src/scripts/measure_routing_signal.ts +307 -0
- package/src/scripts/model_rule_injection.ts +19 -3
- package/src/scripts/probe_council_retention.ts +495 -0
- package/src/scripts/release.ts +21 -37
- package/src/scripts/release_drill.ts +199 -0
- package/src/scripts/release_env.ts +35 -0
- package/src/scripts/release_publication.ts +142 -0
- package/src/scripts/roadmap_context.ts +15 -0
- package/src/scripts/skill_scout.ts +633 -0
- package/src/scripts/sync_pr_branch.ts +521 -42
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The activation-receipt producer — independent, append-only, zero model calls.
|
|
3
|
+
*
|
|
4
|
+
* `road-to-governed-evidence-production` step 1.1. The receipt SCHEMA
|
|
5
|
+
* ({@link ActivationReceipt}) and the CLASSIFIER ({@link classifyFailure}) both
|
|
6
|
+
* predate this module and are NOT reimplemented here; what was missing was a
|
|
7
|
+
* producer, so nothing ever wrote an `activation` object into an audit line and
|
|
8
|
+
* the receipt-bearing cascade stages had no subject.
|
|
9
|
+
*
|
|
10
|
+
* ## The claims this module is written against
|
|
11
|
+
*
|
|
12
|
+
* Stated once, in `docs/contracts/activation-receipt-trust-boundary.md`, and
|
|
13
|
+
* CITED here rather than restated — step 1.3's verify clause asks for exactly
|
|
14
|
+
* that split, and a second copy of a falsifiable claim is a second thing to
|
|
15
|
+
* keep true.
|
|
16
|
+
*
|
|
17
|
+
* | Claim | Where it binds in this file |
|
|
18
|
+
* |---|---|
|
|
19
|
+
* | TB-1 (no evaluation input decides a STATE) | the import list: no cascade, record, vector or verdict module appears |
|
|
20
|
+
* | TB-2 (an unobserved rung is absent) | {@link buildActivationReceipt} writes only the rungs it was given |
|
|
21
|
+
* | TB-3 (every state names an admitted source) | {@link EVIDENCE_SOURCES} + the refusal in {@link buildActivationReceipt} |
|
|
22
|
+
* | TB-4 (append, never rewrite) | {@link appendActivationLine} opens with `appendFileSync` and mints a fresh `id` |
|
|
23
|
+
* | EC-1 (zero model calls) | no transport import, no spawn, no key read — asserted by this module's test |
|
|
24
|
+
* | EC-3 (a missing observation is never bought) | {@link observeProjection} returns absence and does not retry |
|
|
25
|
+
*
|
|
26
|
+
* ## What is observable today, and what is not
|
|
27
|
+
*
|
|
28
|
+
* Three sources are admitted and all three ship an observer. `adhered` has no
|
|
29
|
+
* admitted source, so this producer CANNOT emit that rung and a real receipt
|
|
30
|
+
* reads `unknown` there. That is a coverage fact recorded rather than papered
|
|
31
|
+
* over: admitting a fourth source with no observer behind it would describe a
|
|
32
|
+
* capability that does not exist, which is the "population of zero" failure the
|
|
33
|
+
* roadmap already flags elsewhere.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import * as fs from 'node:fs';
|
|
37
|
+
import * as path from 'node:path';
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
LADDER_RUNGS,
|
|
41
|
+
type ActivationReceipt,
|
|
42
|
+
type LadderRung,
|
|
43
|
+
type PrecedenceReason,
|
|
44
|
+
type RungState,
|
|
45
|
+
} from './activation_ladder.js';
|
|
46
|
+
import type { NoFreeForm } from './runtime_journal.js';
|
|
47
|
+
import { type PrivacyClass } from './privacy_class.js';
|
|
48
|
+
|
|
49
|
+
type Assert<T extends true> = T;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The closed set of evidence sources a rung state may come from (TB-3).
|
|
53
|
+
*
|
|
54
|
+
* Each entry names a surface this producer can read without consulting the
|
|
55
|
+
* evaluation side. A source may not be added without an observer: the set
|
|
56
|
+
* describes what can be observed, never what one would like to observe.
|
|
57
|
+
*
|
|
58
|
+
* - `source-tree` — the authored artefact exists under `src/`, so it can match
|
|
59
|
+
* at all. Reads the `eligible` rung.
|
|
60
|
+
* - `discovery-manifest` — the built manifest names the artefact for the active
|
|
61
|
+
* install, i.e. the selector kept it. Reads the `selected` rung.
|
|
62
|
+
* - `host-projection` — a host tree carries a file for the artefact. Reads the
|
|
63
|
+
* `projected` rung.
|
|
64
|
+
*/
|
|
65
|
+
export const EVIDENCE_SOURCES = ['source-tree', 'discovery-manifest', 'host-projection'] as const;
|
|
66
|
+
export type EvidenceSource = (typeof EVIDENCE_SOURCES)[number];
|
|
67
|
+
|
|
68
|
+
/** The rung each admitted source is allowed to speak about. One source, one rung. */
|
|
69
|
+
export const SOURCE_RUNG: Readonly<Record<EvidenceSource, LadderRung>> = {
|
|
70
|
+
'source-tree': 'eligible',
|
|
71
|
+
'discovery-manifest': 'selected',
|
|
72
|
+
'host-projection': 'projected',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Rungs no admitted source covers today. Present so the gap is enumerable. */
|
|
76
|
+
export const UNOBSERVED_RUNGS: readonly LadderRung[] = LADDER_RUNGS.filter(
|
|
77
|
+
(r) => !Object.values(SOURCE_RUNG).includes(r),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* One observation. `state` is the evidence; `evidence_source` is where it came
|
|
82
|
+
* from.
|
|
83
|
+
*
|
|
84
|
+
* The field is `evidence_source` and not `source` on purpose: `source` is a
|
|
85
|
+
* member of `FREE_FORM_KEYS`, so a field of that name would make the privacy
|
|
86
|
+
* assertion at the bottom of this file `Assert<false>` and stop the build. The
|
|
87
|
+
* rename is the cheap half of keeping this input type incapable of carrying
|
|
88
|
+
* content.
|
|
89
|
+
*/
|
|
90
|
+
export interface RungObservation {
|
|
91
|
+
readonly rung: LadderRung;
|
|
92
|
+
readonly state: Exclude<RungState, 'unknown'>;
|
|
93
|
+
readonly evidence_source: EvidenceSource;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ReceiptBuild {
|
|
97
|
+
readonly receipt: ActivationReceipt | null;
|
|
98
|
+
readonly errors: readonly string[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build a receipt from observations.
|
|
103
|
+
*
|
|
104
|
+
* TB-2 in one line: the output's `rungs` map has exactly the keys the caller
|
|
105
|
+
* observed. Nothing is defaulted, nothing is filled in, and a rung nobody looked
|
|
106
|
+
* at is absent — which `rungState` reads as `unknown` and `ladderRate` keeps out
|
|
107
|
+
* of its denominator.
|
|
108
|
+
*
|
|
109
|
+
* TB-3 in the refusals: an unadmitted source, or an admitted source speaking
|
|
110
|
+
* about a rung that is not its own, is an ERROR and yields no receipt. It is
|
|
111
|
+
* not a warning and it is not silently dropped, because a receipt missing a rung
|
|
112
|
+
* is indistinguishable from a receipt whose rung was refused, and only one of
|
|
113
|
+
* those is honest.
|
|
114
|
+
*/
|
|
115
|
+
export function buildActivationReceipt(
|
|
116
|
+
artefact: string,
|
|
117
|
+
observations: readonly RungObservation[],
|
|
118
|
+
reason?: PrecedenceReason,
|
|
119
|
+
): ReceiptBuild {
|
|
120
|
+
const errors: string[] = [];
|
|
121
|
+
if (!artefact) errors.push('artefact id is required');
|
|
122
|
+
|
|
123
|
+
const rungs: Partial<Record<LadderRung, RungState>> = {};
|
|
124
|
+
for (const o of observations) {
|
|
125
|
+
if (!(EVIDENCE_SOURCES as readonly string[]).includes(o.evidence_source)) {
|
|
126
|
+
errors.push(`evidence_source '${o.evidence_source}' is not admitted (TB-3)`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (SOURCE_RUNG[o.evidence_source] !== o.rung) {
|
|
130
|
+
errors.push(
|
|
131
|
+
`evidence_source '${o.evidence_source}' may only observe rung ` +
|
|
132
|
+
`'${SOURCE_RUNG[o.evidence_source]}', not '${o.rung}' (TB-3)`,
|
|
133
|
+
);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (rungs[o.rung] !== undefined && rungs[o.rung] !== o.state) {
|
|
137
|
+
errors.push(`rung '${o.rung}' observed twice with conflicting states`);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
rungs[o.rung] = o.state;
|
|
141
|
+
}
|
|
142
|
+
if (errors.length > 0) return { receipt: null, errors };
|
|
143
|
+
|
|
144
|
+
const receipt: ActivationReceipt = reason === undefined
|
|
145
|
+
? { artefact, rungs }
|
|
146
|
+
: { artefact, rungs, reason };
|
|
147
|
+
return { receipt, errors: [] };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// --- observers ---------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Observe the `projected` rung: does a host tree carry a file for this artefact?
|
|
154
|
+
*
|
|
155
|
+
* EC-3 is the reason this returns `undefined` rather than probing further when
|
|
156
|
+
* the host root itself is missing. A host tree that does not exist is not
|
|
157
|
+
* evidence that the artefact was not projected into it — it is the absence of a
|
|
158
|
+
* place to look, and the honest output of a missing observation is nothing at
|
|
159
|
+
* all. Escalating from "read a path" to "go find the tree" is the unbounded-cost
|
|
160
|
+
* mistake EC-3 names, and it is the one a reader would forgive.
|
|
161
|
+
*/
|
|
162
|
+
export function observeProjection(
|
|
163
|
+
hostRoot: string,
|
|
164
|
+
artefactRelPath: string,
|
|
165
|
+
): RungObservation | undefined {
|
|
166
|
+
if (!existsQuiet(hostRoot)) return undefined;
|
|
167
|
+
return {
|
|
168
|
+
rung: 'projected',
|
|
169
|
+
state: existsQuiet(path.join(hostRoot, artefactRelPath)) ? 'reached' : 'not-reached',
|
|
170
|
+
evidence_source: 'host-projection',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Observe the `eligible` rung: is the artefact authored in the source tree?
|
|
176
|
+
*
|
|
177
|
+
* Same absence rule as {@link observeProjection}: an unreadable source root
|
|
178
|
+
* yields no observation rather than a negative one.
|
|
179
|
+
*/
|
|
180
|
+
export function observeSourceTree(
|
|
181
|
+
sourceRoot: string,
|
|
182
|
+
artefactRelPath: string,
|
|
183
|
+
): RungObservation | undefined {
|
|
184
|
+
if (!existsQuiet(sourceRoot)) return undefined;
|
|
185
|
+
return {
|
|
186
|
+
rung: 'eligible',
|
|
187
|
+
state: existsQuiet(path.join(sourceRoot, artefactRelPath)) ? 'reached' : 'not-reached',
|
|
188
|
+
evidence_source: 'source-tree',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Observe the `selected` rung from an already-loaded discovery-manifest id set.
|
|
194
|
+
*
|
|
195
|
+
* Takes the id set rather than a path: loading and parsing the manifest is the
|
|
196
|
+
* caller's, so this stays a pure predicate and EC-1's "no I/O beyond a read"
|
|
197
|
+
* holds trivially for it. An EMPTY set is treated as no manifest — not as a
|
|
198
|
+
* manifest that selected nothing — because those are different observations and
|
|
199
|
+
* folding them is exactly TB-2's failure.
|
|
200
|
+
*/
|
|
201
|
+
export function observeSelection(
|
|
202
|
+
selectedIds: ReadonlySet<string>,
|
|
203
|
+
artefactId: string,
|
|
204
|
+
): RungObservation | undefined {
|
|
205
|
+
if (selectedIds.size === 0) return undefined;
|
|
206
|
+
return {
|
|
207
|
+
rung: 'selected',
|
|
208
|
+
state: selectedIds.has(artefactId) ? 'reached' : 'not-reached',
|
|
209
|
+
evidence_source: 'discovery-manifest',
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function existsQuiet(p: string): boolean {
|
|
214
|
+
try {
|
|
215
|
+
fs.statSync(p);
|
|
216
|
+
return true;
|
|
217
|
+
} catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// --- the audit line ----------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
/** What THIS producer's lines carry: an artefact id, enums, and counts. */
|
|
225
|
+
const PRODUCER_PRIVACY_CLASS: PrivacyClass = 'ids-only';
|
|
226
|
+
|
|
227
|
+
export interface ActivationLineInput {
|
|
228
|
+
/** The artefact the receipt is about. An id, never a path. */
|
|
229
|
+
artefact: string;
|
|
230
|
+
/** The rungs actually observed. Absent rungs stay absent (TB-2). */
|
|
231
|
+
rungs: Readonly<Partial<Record<LadderRung, RungState>>>;
|
|
232
|
+
/**
|
|
233
|
+
* The precedence reason, when there is one.
|
|
234
|
+
*
|
|
235
|
+
* Named `precedence_reason` and NOT `reason`, which is a `FREE_FORM_KEYS`
|
|
236
|
+
* member: a field called `reason` on this type makes the assertion at the
|
|
237
|
+
* bottom of this file `Assert<false>` and stops the build, even though the
|
|
238
|
+
* value it would hold is a closed six-value enum. The emitted JSON key is
|
|
239
|
+
* still `reason`, because that is `ActivationReceipt`'s field name and the
|
|
240
|
+
* ladder module owns the receipt shape. The rename buys the compile-time
|
|
241
|
+
* floor at the cost of one line of mapping, which is the right trade: a
|
|
242
|
+
* guard that only rejects genuinely dangerous names is a guard someone has
|
|
243
|
+
* to keep tuning.
|
|
244
|
+
*/
|
|
245
|
+
precedence_reason?: PrecedenceReason | undefined;
|
|
246
|
+
/** ISO-8601 UTC; the caller supplies it so this stays pure. */
|
|
247
|
+
ts: string;
|
|
248
|
+
/** ULID, UUID or content hash; the caller supplies it. */
|
|
249
|
+
id: string;
|
|
250
|
+
work_id?: string | undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface BuiltActivationLine {
|
|
254
|
+
line: Record<string, unknown> | null;
|
|
255
|
+
errors: string[];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Build ONE audit-log-v1 line carrying an `activation` object.
|
|
260
|
+
*
|
|
261
|
+
* Deliberately the same envelope as `_lib/review_skipped_record.ts` — same
|
|
262
|
+
* mandatory fields, same `{line, errors}` contract, same refusal to return a
|
|
263
|
+
* half-built line. `skills_applied` is omitted for the same reason it is there:
|
|
264
|
+
* this producer has no skill observation to offer, and `[]` would assert one.
|
|
265
|
+
*/
|
|
266
|
+
export function buildActivationLine(input: ActivationLineInput): BuiltActivationLine {
|
|
267
|
+
const errors: string[] = [];
|
|
268
|
+
if (!input.artefact) errors.push('artefact is required');
|
|
269
|
+
if (!input.ts) errors.push('ts (ISO-8601 UTC) is required');
|
|
270
|
+
if (!input.id) errors.push('id (ULID, UUID, or content hash) is required');
|
|
271
|
+
const observed = Object.keys(input.rungs);
|
|
272
|
+
if (observed.length === 0) {
|
|
273
|
+
// A receipt that observed nothing is not a receipt. Writing one would
|
|
274
|
+
// add a line to an append-only ledger that carries no evidence and
|
|
275
|
+
// still counts as a record — the shape a later reader mistakes for
|
|
276
|
+
// coverage.
|
|
277
|
+
errors.push('a receipt must carry at least one observed rung');
|
|
278
|
+
}
|
|
279
|
+
for (const r of observed) {
|
|
280
|
+
if (!(LADDER_RUNGS as readonly string[]).includes(r)) {
|
|
281
|
+
errors.push(`'${r}' is not a ladder rung`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (errors.length) return { line: null, errors };
|
|
285
|
+
|
|
286
|
+
const activation: Record<string, unknown> = { artefact: input.artefact, rungs: { ...input.rungs } };
|
|
287
|
+
if (input.precedence_reason !== undefined) activation['reason'] = input.precedence_reason;
|
|
288
|
+
|
|
289
|
+
const line: Record<string, unknown> = {
|
|
290
|
+
schema_version: 1,
|
|
291
|
+
id: input.id,
|
|
292
|
+
ts: input.ts,
|
|
293
|
+
work_id: input.work_id ?? `activation-${input.ts}`,
|
|
294
|
+
phase: 'report',
|
|
295
|
+
outcome: 'success',
|
|
296
|
+
confidence_band: 'high',
|
|
297
|
+
risk_class: 'low',
|
|
298
|
+
memory: { asks: 0, hits: 0 },
|
|
299
|
+
verify: { claims: 0, first_try_passes: 0 },
|
|
300
|
+
rules_applied: [],
|
|
301
|
+
privacy_class: PRODUCER_PRIVACY_CLASS,
|
|
302
|
+
persona: null,
|
|
303
|
+
input_kind: 'prompt',
|
|
304
|
+
type: 'note',
|
|
305
|
+
activation,
|
|
306
|
+
};
|
|
307
|
+
return { line, errors: [] };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Append ONE line to the monthly audit log (TB-4).
|
|
312
|
+
*
|
|
313
|
+
* `appendFileSync` and nothing else: no read-modify-write, no truncation, no
|
|
314
|
+
* seek. Returns the file it wrote so a caller can assert the write happened
|
|
315
|
+
* rather than trusting that it did.
|
|
316
|
+
*/
|
|
317
|
+
export function appendActivationLine(
|
|
318
|
+
workspaceRoot: string,
|
|
319
|
+
line: Record<string, unknown>,
|
|
320
|
+
ts: string,
|
|
321
|
+
): string {
|
|
322
|
+
const dir = path.join(workspaceRoot, 'agents', 'runtime', 'state', 'audit');
|
|
323
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
324
|
+
const file = path.join(dir, `${ts.slice(0, 7)}.jsonl`);
|
|
325
|
+
fs.appendFileSync(file, `${JSON.stringify(line)}\n`, 'utf8');
|
|
326
|
+
return file;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The privacy floor, by construction, on this producer's input type.
|
|
331
|
+
*
|
|
332
|
+
* Same mechanism as the two producers `docs/contracts/audit-log-v1.md`
|
|
333
|
+
* § Privacy floor already names. Adding a `path`, `detail`, `reason_text` or
|
|
334
|
+
* any other `FREE_FORM_KEYS` member below makes this `Assert<false>` and stops
|
|
335
|
+
* the build. `reason` is one of them, which is why the precedence field is
|
|
336
|
+
* called `precedence_reason` here and mapped to `reason` on the way out — see
|
|
337
|
+
* the field's own note.
|
|
338
|
+
*/
|
|
339
|
+
type _ActivationLineInputCarriesNoFreeFormField = Assert<
|
|
340
|
+
[NoFreeForm<ActivationLineInput>] extends [never] ? false : true
|
|
341
|
+
>;
|
|
@@ -308,6 +308,32 @@ export type EvidenceStrength = (typeof EVIDENCE_STRENGTHS)[number];
|
|
|
308
308
|
|
|
309
309
|
export const DISCOVERY_STATES = ['complete', 'incomplete'] as const;
|
|
310
310
|
export const AUTHORITY_BASES = ['evidence', 'owner_intent'] as const;
|
|
311
|
+
export type AuthorityBasis = (typeof AUTHORITY_BASES)[number];
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Reopen-authority vocabulary (`adr-layout.md` § Reopen authority).
|
|
315
|
+
*
|
|
316
|
+
* Moved here from `check_adr_frontmatter.ts` on 2026-08-31 so the ADR gate and
|
|
317
|
+
* `road-to-harness-promotion-bridge` step 7.2 read ONE list. 7.2's whole
|
|
318
|
+
* requirement is "route through the existing gate, not a second governance
|
|
319
|
+
* system"; importing the validator's private constant was not available, and
|
|
320
|
+
* copying it would have created the second system the step forbids.
|
|
321
|
+
*
|
|
322
|
+
* Both fields stay OPTIONAL at the ADR reader — an absent `reopen_policy`
|
|
323
|
+
* resolves to `unclassified`, never to `owner`.
|
|
324
|
+
*/
|
|
325
|
+
export const REOPEN_POLICIES = ['directional', 'owner', 'unclassified'] as const;
|
|
326
|
+
export type ReopenPolicy = (typeof REOPEN_POLICIES)[number];
|
|
327
|
+
|
|
328
|
+
export const PROTECTED_DIMENSIONS = [
|
|
329
|
+
'purpose',
|
|
330
|
+
'security_floor',
|
|
331
|
+
'privacy_floor',
|
|
332
|
+
'external_commitment',
|
|
333
|
+
'governance',
|
|
334
|
+
'none',
|
|
335
|
+
] as const;
|
|
336
|
+
export type ProtectedDimension = (typeof PROTECTED_DIMENSIONS)[number];
|
|
311
337
|
|
|
312
338
|
export interface AdrProvenance {
|
|
313
339
|
kind: string | null;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-target branch-convergence policy — does the DEFAULT branch belong in the
|
|
3
|
+
* integration set when a PR targets something else?
|
|
4
|
+
*
|
|
5
|
+
* Decided by AI council on 2026-09-03 (`anthropic/claude-sonnet-4-5` +
|
|
6
|
+
* `openai/codex-default`) for the `cascade-default-inclusion-policy` blocker of
|
|
7
|
+
* `road-to-cascading-base-integration`. Both seats independently rejected the
|
|
8
|
+
* two offered answers — always-include, and include-past-a-commit-distance —
|
|
9
|
+
* and named the same third one: an explicit per-target policy read from
|
|
10
|
+
* repository intent. A commit-distance threshold was refused by both as an
|
|
11
|
+
* unmeasured constant.
|
|
12
|
+
*
|
|
13
|
+
* The load-bearing property is WHERE the policy is read from, and both seats
|
|
14
|
+
* called it non-negotiable: **the resolved PR target commit SHA, and nothing
|
|
15
|
+
* else.** Reading it from the PR head, the checkout, or an unpinned branch name
|
|
16
|
+
* lets a reviewed change edit its own acceptance criteria — the confused-deputy
|
|
17
|
+
* bypass. One seat: *"I would block any implementation reading policy from PR
|
|
18
|
+
* head, checkout state, or unpinned branch name."* So this module takes a
|
|
19
|
+
* `targetSha` it never resolves itself and a reader that can only answer at a
|
|
20
|
+
* SHA; there is deliberately no filesystem access anywhere in it.
|
|
21
|
+
*
|
|
22
|
+
* ── Where the policy file lives, and why it is NOT `.agent-settings.yml` ──
|
|
23
|
+
*
|
|
24
|
+
* The council's specification named `.agent-settings.yml`, section
|
|
25
|
+
* `branchConvergence`, on the premise that it is a committed file. In this
|
|
26
|
+
* repository, and in every consumer this package installs into, it is not:
|
|
27
|
+
* `.gitignore:317` ignores it here, and `src/config/gitignore-block.txt:56`
|
|
28
|
+
* writes that same ignore into every consumer's managed block. `git show
|
|
29
|
+
* <sha>:.agent-settings.yml` therefore returns nothing at every SHA in every
|
|
30
|
+
* repository this ships to, which would make the policy permanently unreadable
|
|
31
|
+
* and the feature dead on arrival.
|
|
32
|
+
*
|
|
33
|
+
* A SHA-pinned read requires a TRACKED file. The architecture the council
|
|
34
|
+
* specified is kept byte for byte — same section name, same shape, same
|
|
35
|
+
* exclusive read from the target SHA — and only the path moves, to a tracked
|
|
36
|
+
* repo-root file. If `.agent-settings.yml` ever becomes tracked, the
|
|
37
|
+
* `branchConvergence:` section moves across unchanged.
|
|
38
|
+
*
|
|
39
|
+
* Deferred by explicit council instruction and not to be added without
|
|
40
|
+
* reopening that decision: globs, pattern precedence, inheritance, configurable
|
|
41
|
+
* reason strings, and any repository-wide fallback policy.
|
|
42
|
+
*/
|
|
43
|
+
import { parse as parseYaml } from 'yaml';
|
|
44
|
+
|
|
45
|
+
/** The tracked, SHA-readable file the policy is read from. */
|
|
46
|
+
export const POLICY_PATH = '.branch-convergence.yml';
|
|
47
|
+
|
|
48
|
+
/** The only two answers a target may give. Closed by council instruction. */
|
|
49
|
+
export type DefaultBranchPolicy = 'include' | 'exclude';
|
|
50
|
+
|
|
51
|
+
export interface BranchConvergenceTarget {
|
|
52
|
+
readonly defaultBranch: DefaultBranchPolicy;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface BranchConvergencePolicy {
|
|
56
|
+
readonly enabled: boolean;
|
|
57
|
+
/** Keyed by EXACT target branch name (`release/1.x`), never a pattern. */
|
|
58
|
+
readonly targets: Readonly<Record<string, BranchConvergenceTarget>>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Read one path at one commit. Returns `null` when the path does not exist at
|
|
63
|
+
* that commit — which is a real answer, not an error.
|
|
64
|
+
*
|
|
65
|
+
* The signature takes the SHA FIRST and offers no way to ask for the working
|
|
66
|
+
* tree. That is the trust boundary expressed in a type: a caller cannot
|
|
67
|
+
* accidentally read the PR head, because there is no argument for it.
|
|
68
|
+
*/
|
|
69
|
+
export type ShaFileReader = (sha: string, repoPath: string) => string | null;
|
|
70
|
+
|
|
71
|
+
/** The target is not the default branch and no exact entry names it. */
|
|
72
|
+
export class MissingBranchConvergencePolicy extends Error {
|
|
73
|
+
readonly target: string;
|
|
74
|
+
constructor(target: string) {
|
|
75
|
+
super(`MissingBranchConvergencePolicy(target="${target}")`);
|
|
76
|
+
this.name = 'MissingBranchConvergencePolicy';
|
|
77
|
+
this.target = target;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The policy exists at the target SHA but does not validate. */
|
|
82
|
+
export class InvalidBranchConvergencePolicy extends Error {
|
|
83
|
+
constructor(detail: string) {
|
|
84
|
+
super(`InvalidBranchConvergencePolicy: ${detail}`);
|
|
85
|
+
this.name = 'InvalidBranchConvergencePolicy';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The PR target names no commit the server will report. Fails closed. */
|
|
90
|
+
export class UnresolvableTargetSha extends Error {
|
|
91
|
+
readonly target: string;
|
|
92
|
+
constructor(target: string) {
|
|
93
|
+
super(`UnresolvableTargetSha(target="${target}")`);
|
|
94
|
+
this.name = 'UnresolvableTargetSha';
|
|
95
|
+
this.target = target;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const TARGET_KEYS = new Set(['defaultBranch']);
|
|
100
|
+
const ROOT_KEYS = new Set(['enabled', 'targets']);
|
|
101
|
+
|
|
102
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
103
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Parse and validate the `branchConvergence` section of a policy document.
|
|
108
|
+
*
|
|
109
|
+
* Exported separately from the SHA read so the VALIDATION is testable without a
|
|
110
|
+
* git object, for the same reason `classifyConflicts` is pure in
|
|
111
|
+
* `sync_pr_branch.ts`: the rejection of a malformed policy is the useful part,
|
|
112
|
+
* and a validator only reachable through a git call is a validator nobody
|
|
113
|
+
* covers.
|
|
114
|
+
*
|
|
115
|
+
* Duplicate keys are rejected by the YAML parser itself (`uniqueKeys` is on by
|
|
116
|
+
* default in `yaml` v2), so a target named twice is a parse failure rather than
|
|
117
|
+
* a silent last-wins.
|
|
118
|
+
*/
|
|
119
|
+
export function parseBranchConvergencePolicy(text: string): BranchConvergencePolicy | null {
|
|
120
|
+
let doc: unknown;
|
|
121
|
+
try {
|
|
122
|
+
doc = parseYaml(text);
|
|
123
|
+
} catch (exc) {
|
|
124
|
+
throw new InvalidBranchConvergencePolicy(exc instanceof Error ? exc.message : String(exc));
|
|
125
|
+
}
|
|
126
|
+
if (!isPlainObject(doc)) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const section = doc['branchConvergence'];
|
|
130
|
+
if (section === undefined) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
if (!isPlainObject(section)) {
|
|
134
|
+
throw new InvalidBranchConvergencePolicy('`branchConvergence` is not a mapping');
|
|
135
|
+
}
|
|
136
|
+
for (const k of Object.keys(section)) {
|
|
137
|
+
if (!ROOT_KEYS.has(k)) {
|
|
138
|
+
throw new InvalidBranchConvergencePolicy(`unknown field \`branchConvergence.${k}\``);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const enabled = section['enabled'];
|
|
142
|
+
if (typeof enabled !== 'boolean') {
|
|
143
|
+
throw new InvalidBranchConvergencePolicy('`enabled` must be true or false');
|
|
144
|
+
}
|
|
145
|
+
const rawTargets = section['targets'] ?? {};
|
|
146
|
+
if (!isPlainObject(rawTargets)) {
|
|
147
|
+
throw new InvalidBranchConvergencePolicy('`targets` must be a mapping');
|
|
148
|
+
}
|
|
149
|
+
const targets: Record<string, BranchConvergenceTarget> = {};
|
|
150
|
+
for (const [name, entry] of Object.entries(rawTargets)) {
|
|
151
|
+
if (!isPlainObject(entry)) {
|
|
152
|
+
throw new InvalidBranchConvergencePolicy(`\`targets.${name}\` is not a mapping`);
|
|
153
|
+
}
|
|
154
|
+
for (const k of Object.keys(entry)) {
|
|
155
|
+
if (!TARGET_KEYS.has(k)) {
|
|
156
|
+
throw new InvalidBranchConvergencePolicy(`unknown field \`targets.${name}.${k}\``);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const v = entry['defaultBranch'];
|
|
160
|
+
if (v !== 'include' && v !== 'exclude') {
|
|
161
|
+
throw new InvalidBranchConvergencePolicy(
|
|
162
|
+
`\`targets.${name}.defaultBranch\` must be include or exclude, got ${JSON.stringify(v)}`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
targets[name] = { defaultBranch: v };
|
|
166
|
+
}
|
|
167
|
+
return { enabled, targets };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The policy as the PR TARGET carries it, at the target's own commit.
|
|
172
|
+
*
|
|
173
|
+
* `null` means the file does not exist at that commit. That is not an error
|
|
174
|
+
* here — it becomes `MissingBranchConvergencePolicy` one layer up, and only for
|
|
175
|
+
* a target that actually needs an entry.
|
|
176
|
+
*/
|
|
177
|
+
export function loadPolicyAtSha(targetSha: string, read: ShaFileReader): BranchConvergencePolicy | null {
|
|
178
|
+
if (!/^[0-9a-f]{7,40}$/.test(targetSha)) {
|
|
179
|
+
throw new InvalidBranchConvergencePolicy(`\`targetSha\` is not a commit id: ${JSON.stringify(targetSha)}`);
|
|
180
|
+
}
|
|
181
|
+
const text = read(targetSha, POLICY_PATH);
|
|
182
|
+
if (text === null) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return parseBranchConvergencePolicy(text);
|
|
186
|
+
}
|