create-agent-rig 0.7.1 → 0.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/CHANGELOG.md CHANGED
@@ -9,7 +9,85 @@ Versions are published to npm as [`create-agent-rig`](https://www.npmjs.com/pack
9
9
 
10
10
  Numbering is ordinary semver — **additive is a minor, a fix is a patch** — so
11
11
  that "I only take minors" remains a usable policy; 0.3.2 shipped additive
12
- content as a patch by the owner's call and stays recorded as one.
12
+ content as a patch by the owner's call and stays recorded as one. 0.8.0 is the
13
+ second recorded departure; its own entry states the direction and the reason,
14
+ and this paragraph deliberately does not restate them — a numbering rule with
15
+ two copies of its exceptions is the shape 0.8.0 exists to remove.
16
+
17
+ ## 0.8.0
18
+
19
+ **Three stale second copies, spread over three payload files a rig obeys,
20
+ described the mechanisms behind them wrongly — and each fix is a deletion.** A
21
+ count and a path list, both in `.claude/rules/autonomy.md`, and a paraphrase of
22
+ that same path list in `loop/SKILL.md`, which ships in both harnesses' copies.
23
+ Every one of them was a second writing of a fact the code owns, and every one
24
+ had drifted from its source while the suite stayed green.
25
+
26
+ **The `templates/agent-os/` payload gains no file and loses none**, measured
27
+ tarball to tarball — `npm pack create-agent-rig@0.7.1` unpacked, against this
28
+ release's own `npm pack`. That payload is the same 95 files either way, and
29
+ exactly three of them differ in content. ⚠ The tarball as a whole is **not**
30
+ unchanged: it grows by eleven files, and the section below names which and why.
31
+
32
+ **Numbered a minor by the owner's call, not by this file's rule.** The rule at
33
+ the top — additive is a minor, a fix is a patch — makes this delta a patch,
34
+ because nothing is added to what a project installs. It ships as `0.8.0`
35
+ because the owner's milestone of that name closes here and the number was fixed
36
+ before the delta was measured. Recorded rather than reconciled, the way `0.3.2`
37
+ is recorded above: a consumer on "I only take minors" receives three corrected
38
+ documents and no new capability.
39
+
40
+ ### Fixed
41
+
42
+ - **`.claude/rules/autonomy.md` stated a blind-spot count `guard-secret-file`
43
+ had outgrown.** The Never bullet said the guard's header "states the four
44
+ blind spots"; the header had been raised past four in `51402e99`, and the
45
+ stale copy shipped in 0.6.1, 0.6.2, 0.7.0 and 0.7.1 — a security rule
46
+ describing its own mechanism wrongly for four releases. The fix is not a
47
+ corrected number, which would only restart the same clock: the prose states
48
+ no count and points at the header, and `README.md` moved with it for the same
49
+ reason. Pinned in the generator's `test/template/guard-secret-file.test.ts`
50
+ (absent in a generated rig) › "no live rulebook document restates the guard’s
51
+ limit count" and › "the rule and the README still send the reader to the
52
+ guard’s own header".
53
+
54
+ - **The same file re-listed the paths `guard-rulebook` protects, and its list
55
+ was incomplete.** The enumeration of trees an unattended run may not edit
56
+ omitted `.claude/doctor-exemptions.json`, which the guard does refuse. It is
57
+ replaced by a pointer to `RULEBOOK_PREFIXES` in
58
+ `.claude/scripts/unattended-flag.mjs` — the set the guard judges an edit
59
+ against — so there is one spelling of it rather than two. One fact that
60
+ pointer cannot carry is stated beside it, because it does not follow from the
61
+ set: the checkout board selector is refused **even when an item's allow-list
62
+ names it**.
63
+
64
+ - **`loop/SKILL.md` paraphrased that set too, in both harnesses' copies.** The
65
+ step where a session composes an allow-list summarised the protected rulebook
66
+ in prose, so the one moment the set is read in anger was the moment it was
67
+ read from a summary that had already drifted. It now names
68
+ `RULEBOOK_PREFIXES` and says to open it. Both copies carry the change — the
69
+ Claude skill at `.claude/skills/loop/SKILL.md` and its Codex projection at
70
+ `.agents/skills/loop/SKILL.md`.
71
+
72
+ ### Inside the generator, and not inside a rig
73
+
74
+ `packages/cli/src/policy/` is new — a typed policy declaration, a registry, a
75
+ decision-record schema and one adapter per harness. Its compiled output adds
76
+ eleven files to the published tarball, 245 → 256. **No command imports it**, so nothing a
77
+ project scaffolded from this release does comes from it. What such a project
78
+ does get from this release is the three corrected documents above — and an
79
+ installed 0.7.1 rig still needs `create-agent-rig upgrade` to receive them —
80
+ which is a necessary condition and not a sufficient one, because a rig whose
81
+ owner has edited one of those three files is handed a conflict to merge rather
82
+ than an overwrite.
83
+ This module is named here only because a reader diffing the two tarballs sees
84
+ eleven new files and is owed the reason they are not part of that answer.
85
+
86
+ `templates/release-ledger.json` carries `0.7.1` at `52e879b6`, the commit it was
87
+ published from, read from `npm view create-agent-rig@0.7.1 gitHead` and verified
88
+ an ancestor of `master`. `templates/hash-history.json` is regenerated from it and
89
+ now covers eleven releases, `0.2.0` through `0.7.1`, so `upgrade` can tell an
90
+ untouched file from an edited one in a rig installed from 0.7.1.
13
91
 
14
92
  ## 0.7.1
15
93
 
package/README.md CHANGED
@@ -116,7 +116,7 @@ wired by both `.claude/settings.json` and `.codex/hooks.json`:
116
116
  carries a credential VALUE. Both arms read one vocabulary,
117
117
  `.claude/scripts/lib/secrets.mjs`, and a refusal names the pattern and the line
118
118
  and **never the matched value** — printing it would leak the secret in the act
119
- of refusing it. Its four blind spots are in its own header, each naming the
119
+ of refusing it. Its blind spots are in its own header, each naming the
120
120
  test that pins it or saying plainly that none does — and those tests live in
121
121
  this generator, not in the rig;
122
122
  - **`block-no-verify`** — refuses bypassing pre-commit checks (and knows the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-agent-rig",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Scaffold a new project with an agent operating system (rules, gates, hooks) and a runnable code skeleton",
5
5
  "keywords": [
6
6
  "create",
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The contract a harness adapter satisfies (RP-76).
3
+ *
4
+ * The core knows that a harness exists and has a hook-wiring surface; it does
5
+ * not know any harness's name, event vocabulary, tool names or paths. Those
6
+ * belong to the adapter, one module per harness under `../harness/`, and
7
+ * adding a harness means adding one such module and registering it — pinned in
8
+ * `test/template/policy-declaration.test.ts` under "adding a harness touches
9
+ * adapters only", whose two tests name exactly the files that may mention each
10
+ * harness. Their names carry the harness words this file may not, which is why
11
+ * the describe is cited here rather than either test.
12
+ *
13
+ * An adapter is a mapping, not a compiler: given a declaration it names the
14
+ * event, matcher and hook path the harness wires for it. The correspondence
15
+ * between that answer and the snapshot the rig actually ships is a test, in
16
+ * both directions, in the same file.
17
+ */
18
+ export {};
@@ -0,0 +1,201 @@
1
+ /**
2
+ * The decision-record schema (RP-76): what one verdict of one policy on one
3
+ * harness must carry to be audited later, and the validator that refuses a
4
+ * record which would read as more than it is.
5
+ *
6
+ * Emitting these at runtime is a separate task; this file is the shape and the
7
+ * rules. The rules that go beyond "the field is in its vocabulary":
8
+ *
9
+ * - the policy must be registered and the version compatible with it, so the
10
+ * record names semantics a reader can look up;
11
+ * - the operation must be one the policy declares;
12
+ * - a qualifier (`UNVERIFIABLE`, `UNMEASURED`) must carry a reason;
13
+ * - a capability state of `UNSUPPORTED` or `INTEGRATION-FAILED` must qualify
14
+ * the verdict `UNVERIFIABLE` — an unenforceable policy never yields a silent
15
+ * pass (`./vocabulary.ts`, `CAPABILITY_STATES`);
16
+ * - every evidence kind the policy requires must be present;
17
+ * - a policy that redacts must not be recorded with unredacted diagnostics;
18
+ * - the timestamp is supplied by the caller and must be an ISO-8601 date-time
19
+ * with seconds and an explicit zone (`ISO_8601` below; a bare date is
20
+ * refused) — no clock here.
21
+ *
22
+ * Each rule is one test in `packages/cli/test/policy-declaration.test.ts`
23
+ * under "validating a decision record".
24
+ *
25
+ * ⚠ `diagnostics.redacted` is the emitter's claim, and this validator enforces
26
+ * the claim's presence, not the property: a record marked redacted whose
27
+ * `diagnostics.text`, `observedFacts[].value` or `evidence[].value` still
28
+ * carries a matched value is accepted here. Scanning content would pull the
29
+ * secret vocabulary into the core, which the dependency-direction test
30
+ * forbids — so the emitting task owns that scan, over those three fields,
31
+ * before it persists a record.
32
+ */
33
+ import { compatibilityOf, findPolicy } from './registry.js';
34
+ import { CAPABILITY_STATES, DECISION_OUTCOMES, EVIDENCE_KINDS, OPERATIONS, VERDICT_QUALIFIERS, } from './vocabulary.js';
35
+ import { isRecord, member, nonEmptyString, unknownKeys } from './validation.js';
36
+ export const DECISION_RECORD_SCHEMA_VERSION = 1;
37
+ const KEYS = [
38
+ 'schemaVersion',
39
+ 'policyId',
40
+ 'policyVersion',
41
+ 'harness',
42
+ 'operation',
43
+ 'capabilityState',
44
+ 'observedFacts',
45
+ 'verdict',
46
+ 'evidence',
47
+ 'artifactVersion',
48
+ 'diagnostics',
49
+ 'recordedAt',
50
+ ];
51
+ const NEVER_SILENT_PASS = ['UNSUPPORTED', 'INTEGRATION-FAILED'];
52
+ /** Date, `T`, time to the second (fractions allowed), and an explicit zone. */
53
+ const ISO_8601 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
54
+ const namedPairs = (problems, field, value, keys) => {
55
+ if (!Array.isArray(value)) {
56
+ problems.push({ field, message: 'must be a list' });
57
+ return;
58
+ }
59
+ value.forEach((entry, index) => {
60
+ if (!isRecord(entry)) {
61
+ problems.push({ field: `${field}[${index}]`, message: 'must be an object' });
62
+ return;
63
+ }
64
+ unknownKeys(problems, entry, keys, `${field}[${index}]`);
65
+ for (const key of keys)
66
+ nonEmptyString(problems, `${field}[${index}].${key}`, entry[key]);
67
+ });
68
+ };
69
+ const checkVerdict = (problems, value, capabilityState) => {
70
+ if (!isRecord(value)) {
71
+ problems.push({ field: 'verdict', message: 'must be an object' });
72
+ return;
73
+ }
74
+ unknownKeys(problems, value, ['outcome', 'qualifier', 'reason'], 'verdict');
75
+ member(problems, 'verdict.outcome', value.outcome, DECISION_OUTCOMES);
76
+ const qualified = 'qualifier' in value;
77
+ if (qualified) {
78
+ member(problems, 'verdict.qualifier', value.qualifier, VERDICT_QUALIFIERS);
79
+ if (typeof value.reason !== 'string' || value.reason.trim() === '') {
80
+ problems.push({
81
+ field: 'verdict.reason',
82
+ message: `a ${String(value.qualifier)} verdict must say why`,
83
+ });
84
+ }
85
+ }
86
+ else if ('reason' in value && typeof value.reason !== 'string') {
87
+ problems.push({ field: 'verdict.reason', message: 'must be a string when present' });
88
+ }
89
+ if (capabilityState !== null && NEVER_SILENT_PASS.includes(capabilityState)) {
90
+ if (value.qualifier !== 'UNVERIFIABLE') {
91
+ problems.push({
92
+ field: 'verdict.qualifier',
93
+ message: `capabilityState ${capabilityState} never yields a silent pass: ` +
94
+ 'the verdict must be qualified UNVERIFIABLE with a reason',
95
+ });
96
+ }
97
+ }
98
+ };
99
+ const checkEvidence = (problems, value, required) => {
100
+ if (!Array.isArray(value)) {
101
+ problems.push({ field: 'evidence', message: 'must be a list' });
102
+ return;
103
+ }
104
+ const present = new Set();
105
+ value.forEach((entry, index) => {
106
+ if (!isRecord(entry)) {
107
+ problems.push({ field: `evidence[${index}]`, message: 'must be an object' });
108
+ return;
109
+ }
110
+ unknownKeys(problems, entry, ['kind', 'value'], `evidence[${index}]`);
111
+ if (member(problems, `evidence[${index}].kind`, entry.kind, EVIDENCE_KINDS)) {
112
+ present.add(entry.kind);
113
+ }
114
+ nonEmptyString(problems, `evidence[${index}].value`, entry.value);
115
+ });
116
+ for (const kind of required) {
117
+ if (!present.has(kind)) {
118
+ problems.push({ field: 'evidence', message: `the policy requires ${kind} evidence` });
119
+ }
120
+ }
121
+ };
122
+ const checkDiagnostics = (problems, value, mustRedact) => {
123
+ if (!isRecord(value)) {
124
+ problems.push({ field: 'diagnostics', message: 'must be an object' });
125
+ return;
126
+ }
127
+ unknownKeys(problems, value, ['redacted', 'text'], 'diagnostics');
128
+ if (typeof value.redacted !== 'boolean') {
129
+ problems.push({ field: 'diagnostics.redacted', message: 'must be a boolean' });
130
+ }
131
+ else if (mustRedact && !value.redacted) {
132
+ problems.push({
133
+ field: 'diagnostics.redacted',
134
+ message: 'the policy redacts matched values, so its diagnostics must be recorded redacted',
135
+ });
136
+ }
137
+ if (typeof value.text !== 'string') {
138
+ problems.push({ field: 'diagnostics.text', message: 'must be a string' });
139
+ }
140
+ };
141
+ /** Validate an unknown value as a decision record, reporting every problem at once. */
142
+ export function validateDecisionRecord(input) {
143
+ if (!isRecord(input)) {
144
+ return { ok: false, problems: [{ field: '', message: 'a decision record is an object' }] };
145
+ }
146
+ const problems = [];
147
+ unknownKeys(problems, input, KEYS);
148
+ if (input.schemaVersion !== DECISION_RECORD_SCHEMA_VERSION) {
149
+ problems.push({
150
+ field: 'schemaVersion',
151
+ message: `must be ${DECISION_RECORD_SCHEMA_VERSION}, got ${String(input.schemaVersion)}`,
152
+ });
153
+ }
154
+ const policy = typeof input.policyId === 'string' ? findPolicy(input.policyId) : null;
155
+ if (policy === null) {
156
+ problems.push({
157
+ field: 'policyId',
158
+ message: `${JSON.stringify(input.policyId)} is not a registered policy`,
159
+ });
160
+ }
161
+ else if (typeof input.policyVersion === 'string') {
162
+ const compatibility = compatibilityOf(policy.policyId, input.policyVersion);
163
+ if (compatibility !== 'compatible') {
164
+ problems.push({
165
+ field: 'policyVersion',
166
+ message: `${JSON.stringify(input.policyVersion)} is ${compatibility} with ${policy.policyId} ${policy.policyVersion}`,
167
+ });
168
+ }
169
+ }
170
+ else {
171
+ nonEmptyString(problems, 'policyVersion', input.policyVersion);
172
+ }
173
+ nonEmptyString(problems, 'harness', input.harness);
174
+ if (member(problems, 'operation', input.operation, OPERATIONS) && policy !== null) {
175
+ if (!policy.operations.includes(input.operation)) {
176
+ problems.push({
177
+ field: 'operation',
178
+ message: `${policy.policyId} does not apply to ${input.operation}`,
179
+ });
180
+ }
181
+ }
182
+ const capabilityState = member(problems, 'capabilityState', input.capabilityState, CAPABILITY_STATES)
183
+ ? input.capabilityState
184
+ : null;
185
+ namedPairs(problems, 'observedFacts', input.observedFacts, ['name', 'value']);
186
+ checkVerdict(problems, input.verdict, capabilityState);
187
+ checkEvidence(problems, input.evidence, policy?.requiredEvidence ?? []);
188
+ nonEmptyString(problems, 'artifactVersion', input.artifactVersion);
189
+ checkDiagnostics(problems, input.diagnostics, policy !== null && policy.redaction !== 'none');
190
+ if (typeof input.recordedAt !== 'string' ||
191
+ !ISO_8601.test(input.recordedAt) ||
192
+ Number.isNaN(Date.parse(input.recordedAt))) {
193
+ problems.push({
194
+ field: 'recordedAt',
195
+ message: `must be an ISO-8601 date-time with seconds and an explicit zone, got ${JSON.stringify(input.recordedAt)}`,
196
+ });
197
+ }
198
+ if (problems.length > 0)
199
+ return { ok: false, problems };
200
+ return { ok: true, value: input };
201
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The typed policy declaration (RP-76).
3
+ *
4
+ * A declaration is the semantic source of one policy: what the invariant is,
5
+ * where it applies, what the mechanism can answer, how it fails, what a
6
+ * verdict must carry as evidence, and which version of those semantics a
7
+ * decision record is talking about. It is data plus a validator — not a policy
8
+ * language, not an interpreter, not a compiler: a harness adapter reads a
9
+ * declaration and names its own native surface for it (`./adapter.ts`), and
10
+ * that is the whole of the transformation.
11
+ *
12
+ * The shape is closed: a field this interface does not name is refused, so a
13
+ * field added by mistake cannot travel unnoticed into a record somebody later
14
+ * audits. Every enumerated field draws from `./vocabulary.ts`.
15
+ */
16
+ import { AUTONOMY_TIERS, DECISION_OUTCOMES, ENFORCEMENT_TIMINGS, EVIDENCE_KINDS, FAILURE_SEMANTICS, HARNESS_CAPABILITIES, LIFECYCLE_STATES, OPERATIONS, REDACTION_RULES, } from './vocabulary.js';
17
+ import { isRecord, matching, member, members, nonEmptyString, unknownKeys } from './validation.js';
18
+ const KEYS = [
19
+ 'policyId',
20
+ 'policyVersion',
21
+ 'lifecycle',
22
+ 'invariant',
23
+ 'tier',
24
+ 'operations',
25
+ 'timing',
26
+ 'requiredCapability',
27
+ 'mechanism',
28
+ 'outcomes',
29
+ 'onInternalError',
30
+ 'onUnreadableInput',
31
+ 'requiredEvidence',
32
+ 'redaction',
33
+ 'statedIn',
34
+ ];
35
+ export const KEBAB_CASE = /^[a-z][a-z0-9-]*$/;
36
+ export const POLICY_VERSION = /^\d+\.\d+$/;
37
+ /** Validate an unknown value as a declaration, reporting every problem at once. */
38
+ export function validateDeclaration(input) {
39
+ if (!isRecord(input)) {
40
+ return { ok: false, problems: [{ field: '', message: 'a declaration is an object' }] };
41
+ }
42
+ const problems = [];
43
+ unknownKeys(problems, input, KEYS);
44
+ matching(problems, 'policyId', input.policyId, KEBAB_CASE, 'kebab-case');
45
+ matching(problems, 'policyVersion', input.policyVersion, POLICY_VERSION, 'MAJOR.MINOR');
46
+ member(problems, 'lifecycle', input.lifecycle, LIFECYCLE_STATES);
47
+ nonEmptyString(problems, 'invariant', input.invariant);
48
+ member(problems, 'tier', input.tier, AUTONOMY_TIERS);
49
+ members(problems, 'operations', input.operations, OPERATIONS, { nonEmpty: true });
50
+ member(problems, 'timing', input.timing, ENFORCEMENT_TIMINGS);
51
+ member(problems, 'requiredCapability', input.requiredCapability, HARNESS_CAPABILITIES);
52
+ matching(problems, 'mechanism', input.mechanism, KEBAB_CASE, 'kebab-case');
53
+ members(problems, 'outcomes', input.outcomes, DECISION_OUTCOMES, { nonEmpty: true });
54
+ member(problems, 'onInternalError', input.onInternalError, FAILURE_SEMANTICS);
55
+ member(problems, 'onUnreadableInput', input.onUnreadableInput, FAILURE_SEMANTICS);
56
+ members(problems, 'requiredEvidence', input.requiredEvidence, EVIDENCE_KINDS, {
57
+ nonEmpty: false,
58
+ });
59
+ member(problems, 'redaction', input.redaction, REDACTION_RULES);
60
+ nonEmptyString(problems, 'statedIn', input.statedIn);
61
+ if (problems.length > 0)
62
+ return { ok: false, problems };
63
+ // Every field above was checked against the shape, so the narrowing is earned
64
+ // rather than asserted: the cast is to the type the checks just established.
65
+ return { ok: true, value: input };
66
+ }
67
+ /**
68
+ * Define a policy: validate it and hand back a frozen copy. Throws an Error
69
+ * whose message lists every problem as `field: message`, so a registry that
70
+ * loads at import time fails with the whole list rather than one line at a
71
+ * time.
72
+ */
73
+ export function definePolicy(input) {
74
+ const result = validateDeclaration(input);
75
+ if (!result.ok) {
76
+ const lines = result.problems.map(({ field, message }) => `${field}: ${message}`);
77
+ throw new Error(`invalid policy declaration:\n ${lines.join('\n ')}`);
78
+ }
79
+ const policy = result.value;
80
+ return Object.freeze({
81
+ ...policy,
82
+ operations: Object.freeze([...policy.operations]),
83
+ outcomes: Object.freeze([...policy.outcomes]),
84
+ requiredEvidence: Object.freeze([...policy.requiredEvidence]),
85
+ });
86
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The policy registry (RP-76): the declarations of the policies this rig
3
+ * enforces, and the compatibility rule a decision record is read against.
4
+ *
5
+ * The three entries are the guards the rulebook already ships; declaring them
6
+ * changes nothing about how they run. Each field below was read off the
7
+ * guard's own header and its tests rather than inferred from its name, and the
8
+ * correspondence between a declaration and the hook wiring both harnesses
9
+ * carry is pinned in `test/template/policy-declaration.test.ts` › "every
10
+ * registered policy is wired in the %s snapshot under its event, matcher and
11
+ * hook path". Which hook file a mechanism name resolves to is the adapter's
12
+ * business (`../harness/`), not this file's.
13
+ */
14
+ import { definePolicy } from './declaration.js';
15
+ import { POLICY_VERSION } from './declaration.js';
16
+ const NEVER_TIER = 'rules/autonomy.md#never';
17
+ /**
18
+ * What the three share, stated once: each is a pre-operation hook of the
19
+ * Never tier that can allow, block, or refuse to inspect; each fails open on
20
+ * its own error and closed on input it can see but cannot read
21
+ * (`rules/invariants.md`, "Fail closed on a match, fail open on an error" and
22
+ * "Refusing to inspect is a third outcome"). The one evidence every outcome
23
+ * carries is the exit code; a refusal also prints a diagnostic line, an allow
24
+ * prints nothing, so `diagnostic-text` is not required of every record.
25
+ */
26
+ const guard = (declaration) => definePolicy({
27
+ policyVersion: '1.0',
28
+ lifecycle: 'active',
29
+ tier: 'never',
30
+ timing: 'before-operation',
31
+ requiredCapability: 'pre-operation-hook',
32
+ outcomes: ['allow', 'block', 'refuse-to-inspect'],
33
+ onInternalError: 'fail-open',
34
+ onUnreadableInput: 'fail-closed',
35
+ requiredEvidence: ['exit-code'],
36
+ statedIn: NEVER_TIER,
37
+ ...declaration,
38
+ });
39
+ /**
40
+ * Secret-write refusal. The guard refuses an edit that names a credential file
41
+ * or carries a credential value; the value arm never prints what it matched —
42
+ * `test/template/guard-secret-file.test.ts` › "never prints the credential it
43
+ * found, nor a fragment of it" — which is what `omit-matched-values` records.
44
+ */
45
+ const secretWriteRefusal = guard({
46
+ policyId: 'secret-write-refusal',
47
+ invariant: 'A credential never enters the repository through an edit.',
48
+ operations: ['file-edit'],
49
+ mechanism: 'guard-secret-file',
50
+ redaction: 'omit-matched-values',
51
+ });
52
+ /**
53
+ * No-verify refusal. The guard refuses a shell command that bypasses the
54
+ * pre-commit gate — `test/template/shell-tools.test.ts` › "refuses a
55
+ * pre-commit bypass through %s" pins the block on every shell tool. The
56
+ * refuse-to-inspect outcome and the fail-open on an unparseable payload are
57
+ * `test/template/hook-command-shape.test.ts` › "%s does not tell the caller to
58
+ * split and retry" and › "allows a malformed payload it cannot parse at all".
59
+ * ⚠ One limit of `onInternalError: 'fail-open'` for this guard: it has no
60
+ * try/catch, so an internal throw exits 1, which the harness reads as allow —
61
+ * a property of the harness that no test here pins.
62
+ */
63
+ const noVerifyRefusal = guard({
64
+ policyId: 'no-verify-refusal',
65
+ invariant: 'The pre-commit gate is never bypassed.',
66
+ operations: ['shell-command'],
67
+ mechanism: 'block-no-verify',
68
+ redaction: 'none',
69
+ });
70
+ /**
71
+ * Rulebook-mutation restriction. In an unattended run the guard refuses an
72
+ * edit under a rulebook prefix outside the current item's allow-list; in an
73
+ * attended session it does nothing — `test/template/guard-rulebook.test.ts` ›
74
+ * "blocks an edit to a rulebook path the allow-list does not name" and ›
75
+ * "allows a hook edit when no unattended flag exists".
76
+ */
77
+ const rulebookMutationRestriction = guard({
78
+ policyId: 'rulebook-mutation-restriction',
79
+ invariant: "In an unattended run, the rulebook is never edited outside the current item's allow-list.",
80
+ operations: ['file-edit'],
81
+ mechanism: 'guard-rulebook',
82
+ redaction: 'none',
83
+ });
84
+ /** Every declared policy, in declaration order. */
85
+ export const POLICIES = Object.freeze([
86
+ secretWriteRefusal,
87
+ noVerifyRefusal,
88
+ rulebookMutationRestriction,
89
+ ]);
90
+ export function findPolicy(policyId) {
91
+ return POLICIES.find((policy) => policy.policyId === policyId) ?? null;
92
+ }
93
+ export function policyIds() {
94
+ return POLICIES.map((policy) => policy.policyId);
95
+ }
96
+ /** The policies the registry still offers — everything not retired. */
97
+ export function activePolicies() {
98
+ return POLICIES.filter((policy) => policy.lifecycle !== 'retired');
99
+ }
100
+ const majorOf = (version) => Number(version.split('.')[0]);
101
+ /**
102
+ * Can a decision record naming `policyVersion` be read against the registered
103
+ * policy? Same MAJOR: yes, whatever the MINOR — a MINOR bump is additive.
104
+ * Different MAJOR: no, the semantics that produced the verdict are not these.
105
+ * A version that is not `MAJOR.MINOR` is refused as malformed rather than
106
+ * parsed for a MAJOR it might have meant.
107
+ */
108
+ export function compatibilityOf(policyId, policyVersion) {
109
+ const policy = findPolicy(policyId);
110
+ if (policy === null)
111
+ return 'unknown-policy';
112
+ if (!POLICY_VERSION.test(policyVersion))
113
+ return 'malformed-version';
114
+ return majorOf(policyVersion) === majorOf(policy.policyVersion) ? 'compatible' : 'incompatible';
115
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The small set of checks the declaration and the decision record share.
3
+ *
4
+ * Every check appends to a problem list instead of throwing, so a caller sees
5
+ * every defect of a record at once — `validateDeclaration` › "reports every
6
+ * problem at once rather than stopping at the first" in
7
+ * `packages/cli/test/policy-declaration.test.ts`. Each message that refuses an
8
+ * enumerated value quotes the value, because a refusal that names the field
9
+ * and not the word leaves the caller guessing which of two spellings it sent.
10
+ */
11
+ export const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
12
+ const quote = (value) => {
13
+ try {
14
+ return JSON.stringify(value) ?? String(value);
15
+ }
16
+ catch {
17
+ return String(value);
18
+ }
19
+ };
20
+ const list = (vocabulary) => vocabulary.map(quote).join(', ');
21
+ /**
22
+ * Refuse a key the shape does not declare — the shape is closed on purpose.
23
+ * A nested shape passes its own field name as `prefix`, so the problem names
24
+ * `verdict.severity` rather than a bare `severity` the caller cannot place.
25
+ */
26
+ export const unknownKeys = (problems, input, known, prefix = '') => {
27
+ for (const key of Object.keys(input)) {
28
+ if (!known.includes(key)) {
29
+ problems.push({ field: prefix === '' ? key : `${prefix}.${key}`, message: 'unknown field' });
30
+ }
31
+ }
32
+ };
33
+ /** Refuse a string that is absent, not a string, or empty. */
34
+ export const nonEmptyString = (problems, field, value) => {
35
+ if (typeof value !== 'string' || value === '') {
36
+ problems.push({ field, message: `must be a non-empty string, got ${quote(value)}` });
37
+ return false;
38
+ }
39
+ return true;
40
+ };
41
+ /** Refuse a string outside a closed vocabulary, quoting the offending value. */
42
+ export const member = (problems, field, value, vocabulary) => {
43
+ if (typeof value === 'string' && vocabulary.includes(value))
44
+ return true;
45
+ problems.push({ field, message: `${quote(value)} is not one of ${list(vocabulary)}` });
46
+ return false;
47
+ };
48
+ /**
49
+ * Refuse a list that is not an array, carries a value outside the vocabulary,
50
+ * repeats one, or — when `nonEmpty` — is empty.
51
+ */
52
+ export const members = (problems, field, value, vocabulary, { nonEmpty }) => {
53
+ if (!Array.isArray(value)) {
54
+ problems.push({ field, message: `must be a list, got ${quote(value)}` });
55
+ return false;
56
+ }
57
+ let clean = true;
58
+ if (nonEmpty && value.length === 0) {
59
+ problems.push({ field, message: 'must not be empty' });
60
+ clean = false;
61
+ }
62
+ const seen = new Set();
63
+ for (const entry of value) {
64
+ if (!member(problems, field, entry, vocabulary)) {
65
+ clean = false;
66
+ continue;
67
+ }
68
+ if (seen.has(entry)) {
69
+ problems.push({ field, message: `${quote(entry)} is listed twice` });
70
+ clean = false;
71
+ }
72
+ seen.add(entry);
73
+ }
74
+ return clean;
75
+ };
76
+ /** Refuse a string that does not match the pattern, saying what shape was expected. */
77
+ export const matching = (problems, field, value, pattern, expected) => {
78
+ if (typeof value === 'string' && pattern.test(value))
79
+ return true;
80
+ problems.push({ field, message: `must be ${expected}, got ${quote(value)}` });
81
+ return false;
82
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The closed vocabularies of the policy declaration (RP-76).
3
+ *
4
+ * Every value a declaration or a decision record may carry in an enumerated
5
+ * field is listed here and nowhere else. Adding a value is a schema edit — a
6
+ * change to this file plus the test that pins the list — never a string a
7
+ * caller invents at runtime. That is what makes a record auditable: an unknown
8
+ * word is refused rather than read as something close to a known one.
9
+ *
10
+ * Harness-neutral by construction: nothing here names a harness, a vendor, a
11
+ * native tool or a native path. The per-harness spellings live in
12
+ * `../harness/`, and `test/template/policy-declaration.test.ts` › "no file
13
+ * under src/policy/core mentions a harness, a vendor, a native tool or a native
14
+ * path" is what keeps them out of here.
15
+ */
16
+ const closed = (values) => Object.freeze(values);
17
+ /**
18
+ * Whether a declared policy can actually be enforced on a given harness
19
+ * surface. `UNSUPPORTED` and `INTEGRATION-FAILED` never yield a silent pass:
20
+ * a decision record carrying either must qualify its verdict `UNVERIFIABLE`
21
+ * (`./decision-record.ts`). The four states are defined here.
22
+ */
23
+ export const CAPABILITY_STATES = closed([
24
+ 'SUPPORTED',
25
+ 'DEGRADED',
26
+ 'UNSUPPORTED',
27
+ 'INTEGRATION-FAILED',
28
+ ]);
29
+ /** The autonomy tiers of `rules/autonomy.md`; `never` is the tier the guards enforce. */
30
+ export const AUTONOMY_TIERS = closed(['tier-0', 'tier-1', 'tier-2', 'never']);
31
+ /** The operations a policy can apply to, named by what the agent does, not by a tool. */
32
+ export const OPERATIONS = closed(['file-edit', 'shell-command']);
33
+ /** When the mechanism decides, relative to the operation it judges. */
34
+ export const ENFORCEMENT_TIMINGS = closed(['before-operation']);
35
+ /** What a harness must provide for the mechanism to run at all. */
36
+ export const HARNESS_CAPABILITIES = closed(['pre-operation-hook']);
37
+ /**
38
+ * The three outcomes a guard can reach: allow, block, or refuse to inspect —
39
+ * the third being neither a match nor an error (`rules/invariants.md`,
40
+ * "Refusing to inspect is a third outcome").
41
+ */
42
+ export const DECISION_OUTCOMES = closed(['allow', 'block', 'refuse-to-inspect']);
43
+ /** What a mechanism does when it cannot decide: let the operation through, or stop it. */
44
+ export const FAILURE_SEMANTICS = closed(['fail-open', 'fail-closed']);
45
+ /** The kinds of evidence a decision record may carry, and a policy may require. */
46
+ export const EVIDENCE_KINDS = closed(['exit-code', 'diagnostic-text', 'test-pointer']);
47
+ /** How a mechanism treats what it matched when it reports: verbatim, or omitted. */
48
+ export const REDACTION_RULES = closed(['none', 'omit-matched-values']);
49
+ /** Where a policy is in its life; a `retired` policy is no longer offered by the registry. */
50
+ export const LIFECYCLE_STATES = closed(['active', 'deprecated', 'retired']);
51
+ /**
52
+ * The two ways a verdict can say "this word is weaker than it looks": the
53
+ * question could not be put (`UNVERIFIABLE`), or nothing backs the answer
54
+ * (`UNMEASURED`). Either one must carry a reason.
55
+ */
56
+ export const VERDICT_QUALIFIERS = closed(['UNVERIFIABLE', 'UNMEASURED']);
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The Claude Code adapter: a declaration → the native hook surface Claude Code
3
+ * wires for it. The authoring surface of this rulebook is Claude-shaped
4
+ * (`CLAUDE.md`, "One operating system, two harnesses"), so the hook files
5
+ * themselves live in the historical directory `./shared-hooks.ts` names and are
6
+ * shared by every harness.
7
+ *
8
+ * What is native here and nowhere in the core: the `PreToolUse` event, the
9
+ * tool names in the matchers, and the snapshot path. The matcher strings are
10
+ * the ones `.claude/settings.json` carries, and the correspondence test holds
11
+ * the two to the SAME tool set, not a subset: a tool dropped here or gained
12
+ * there is reported for this adapter —
13
+ * `test/template/policy-declaration.test.ts` › "reports the no-verify policy
14
+ * on %s when the shell matcher loses PowerShell (mutation: matcher)", › "reports
15
+ * the no-verify policy on %s when the snapshot gains a tool the adapter does
16
+ * not name (mutation: widened snapshot)" and › "reports a policy on %s whose
17
+ * adapter matcher drops a tool the snapshot still wires (mutation: narrowed
18
+ * adapter)". The shell matcher's tool set is owned by `shell-tools.mjs` in the
19
+ * shipped scripts, and `test/template/shell-tools.test.ts` holds that
20
+ * correspondence.
21
+ */
22
+ import { SHARED_HOOKS_DIR } from './shared-hooks.js';
23
+ const EVENT_OF = {
24
+ 'before-operation': 'PreToolUse',
25
+ };
26
+ const MATCHER_OF = {
27
+ 'file-edit': 'Write|Edit|MultiEdit|NotebookEdit|apply_patch',
28
+ 'shell-command': 'Bash|PowerShell',
29
+ };
30
+ export const nativeSurfaceOf = (policy) => ({
31
+ event: EVENT_OF[policy.timing],
32
+ matcher: policy.operations.map((operation) => MATCHER_OF[operation]).join('|'),
33
+ hookPath: `${SHARED_HOOKS_DIR}/${policy.mechanism}.mjs`,
34
+ });
35
+ export const claudeAdapter = Object.freeze({
36
+ harness: 'claude',
37
+ surfaceFile: '.claude/settings.json',
38
+ nativeSurfaceOf,
39
+ });
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The Codex adapter: a declaration → the native hook surface Codex wires for
3
+ * it. Codex's hook wiring (`.codex/hooks.json`) is DERIVED from the authoring
4
+ * harness's snapshot by `scripts/sync-codex-adapter.mjs`
5
+ * (`docs/decisions/codex-adapter.md`): it keeps the authoring harness's matcher
6
+ * spellings, adds the canonical edit tool `apply_patch`, and runs the same hook
7
+ * files from the shared hooks directory. So the strings below coincide with the
8
+ * other adapter's today — by derivation, not by accident — and each adapter
9
+ * still owns its own spelling. The correspondence test holds this adapter's
10
+ * matcher and the derived snapshot's to the SAME tool set, so a tool the
11
+ * snapshot gains or loses, or one this adapter drops, is reported for this
12
+ * adapter alone — `test/template/policy-declaration.test.ts` › "reports the
13
+ * no-verify policy on %s when the shell matcher loses PowerShell (mutation:
14
+ * matcher)", › "reports the no-verify policy on %s when the snapshot gains a
15
+ * tool the adapter does not name (mutation: widened snapshot)" and › "reports
16
+ * a policy on %s whose adapter matcher drops a tool the snapshot still wires
17
+ * (mutation: narrowed adapter)".
18
+ *
19
+ * The shared hooks directory is imported from `./shared-hooks.ts` rather than
20
+ * restated, for the same reason the snapshot is derived rather than
21
+ * hand-written: one spelling of one fact.
22
+ */
23
+ import { SHARED_HOOKS_DIR } from './shared-hooks.js';
24
+ const EVENT_OF = {
25
+ 'before-operation': 'PreToolUse',
26
+ };
27
+ const MATCHER_OF = {
28
+ 'file-edit': 'Write|Edit|MultiEdit|NotebookEdit|apply_patch',
29
+ 'shell-command': 'Bash|PowerShell',
30
+ };
31
+ export const nativeSurfaceOf = (policy) => ({
32
+ event: EVENT_OF[policy.timing],
33
+ matcher: policy.operations.map((operation) => MATCHER_OF[operation]).join('|'),
34
+ hookPath: `${SHARED_HOOKS_DIR}/${policy.mechanism}.mjs`,
35
+ });
36
+ export const codexAdapter = Object.freeze({
37
+ harness: 'codex',
38
+ surfaceFile: '.codex/hooks.json',
39
+ nativeSurfaceOf,
40
+ });
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The harness adapters this generator knows. Adding a harness is one new
3
+ * module beside these two and one entry in the list below — nothing in
4
+ * `../core/` changes, which `test/template/policy-declaration.test.ts` ›
5
+ * "codex is named only by its own adapter and the adapter index" pins by
6
+ * naming exactly the files that may mention each harness.
7
+ */
8
+ import { claudeAdapter } from './claude.js';
9
+ import { codexAdapter } from './codex.js';
10
+ export { claudeAdapter, codexAdapter };
11
+ export { SHARED_HOOKS_DIR } from './shared-hooks.js';
12
+ export const HARNESS_ADAPTERS = Object.freeze([
13
+ claudeAdapter,
14
+ codexAdapter,
15
+ ]);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The one directory every harness runs its hook files from. The rulebook's
3
+ * `.claude/` directory keeps its historical name but holds the shared rules,
4
+ * hooks, scripts and agent specifications for both harnesses (`CLAUDE.md`,
5
+ * "One operating system, two harnesses"), so a hook path is the same string
6
+ * whichever adapter names it. Stated once, here, and imported by each adapter
7
+ * — one spelling of one fact (`rules/invariants.md`, "One mechanism, one
8
+ * implementation").
9
+ */
10
+ export const SHARED_HOOKS_DIR = '.claude/hooks';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The policy declaration, registry, decision-record schema and harness
3
+ * adapters (RP-76). Library surface only — nothing here is reached by the CLI
4
+ * commands yet; emitting decision records at runtime is a separate task.
5
+ */
6
+ export * from './core/vocabulary.js';
7
+ export * from './core/declaration.js';
8
+ export * from './core/registry.js';
9
+ export * from './core/decision-record.js';
10
+ export * from './harness/index.js';
@@ -153,9 +153,13 @@ node .claude/scripts/unattended-flag.mjs on --root "$PWD" --item <item-id> --run
153
153
 
154
154
  `guard-rulebook` reads it (`.claude/rules/autonomy.md`, "Never"): with the flag
155
155
  on, a Write/Edit/MultiEdit/NotebookEdit/`apply_patch` under the generated
156
- rulebook both harnesses' rules, skills, agents and hook wiring, plus their
157
- scripts, queue config and integrity manifest is refused unless its
158
- path starts with an allowed prefix; the board selector is the one always-refused
156
+ rulebook is refused unless its
157
+ path starts with an allowed prefix. 🔴 **Which paths that covers is
158
+ `RULEBOOK_PREFIXES` in `.claude/scripts/unattended-flag.mjs`** read it before
159
+ composing an allow-list, rather than working from a summary here. A summary is a
160
+ second copy, and the one that used to sit in this sentence had gone stale against
161
+ the set it described. One fact the set cannot carry, so it is stated: the board
162
+ selector is the one always-refused
159
163
  exception and cannot be admitted by an allow-list. With no flag the guard does nothing. An
160
164
  item that needs a rulebook path names it here — a decision made at claim
161
165
  time, never a default — and the stop step below turns the flag off. Pinned in
@@ -99,7 +99,7 @@ own cost figures are read next to the lane they do not cover.
99
99
  `MultiEdit`, `NotebookEdit`, or `apply_patch` that names a credential file or carries a credential value,
100
100
  reading its vocabulary from `.claude/scripts/lib/secrets.mjs`. ⚠ **Only that
101
101
  part.** The hook sees what an agent writes through those five tools and
102
- nothing else — its own header states the four blind spots — so whether a
102
+ nothing else — its own header states its blind spots — so whether a
103
103
  credential typed by a human, or committed from disk, is also refused depends
104
104
  on whether this project has a commit-time check. Look at `.husky/` and the CI
105
105
  workflow; this file cannot tell you, and a
@@ -108,15 +108,25 @@ own cost figures are read next to the lane they do not cover.
108
108
  at runtime instead of writing it out, or the check reports its own test data as
109
109
  a leak.
110
110
  - touch production data outside a reviewed migration
111
- - edit the rulebook from an **unattended** run outside the item's allow-list — `guard-rulebook` refuses it.
112
- The rulebook is both harnesses' instruction, agent, skill, script and hook
113
- trees (`CLAUDE.md`, `AGENTS.md`, `.claude/{agents,hooks,rules,scripts,skills}`,
114
- `.agents/`, `.codex/`), plus `.claude/settings.json`, the queue config and its
115
- always-refused board selector, and the integrity manifest. Mechanical:
111
+ - edit the rulebook from an **unattended** run outside the item's allow-list — `guard-rulebook` refuses it,
112
+ and the checkout board selector is refused **even when the item's allow-list
113
+ names it**; that one carve-out does not follow from the set below.
114
+ The rulebook is both harnesses' instruction, rule, agent, skill, script and
115
+ hook trees, plus the settings, queue, exemption and integrity files that decide
116
+ what a session may do. **Which paths exactly is not restated here**: the
117
+ mechanism reads them from `RULEBOOK_PREFIXES` in
118
+ `.claude/scripts/unattended-flag.mjs`, and so should you — the guard judges an
119
+ edit against it, and the flag writer refuses an allow entry that *widens* it.
120
+ Which entries those are is `isWidening`'s answer in that same module, not a
121
+ paraphrase here: pinned in the generator's `unattended-flag.test.ts` — absent
122
+ in a generated rig — › "explains exact protected-prefix refusal separately
123
+ from proper-prefix widening". An entry
124
+ outside the set — ordinary source — is not widening and is accepted. A second
125
+ copy in prose is a copy that goes stale, and this one did. Mechanical:
116
126
  the hook refuses the edit while the unattended flag the `loop` skill writes
117
127
  at claim time is on disk (`.claude/scripts/unattended-flag.mjs`), and does
118
128
  nothing in an attended session. ⚠ It sees edit tool calls only — a
119
- shell redirect into `.claude/settings.json` is not one — and the flag, not
129
+ shell redirect into a protected file is not one — and the flag, not
120
130
  the run, is what arms it; its header states the rest of its limits.
121
131
 
122
132
  ## Stop rules — by work-state, not by feelings
@@ -153,9 +153,13 @@ node .claude/scripts/unattended-flag.mjs on --root "$PWD" --item <item-id> --run
153
153
 
154
154
  `guard-rulebook` reads it (`.claude/rules/autonomy.md`, "Never"): with the flag
155
155
  on, a Write/Edit/MultiEdit/NotebookEdit/`apply_patch` under the generated
156
- rulebook both harnesses' rules, skills, agents and hook wiring, plus their
157
- scripts, queue config and integrity manifest is refused unless its
158
- path starts with an allowed prefix; the board selector is the one always-refused
156
+ rulebook is refused unless its
157
+ path starts with an allowed prefix. 🔴 **Which paths that covers is
158
+ `RULEBOOK_PREFIXES` in `.claude/scripts/unattended-flag.mjs`** read it before
159
+ composing an allow-list, rather than working from a summary here. A summary is a
160
+ second copy, and the one that used to sit in this sentence had gone stale against
161
+ the set it described. One fact the set cannot carry, so it is stated: the board
162
+ selector is the one always-refused
159
163
  exception and cannot be admitted by an allow-list. With no flag the guard does nothing. An
160
164
  item that needs a rulebook path names it here — a decision made at claim
161
165
  time, never a default — and the stop step below turns the flag off. Pinned in
@@ -9,7 +9,8 @@
9
9
  "0.6.0",
10
10
  "0.6.1",
11
11
  "0.6.2",
12
- "0.7.0"
12
+ "0.7.0",
13
+ "0.7.1"
13
14
  ],
14
15
  "files": {
15
16
  ".agents/skills/check-premises/SKILL.md": {
@@ -59,7 +60,8 @@
59
60
  "hashes": [
60
61
  "a2fb0d03c65701af9d2d69cbcb2582ae573f097b1e1071eb2916e5e4f7832b37",
61
62
  "9616e4767942b2021e34ab7bb76eec9a5c280cc608a0756abb02743741d5fc8a",
62
- "a6340b95a03000b89fb27f5357cdd9c7ec12b714054b5334b61a30da600405e8"
63
+ "a6340b95a03000b89fb27f5357cdd9c7ec12b714054b5334b61a30da600405e8",
64
+ "dc1d9c02879e383448f2336ae6433f960f86e8f8efb4ab8ccf9a17a5f7f524c3"
63
65
  ]
64
66
  },
65
67
  ".agents/skills/ro-debug/SKILL.md": {
@@ -438,7 +440,8 @@
438
440
  "hashes": [
439
441
  "32f847a2a3a5136cabcb42f6447da9f8cf98e2d20512212ee5201461128d7798",
440
442
  "b62348a27bcaa1d62a0e4ddd53873fab6cb75f9af8e8c69d24f6ce81c592c4e3",
441
- "b151045e8083c398e94571b35dc9568dfa0102abcb847cdf764f6f694914d53e"
443
+ "b151045e8083c398e94571b35dc9568dfa0102abcb847cdf764f6f694914d53e",
444
+ "f756b7ccc2a4459e58b2125408de1f91372cd6093bf6f4ed14e26dc2eb4190fc"
442
445
  ]
443
446
  },
444
447
  ".claude/scripts/revalidation-report.mjs": {
@@ -550,7 +553,8 @@
550
553
  "75adb84414a330db621ff4f1363b3926f537133199236a9e25bb9880b935e02e",
551
554
  "a2fb0d03c65701af9d2d69cbcb2582ae573f097b1e1071eb2916e5e4f7832b37",
552
555
  "9616e4767942b2021e34ab7bb76eec9a5c280cc608a0756abb02743741d5fc8a",
553
- "a6340b95a03000b89fb27f5357cdd9c7ec12b714054b5334b61a30da600405e8"
556
+ "a6340b95a03000b89fb27f5357cdd9c7ec12b714054b5334b61a30da600405e8",
557
+ "dc1d9c02879e383448f2336ae6433f960f86e8f8efb4ab8ccf9a17a5f7f524c3"
554
558
  ]
555
559
  },
556
560
  ".claude/skills/ro-debug/SKILL.md": {
@@ -9,5 +9,6 @@
9
9
  "0.6.0": "29ca0543368e922349945e52541a92b46ddcc5bd",
10
10
  "0.6.1": "f1d1e3dbd2161545d77ee5e7f90fa8e74b8a9f3e",
11
11
  "0.6.2": "2a1fc8e10cd2b65deb5c95d937fd7de39f2c92a2",
12
- "0.7.0": "6589db36e1daa63a99ec595191db1cccf7373196"
12
+ "0.7.0": "6589db36e1daa63a99ec595191db1cccf7373196",
13
+ "0.7.1": "52e879b6c103f6ba70493007b6a6466c57ea9824"
13
14
  }