create-agent-rig 0.7.0 → 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 +164 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/packages/cli/dist/policy/core/adapter.js +18 -0
- package/packages/cli/dist/policy/core/decision-record.js +201 -0
- package/packages/cli/dist/policy/core/declaration.js +86 -0
- package/packages/cli/dist/policy/core/registry.js +115 -0
- package/packages/cli/dist/policy/core/validation.js +82 -0
- package/packages/cli/dist/policy/core/vocabulary.js +56 -0
- package/packages/cli/dist/policy/harness/claude.js +39 -0
- package/packages/cli/dist/policy/harness/codex.js +40 -0
- package/packages/cli/dist/policy/harness/index.js +15 -0
- package/packages/cli/dist/policy/harness/shared-hooks.js +10 -0
- package/packages/cli/dist/policy/index.js +10 -0
- package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +7 -3
- package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +70 -9
- package/templates/agent-os/universal/.claude/rules/autonomy.md +17 -7
- package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +380 -19
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +7 -3
- package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +70 -9
- package/templates/hash-history.json +82 -24
- package/templates/release-ledger.json +3 -1
|
@@ -5,6 +5,62 @@
|
|
|
5
5
|
* run took up?
|
|
6
6
|
*
|
|
7
7
|
* node .claude/scripts/revalidate.mjs --point BEFORE_PR --ticket <id> [--base origin/master] [--config <queue.json>] [--json]
|
|
8
|
+
* node .claude/scripts/revalidate.mjs --point BEFORE_PR --owner-directed [--base origin/master] [--json]
|
|
9
|
+
*
|
|
10
|
+
* BEFORE_PR has two mutually exclusive modes and neither is inferred (RP-94):
|
|
11
|
+
* `--ticket` for a branch that is an item's take-up, `--owner-directed` for
|
|
12
|
+
* owner-directed work or a hotfix that has no item — the path `pr-ship` step 4
|
|
13
|
+
* already named while step 1 could not execute it. Passing both, or neither,
|
|
14
|
+
* is exit 1 — owner-directed-revalidation.test.ts › "refuses both modes at
|
|
15
|
+
* once: exit 1, stderr only, nothing journaled" and › "refuses neither mode:
|
|
16
|
+
* exit 1, and the message names both ways forward".
|
|
17
|
+
*
|
|
18
|
+
* Owner-directed mode runs the `main:<path>` comparison below and nothing
|
|
19
|
+
* else. It resolves no queue config, so no tracker or adapter is reached and
|
|
20
|
+
* no credential is needed — owner-directed-revalidation.test.ts › "needs no
|
|
21
|
+
* tracker credentials: an adapter name that cannot resolve is never reached".
|
|
22
|
+
* A `hold` there is the same exit 2 as the ticketed path's — › "HOLDs when the
|
|
23
|
+
* default branch moved under a path the branch touches".
|
|
24
|
+
*
|
|
25
|
+
* FOUR refusals keep it from becoming a way around the claim chain — exit 1,
|
|
26
|
+
* nothing journalled, each with its own test in that file:
|
|
27
|
+
*
|
|
28
|
+
* - an unresolved `revalidationHold` in this run's state, which is what the
|
|
29
|
+
* ticketed path writes when it holds or answers UNVERIFIABLE — › "refuses
|
|
30
|
+
* when this run carries an unresolved revalidation hold";
|
|
31
|
+
* - a take-up this run declares — › "refuses when the declared run already
|
|
32
|
+
* carries a take-up";
|
|
33
|
+
* - a tracked `.rig/claims/*.json` this branch touches, added, modified,
|
|
34
|
+
* removed or renamed — › "refuses when the branch diff adds a tracked claim
|
|
35
|
+
* record", › "refuses when the branch diff modifies a tracked claim record",
|
|
36
|
+
* › "refuses when the branch RENAMES a claim record — the case
|
|
37
|
+
* --diff-filter=AM could not see" and › "refuses when the branch DELETES its
|
|
38
|
+
* claim record";
|
|
39
|
+
* - `BEFORE_CLOSE` — › "refuses owner-directed at BEFORE_CLOSE — the mode
|
|
40
|
+
* exists for BEFORE_PR only".
|
|
41
|
+
*
|
|
42
|
+
* The first of those is the one this mode most needs, and the first version
|
|
43
|
+
* shipped without it: a ticketed call that had already held was re-run here and
|
|
44
|
+
* exited 0 with nothing in the repository changed.
|
|
45
|
+
*
|
|
46
|
+
* ⚠ Its limits, stated because the mode is a governance surface. What makes a
|
|
47
|
+
* call owner-directed is the CALLER's word plus those four refusals: nothing
|
|
48
|
+
* here can prove an item does not exist. Three specific gaps, each measured
|
|
49
|
+
* rather than reasoned:
|
|
50
|
+
*
|
|
51
|
+
* - with no `RIG_RUN_DIR` there is no run state, so the hold and take-up
|
|
52
|
+
* refusals have nothing to read and cannot fire — and nothing is journalled
|
|
53
|
+
* either. The result says so in `evidence.runState`, and the report on
|
|
54
|
+
* stdout says so out loud — › "says out loud that an undeclared run checked
|
|
55
|
+
* neither the hold nor the take-up".
|
|
56
|
+
* - the claim refusal reads the branch DIFF, so a claim record already on the
|
|
57
|
+
* default branch, or written but not committed, is not seen.
|
|
58
|
+
* - `--base` decides more here than the verdict. The claim comparison that
|
|
59
|
+
* would otherwise survive a wrong base is absent, AND the claim-touch refusal
|
|
60
|
+
* reads the same `mergeBase..HEAD` range — so `--base HEAD` empties the
|
|
61
|
+
* branch diff and disarms that refusal as well as reporting `continue`. Pass
|
|
62
|
+
* the up-to-date `origin/<default>`; `pr-ship` step 1 refreshes it, since
|
|
63
|
+
* this script never talks to a remote itself.
|
|
8
64
|
*
|
|
9
65
|
* One existing checkpoint chain, with one authoritative durable baseline:
|
|
10
66
|
*
|
|
@@ -45,18 +101,33 @@
|
|
|
45
101
|
* "re-reads each dependant's state, and names one the tracker no longer
|
|
46
102
|
* offers") for the loop's write-back.
|
|
47
103
|
*
|
|
48
|
-
* `outcome --point <P> --ticket <id> --action-changed
|
|
49
|
-
* (AR-136) is the second half of the
|
|
50
|
-
* `revalidation-outcome` record whose
|
|
51
|
-
* `
|
|
52
|
-
* needs, made by the writer rather than guessed by
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
104
|
+
* `outcome --point <P> {--ticket <id> | --owner-directed} --action-changed
|
|
105
|
+
* true|false [--note …]` (AR-136, extended by RP-94) is the second half of the
|
|
106
|
+
* evidence: after the re-read, it appends a `revalidation-outcome` record whose
|
|
107
|
+
* `answers` is the seq of the latest matching `revalidation` at that point in
|
|
108
|
+
* this run — the join a report needs, made by the writer rather than guessed by
|
|
109
|
+
* the reader.
|
|
110
|
+
*
|
|
111
|
+
* **Which revalidation it matches depends on the mode, and the two never
|
|
112
|
+
* cross.** `--ticket` matches by key and skips owner-directed detections
|
|
113
|
+
* outright; `--owner-directed` matches by `mode`, because such a detection
|
|
114
|
+
* carries `ticket: null` and cannot be addressed by key — and a ticketed
|
|
115
|
+
* `--ticket null` must not answer it either. Pinned in the generator's
|
|
116
|
+
* `test/template/owner-directed-revalidation.test.ts` (absent in a generated
|
|
117
|
+
* rig) › "answers an owner-directed hold with an owner-directed outcome", ›
|
|
118
|
+
* "refuses a ticketed outcome aimed at an owner-directed detection" and ›
|
|
119
|
+
* "refuses an owner-directed outcome when a ticketed hold is the only one this
|
|
120
|
+
* run carries — and leaves that hold latched".
|
|
121
|
+
*
|
|
122
|
+
* It refuses without a run, without a matching revalidation, and with any word
|
|
123
|
+
* but `true`/`false`, and writes nothing then. The typed resolution names the
|
|
124
|
+
* stable detection id and clears only the matching run-level hold — and only a
|
|
125
|
+
* hold whose id it actually names, which is why an owner-directed outcome
|
|
126
|
+
* cannot release a ticketed one. Exit 2 on `hold` or `unverifiable`, 0 on
|
|
127
|
+
* `continue`, and 1 when the call cannot be acted on (unknown point, neither
|
|
128
|
+
* mode or both, a base that is not a revision — or, on the paths that reach it,
|
|
129
|
+
* a queue config that does not resolve) — and then nothing is journalled,
|
|
130
|
+
* because a refusal is not an answer.
|
|
60
131
|
*
|
|
61
132
|
* ⚠ It reads `<base>` as it is in this checkout and never updates the remote
|
|
62
133
|
* ref itself; `pr-ship` step 1 does that before calling this. A stale ref
|
|
@@ -66,17 +137,24 @@
|
|
|
66
137
|
*/
|
|
67
138
|
|
|
68
139
|
import { execFileSync } from 'node:child_process';
|
|
140
|
+
import { createHash } from 'node:crypto';
|
|
69
141
|
import { realpathSync } from 'node:fs';
|
|
70
142
|
import { dirname, join } from 'node:path';
|
|
71
143
|
import { fileURLToPath } from 'node:url';
|
|
72
144
|
import { withoutGitLocation } from './git-env.mjs';
|
|
73
145
|
import { readRun, recordEvent } from './run-journal.mjs';
|
|
74
|
-
import {
|
|
146
|
+
import {
|
|
147
|
+
clearRevalidationHold,
|
|
148
|
+
readState,
|
|
149
|
+
readStateForSelection,
|
|
150
|
+
recordRevalidationHold,
|
|
151
|
+
} from './run-state.mjs';
|
|
75
152
|
import { POINTS as ALL_POINTS, REVALIDATES } from './lib/revalidation-points.mjs';
|
|
76
153
|
import { takeUpEvidenceOf } from './queue/core.mjs';
|
|
77
154
|
import { loadConfig, optionsWithPlanPath, resolveAdapter } from './queue/index.mjs';
|
|
78
155
|
import { projectRootOfConfig } from './queue/index.mjs';
|
|
79
156
|
import {
|
|
157
|
+
CLAIM_SCHEMA_VERSION,
|
|
80
158
|
revalidateClaim,
|
|
81
159
|
targetShaOf,
|
|
82
160
|
unverifiableResult,
|
|
@@ -95,6 +173,7 @@ const parseArgs = (argv) => {
|
|
|
95
173
|
outcome: false,
|
|
96
174
|
point: null,
|
|
97
175
|
ticket: null,
|
|
176
|
+
ownerDirected: false,
|
|
98
177
|
base: 'origin/master',
|
|
99
178
|
config: null,
|
|
100
179
|
json: false,
|
|
@@ -105,6 +184,7 @@ const parseArgs = (argv) => {
|
|
|
105
184
|
for (let i = 0; i < argv.length; i += 1) {
|
|
106
185
|
const arg = argv[i];
|
|
107
186
|
if (i === 0 && arg === 'outcome') args.outcome = true;
|
|
187
|
+
else if (arg === '--owner-directed') args.ownerDirected = true;
|
|
108
188
|
else if (arg === '--json') args.json = true;
|
|
109
189
|
else if (arg === '--action-changed') args.actionChanged = argv[++i] ?? null;
|
|
110
190
|
else if (arg === '--note') args.note = argv[++i] ?? null;
|
|
@@ -300,6 +380,79 @@ const answerUnverifiable = ({ runDir, ticket, point, json }, operation, cause) =
|
|
|
300
380
|
process.exit(2);
|
|
301
381
|
};
|
|
302
382
|
|
|
383
|
+
/**
|
|
384
|
+
* A tracked claim record, by repository-relative path (RP-94).
|
|
385
|
+
*
|
|
386
|
+
* ⚠ It is a SECOND spelling of the path `claimPathFor` builds, and the two are
|
|
387
|
+
* kept in step by `test/template/owner-directed-revalidation.test.ts` (absent
|
|
388
|
+
* in a generated rig) › "matches the path claimPathFor actually builds, from
|
|
389
|
+
* the repository root and from a nested rig root" — a correspondence check
|
|
390
|
+
* rather than a comment asking the next reader to remember. The leading
|
|
391
|
+
* `(^|/)` is why a rig whose root sits below the git root is still matched;
|
|
392
|
+
* this mode resolves no queue config, so it cannot ask where that root is.
|
|
393
|
+
*/
|
|
394
|
+
const CLAIM_RECORD = /(^|\/)\.rig\/claims\/[^/]+\.json$/;
|
|
395
|
+
|
|
396
|
+
const OWNER_DIRECTED = 'owner-directed';
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* The BEFORE_PR verdict for work that has no item (RP-94).
|
|
400
|
+
*
|
|
401
|
+
* It carries the SAME `main:<path>` drift decision the ticketed path reaches —
|
|
402
|
+
* this mode drops the claim comparison because there is no claim, and drops
|
|
403
|
+
* nothing else. `ticket` is `null` rather than a placeholder: a record naming
|
|
404
|
+
* an item that does not exist is worse than one that admits it has none, and
|
|
405
|
+
* every reader downstream distinguishes them by that field.
|
|
406
|
+
*
|
|
407
|
+
* Pinned in the generator's `test/template/owner-directed-revalidation.test.ts`
|
|
408
|
+
* — absent in a generated rig — › "runs BEFORE_PR with no item and no claim
|
|
409
|
+
* when the default branch did not move under the branch" and › "HOLDs when the
|
|
410
|
+
* default branch moved under a path the branch touches".
|
|
411
|
+
*/
|
|
412
|
+
const ownerDirectedResult = ({ point, base, mergeBase, cited, changed, now, runDeclared }) => {
|
|
413
|
+
const source = changed.map((path) => `main:${path}`);
|
|
414
|
+
const held = source.length > 0;
|
|
415
|
+
return {
|
|
416
|
+
schemaVersion: CLAIM_SCHEMA_VERSION,
|
|
417
|
+
// 🔴 `mergeBase` is in the hash, and it is what keeps this id from being a
|
|
418
|
+
// CONSTANT. Without it the digest was `{mode, point, source}` alone — the
|
|
419
|
+
// same value for every `continue` that has ever run, and the same for any
|
|
420
|
+
// two holds naming the same paths. `revalidation-report.mjs` flattens every
|
|
421
|
+
// run into one typed-resolution index, so one `--action-changed false`
|
|
422
|
+
// recorded last week would mark a genuine hold today as already answered:
|
|
423
|
+
// the metric the report exists to produce, quietly wrong. It stays stable
|
|
424
|
+
// across RETRIES of the same checkpoint on the same branch, which is the
|
|
425
|
+
// property `answerUnverifiable`'s identity has and the one that matters.
|
|
426
|
+
id: createHash('sha256')
|
|
427
|
+
.update(JSON.stringify({ mode: OWNER_DIRECTED, point, mergeBase, source }))
|
|
428
|
+
.digest('hex'),
|
|
429
|
+
ticket: null,
|
|
430
|
+
mode: OWNER_DIRECTED,
|
|
431
|
+
point,
|
|
432
|
+
checkpoint: point,
|
|
433
|
+
result: held ? 'CHANGED' : 'CURRENT',
|
|
434
|
+
changed: held,
|
|
435
|
+
source,
|
|
436
|
+
action: held ? 'hold' : 'continue',
|
|
437
|
+
movedFingerprintSet: [],
|
|
438
|
+
sourcePointer: null,
|
|
439
|
+
evidence: {
|
|
440
|
+
claim: 'not compared: owner-directed work has no item, so there is no claim record',
|
|
441
|
+
tracker: 'not read: owner-directed mode resolves no queue adapter',
|
|
442
|
+
// 🔴 "Could not check" is recorded as itself, never as "checked and
|
|
443
|
+
// clean". With no run directory the hold and take-up refusals have
|
|
444
|
+
// nothing to read, and a reader who saw only `continue` would take the
|
|
445
|
+
// pair for having passed.
|
|
446
|
+
runState: runDeclared
|
|
447
|
+
? 'read fail-closed: no unresolved revalidation hold, no declared take-up'
|
|
448
|
+
: 'NOT read: no RIG_RUN_DIR, so neither the revalidation hold nor the take-up was checked',
|
|
449
|
+
},
|
|
450
|
+
observedAt: now,
|
|
451
|
+
task: { changed: null, from: null, to: null },
|
|
452
|
+
main: { base, mergeBase, cited, changed },
|
|
453
|
+
};
|
|
454
|
+
};
|
|
455
|
+
|
|
303
456
|
/** Every adapter call in this script goes through here, or it can still crash. */
|
|
304
457
|
const readAdapter = async (operation, read, context) => {
|
|
305
458
|
try {
|
|
@@ -332,7 +485,31 @@ if (invokedDirectly()) {
|
|
|
332
485
|
if (!known.includes(args.point)) {
|
|
333
486
|
refuse(`unknown point: ${args.point ?? '(none)'}. This script knows ${known.join(', ')}.`);
|
|
334
487
|
}
|
|
335
|
-
|
|
488
|
+
// RP-94. BEFORE_PR has two modes, and neither is ever inferred. `pr-ship`
|
|
489
|
+
// already named owner-directed work with no item as a legitimate path while
|
|
490
|
+
// this script refused every call without `--ticket`, so that path could not
|
|
491
|
+
// be walked at all. The mode is now stated at the call site — silence is a
|
|
492
|
+
// refusal, not a default, because a mode chosen by absence is a mode nobody
|
|
493
|
+
// reviewed.
|
|
494
|
+
if (args.ticket && args.ownerDirected) {
|
|
495
|
+
refuse(
|
|
496
|
+
'--ticket and --owner-directed are mutually exclusive: a branch that is an item\'s ' +
|
|
497
|
+
'take-up is not owner-directed work. Pass exactly one.',
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
if (args.ownerDirected) {
|
|
501
|
+
if (args.point !== 'BEFORE_PR') {
|
|
502
|
+
refuse(
|
|
503
|
+
`--owner-directed is a BEFORE_PR mode only; ${args.point} compares the claim record ` +
|
|
504
|
+
'itself and still needs --ticket.',
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
} else if (!args.ticket) {
|
|
508
|
+
refuse(
|
|
509
|
+
'--ticket is required: the item whose take-up this branch is. For owner-directed ' +
|
|
510
|
+
'work or a hotfix that has no item, pass --owner-directed instead.',
|
|
511
|
+
);
|
|
512
|
+
}
|
|
336
513
|
|
|
337
514
|
const runDir = process.env.RIG_RUN_DIR || null;
|
|
338
515
|
|
|
@@ -342,16 +519,27 @@ if (invokedDirectly()) {
|
|
|
342
519
|
refuse(`--action-changed must be true or false, got ${args.actionChanged ?? '(none)'}.`);
|
|
343
520
|
}
|
|
344
521
|
const { events } = readRun({ runDir });
|
|
522
|
+
// RP-94. An owner-directed detection carries `ticket: null`, so it cannot
|
|
523
|
+
// be addressed by key — `String(null)` would also match a literal ticket
|
|
524
|
+
// named "null". It is addressed by MODE instead, which is the only thing
|
|
525
|
+
// that distinguishes it. Without this, a `hold` the owner-directed path
|
|
526
|
+
// returned had no way to be answered at all: the skill's stated exit-2
|
|
527
|
+
// remedy was a command the script refused, which is the same shape of
|
|
528
|
+
// contradiction RP-94 exists to remove.
|
|
529
|
+
const subject = args.ownerDirected ? `${OWNER_DIRECTED} work` : args.ticket;
|
|
345
530
|
const target = [...events]
|
|
346
531
|
.reverse()
|
|
347
532
|
.find(
|
|
348
533
|
(e) =>
|
|
349
534
|
e.kind === 'revalidation' &&
|
|
350
|
-
|
|
351
|
-
|
|
535
|
+
e.data?.point === args.point &&
|
|
536
|
+
(args.ownerDirected
|
|
537
|
+
? e.data?.mode === OWNER_DIRECTED
|
|
538
|
+
: e.data?.mode !== OWNER_DIRECTED &&
|
|
539
|
+
String(e.data?.ticket) === String(args.ticket)),
|
|
352
540
|
);
|
|
353
541
|
if (!target) {
|
|
354
|
-
refuse(`no revalidation of ${
|
|
542
|
+
refuse(`no revalidation of ${subject} at ${args.point} in ${runDir} for this outcome to answer.`);
|
|
355
543
|
}
|
|
356
544
|
const now = new Date().toISOString();
|
|
357
545
|
const actionRequired = args.actionChanged === 'true';
|
|
@@ -364,7 +552,10 @@ if (invokedDirectly()) {
|
|
|
364
552
|
actionRequired,
|
|
365
553
|
driftOrigin: 'unknown',
|
|
366
554
|
resolvedAt: now,
|
|
367
|
-
|
|
555
|
+
// `null`, never a placeholder, for the same reason the detection this
|
|
556
|
+
// answers carries none.
|
|
557
|
+
ticket: args.ownerDirected ? null : args.ticket,
|
|
558
|
+
...(args.ownerDirected ? { mode: OWNER_DIRECTED } : {}),
|
|
368
559
|
point: args.point,
|
|
369
560
|
actionChanged: actionRequired,
|
|
370
561
|
note: args.note,
|
|
@@ -376,11 +567,181 @@ if (invokedDirectly()) {
|
|
|
376
567
|
process.stdout.write(
|
|
377
568
|
args.json
|
|
378
569
|
? `${JSON.stringify(record, null, 2)}\n`
|
|
379
|
-
: `revalidation-outcome: ${
|
|
570
|
+
: `revalidation-outcome: ${subject} at ${args.point} answers seq ${target.seq} — actionChanged ${args.actionChanged}\n`,
|
|
380
571
|
);
|
|
381
572
|
process.exit(0);
|
|
382
573
|
}
|
|
383
574
|
|
|
575
|
+
if (args.ownerDirected) {
|
|
576
|
+
// RP-94. BEFORE_PR for work that has no item: the same main-vs-branch drift
|
|
577
|
+
// comparison, with no tracker, no adapter and no claim record.
|
|
578
|
+
//
|
|
579
|
+
// 🔴 It sits ABOVE the queue-config resolution on purpose. "This mode reads
|
|
580
|
+
// no tracker" has to be a property of the control flow rather than a
|
|
581
|
+
// promise in a comment — nothing below this block runs, so a missing
|
|
582
|
+
// credential or an adapter name that does not resolve cannot reach it.
|
|
583
|
+
// Pinned by › "needs no tracker credentials: an adapter name that cannot
|
|
584
|
+
// resolve is never reached".
|
|
585
|
+
//
|
|
586
|
+
// The refusals below are what keep the mode from becoming a way around the
|
|
587
|
+
// claim chain, and every one is decided BEFORE anything is journalled: a
|
|
588
|
+
// refusal is not an answer, so it leaves no revalidation record behind.
|
|
589
|
+
//
|
|
590
|
+
// 🔴 Read FAIL-CLOSED. `readState` is the permissive reader and its own
|
|
591
|
+
// header forbids exactly this use — "a corrupt file there may be hiding a
|
|
592
|
+
// persisted stop" — so an unreadable run refuses here instead of reading as
|
|
593
|
+
// "this run declares nothing". The first version used `readState`, and a
|
|
594
|
+
// truncated `state.json` carrying a take-up was measured continuing.
|
|
595
|
+
let state = {};
|
|
596
|
+
if (runDir) {
|
|
597
|
+
try {
|
|
598
|
+
state = readStateForSelection(runDir);
|
|
599
|
+
} catch (error) {
|
|
600
|
+
refuse(
|
|
601
|
+
`--owner-directed refused: this run's state could not be read (${runDir}): ` +
|
|
602
|
+
`${error.message}. An unreadable run may be hiding a stop.`,
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// 🔴 The refusal this mode most needs, and the one its first version did
|
|
608
|
+
// not have. A ticketed BEFORE_PR that holds or comes back UNVERIFIABLE
|
|
609
|
+
// latches `revalidationHold`; re-running the same checkpoint here was
|
|
610
|
+
// MEASURED exiting 0 with nothing in the repository changed — the exact
|
|
611
|
+
// bypass RP-94 names. Neither other refusal can fire in that state, and
|
|
612
|
+
// that is structural rather than unlucky: `takeUps` is never populated on
|
|
613
|
+
// the default `plan-md` adapter at all, and the commonest hold is a
|
|
614
|
+
// MISSING claim record, which is precisely when the branch writes none.
|
|
615
|
+
// This one is adapter-independent because `recordRevalidationHold` is.
|
|
616
|
+
const hold = state.revalidationHold;
|
|
617
|
+
if (hold) {
|
|
618
|
+
// A malformed hold still refuses — it is a hold either way — but it must
|
|
619
|
+
// not print `undefined at undefined`, which reads as a broken command
|
|
620
|
+
// rather than as the stop it is.
|
|
621
|
+
const describe = (value, fallback) => (typeof value === 'string' ? value : fallback);
|
|
622
|
+
refuse(
|
|
623
|
+
`--owner-directed refused: this run carries an unresolved revalidation hold ` +
|
|
624
|
+
`(${describe(hold.ticket, 'an unnamed item')} at ` +
|
|
625
|
+
`${describe(hold.checkpoint, 'an unnamed checkpoint')}, ` +
|
|
626
|
+
`${describe(hold.result, 'result unrecorded')}, detection ` +
|
|
627
|
+
`${describe(hold.detectionId, 'unrecorded')}). ` +
|
|
628
|
+
'Resolve it with `revalidate.mjs outcome`; re-running the checkpoint in the other ' +
|
|
629
|
+
'mode is not a resolution, it is the bypass this mode refuses.',
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const takeUps = state.takeUps;
|
|
634
|
+
// A present-but-unreadable take-up record is a refusal, not an empty one:
|
|
635
|
+
// `Object.keys` answers `[]` for a number and for a list, which would turn
|
|
636
|
+
// "this cannot be read" into "there is nothing here".
|
|
637
|
+
if (
|
|
638
|
+
takeUps !== undefined &&
|
|
639
|
+
(typeof takeUps !== 'object' || takeUps === null || Array.isArray(takeUps))
|
|
640
|
+
) {
|
|
641
|
+
refuse(
|
|
642
|
+
`--owner-directed refused: this run's take-up record is not readable ` +
|
|
643
|
+
`(takeUps is ${Array.isArray(takeUps) ? 'a list' : typeof takeUps}, expected an object).`,
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
const takenUp = Object.keys(takeUps ?? {});
|
|
647
|
+
if (takenUp.length > 0) {
|
|
648
|
+
refuse(
|
|
649
|
+
`--owner-directed refused: this run already declares a take-up (${takenUp.join(', ')}). ` +
|
|
650
|
+
'A run holding an item revalidates with --ticket; owner-directed is for work with none.',
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
let mergeBase;
|
|
655
|
+
try {
|
|
656
|
+
mergeBase = git(['merge-base', args.base, 'HEAD']).trim();
|
|
657
|
+
} catch (error) {
|
|
658
|
+
refuse(
|
|
659
|
+
`--base ${args.base} is not a revision this checkout can compare against: ${error.message}`,
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Any claim record this branch TOUCHES, in either direction.
|
|
664
|
+
//
|
|
665
|
+
// 🔴 `--no-renames`, and no `--diff-filter`, because both narrower forms
|
|
666
|
+
// were measured letting a claim record through. `--diff-filter=AM` reports
|
|
667
|
+
// NOTHING for `git mv .rig/claims/RP-1.json .rig/claims/RP-2.json` — git
|
|
668
|
+
// calls it `R100` — so a branch that demonstrably ends up carrying a claim
|
|
669
|
+
// record passed the check. And a branch that DELETES its claim makes the
|
|
670
|
+
// ticketed call `UNVERIFIABLE`, so excluding `D` left the deletion on the
|
|
671
|
+
// bypass path rather than out of scope. `--no-renames` splits a rename back
|
|
672
|
+
// into its delete and its add, which is what puts both halves in front of
|
|
673
|
+
// the filter. Match the CLASS — "this branch touched the claim store" —
|
|
674
|
+
// rather than enumerating the statuses that class can wear.
|
|
675
|
+
const claimsTouched = pathsOf(
|
|
676
|
+
git(['diff', '--name-only', '--no-renames', '-z', mergeBase, 'HEAD']),
|
|
677
|
+
).filter((path) => CLAIM_RECORD.test(path));
|
|
678
|
+
if (claimsTouched.length > 0) {
|
|
679
|
+
refuse(
|
|
680
|
+
`--owner-directed refused: this branch touches tracked claim records ` +
|
|
681
|
+
`(${claimsTouched.join(', ')}). A branch that writes, moves or removes a claim is ` +
|
|
682
|
+
"an item's take-up; revalidate it with --ticket.",
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const branchPaths = pathsOf(git(['diff', '--name-only', '-z', mergeBase, 'HEAD']));
|
|
687
|
+
const mainPaths = pathsOf(git(['diff', '--name-only', '-z', mergeBase, args.base]));
|
|
688
|
+
const cited = [...new Set([...branchPaths, ...citedByPremises(runDir)])];
|
|
689
|
+
const mainChanged = mainPaths.filter((path) => cited.includes(path));
|
|
690
|
+
const result = ownerDirectedResult({
|
|
691
|
+
point: args.point,
|
|
692
|
+
base: args.base,
|
|
693
|
+
mergeBase,
|
|
694
|
+
cited,
|
|
695
|
+
changed: mainChanged,
|
|
696
|
+
now: new Date().toISOString(),
|
|
697
|
+
runDeclared: Boolean(runDir),
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
if (runDir) {
|
|
701
|
+
recordEvent({ runDir, kind: 'revalidation', data: result, now: result.observedAt });
|
|
702
|
+
// 🔴 No run-level revalidation hold is recorded here, and the reason is
|
|
703
|
+
// NOT the one first written down. That said "a hold written here could
|
|
704
|
+
// never be cleared" — which stopped being true the moment `outcome
|
|
705
|
+
// --owner-directed` computed the very id `clearRevalidationHold` matches
|
|
706
|
+
// on. The real reason is narrower: `recordRevalidationHold` requires a
|
|
707
|
+
// string ticket, and this mode has none to give it without inventing
|
|
708
|
+
// one, which is the thing the whole mode refuses to do.
|
|
709
|
+
//
|
|
710
|
+
// ⚠ **State the asymmetry rather than let a reader assume symmetry.** A
|
|
711
|
+
// ticketed hold has TWO stops — the exit code, and a latch that
|
|
712
|
+
// `queue/index.mjs` and `unresolvedBlockingDetectionOf` both read (the
|
|
713
|
+
// latter also requires a string ticket, so it skips this one). An
|
|
714
|
+
// owner-directed hold has ONE: the exit code below. A caller who ignores
|
|
715
|
+
// it is not stopped a second time. Widening the latch to a ticketless
|
|
716
|
+
// hold is a run-state schema change — Tier 2, and not this hotfix's.
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (args.json) {
|
|
720
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
721
|
+
} else {
|
|
722
|
+
const detail = result.source.length > 0 ? ` — ${result.source.join(', ')}` : '';
|
|
723
|
+
process.stdout.write(
|
|
724
|
+
`revalidate ${args.point}: ${OWNER_DIRECTED} (no item) ${result.action}${detail}\n`,
|
|
725
|
+
);
|
|
726
|
+
if (!runDir) {
|
|
727
|
+
// Loud, on the normal path, not only in the JSON: a run this command
|
|
728
|
+
// could not inspect must not read as a run it inspected and cleared.
|
|
729
|
+
process.stdout.write(
|
|
730
|
+
' ⚠ no RIG_RUN_DIR: the revalidation-hold and take-up refusals were NOT checked,\n' +
|
|
731
|
+
' and nothing was journalled. This is not evidence that neither exists.\n',
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
if (result.action === 'hold') {
|
|
735
|
+
process.stdout.write(
|
|
736
|
+
' re-read the default branch on those paths before opening or updating the PR,\n' +
|
|
737
|
+
' then record what the re-read concluded:\n' +
|
|
738
|
+
` node .claude/scripts/revalidate.mjs outcome --point ${args.point} --owner-directed --action-changed <true | false>\n`,
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
process.exit(result.action === 'hold' ? 2 : 0);
|
|
743
|
+
}
|
|
744
|
+
|
|
384
745
|
const projectRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
385
746
|
const configPath = args.config ?? join(projectRoot, '.claude', 'queue.json');
|
|
386
747
|
// 🔴 The queue CONFIG, not the tracker behind it. `readAdapter` covers every
|
|
@@ -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
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
@@ -57,13 +57,66 @@ blockers.
|
|
|
57
57
|
confidently-wrong reviews. Everything below is scoped to this diff.
|
|
58
58
|
|
|
59
59
|
Then, on the fetched ref, ask whether the branch is still the branch the run
|
|
60
|
-
took up
|
|
60
|
+
took up. **Two paths, and you state which one — the command infers neither.**
|
|
61
|
+
A branch that is a queue item's take-up:
|
|
61
62
|
|
|
62
63
|
```sh
|
|
63
64
|
node .claude/scripts/revalidate.mjs --point BEFORE_PR --ticket <item-id> --base origin/<default>
|
|
64
65
|
```
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
Owner-directed work or a hotfix that has **no item** — the case step 4 below
|
|
68
|
+
already tells you to declare to the reviewers:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
node .claude/scripts/revalidate.mjs --point BEFORE_PR --owner-directed --base origin/<default>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
🔴 **The second path is not a lighter checkpoint, and it is never a skip.**
|
|
75
|
+
It runs the same `main:<path>` comparison and holds on the same exit 2; what
|
|
76
|
+
it drops is the claim comparison, because work with no item has no claim to
|
|
77
|
+
compare. It resolves no queue config, so it reaches no tracker and needs no
|
|
78
|
+
credentials. Passing both flags, or neither, is exit 1.
|
|
79
|
+
|
|
80
|
+
**Four refusals** — exit 1, nothing journalled — keep it from being the way
|
|
81
|
+
around a claim or revalidation failure. It is refused when this run carries
|
|
82
|
+
an **unresolved revalidation hold** (what the ticketed path writes when it
|
|
83
|
+
holds or answers `UNVERIFIABLE`), when this run **declares a take-up**, when
|
|
84
|
+
the branch **touches a tracked `.rig/claims/*.json`** in any direction —
|
|
85
|
+
added, modified, removed or renamed — and at `BEFORE_CLOSE`. So re-running a
|
|
86
|
+
held ticketed call in this mode does not get past it: resolve the hold with
|
|
87
|
+
`outcome` instead.
|
|
88
|
+
|
|
89
|
+
⚠ **What those refusals do not cover**, because a governance mode is trusted
|
|
90
|
+
exactly as far as it is described:
|
|
91
|
+
|
|
92
|
+
- **With no `RIG_RUN_DIR` there is no run state to read**, so the hold and
|
|
93
|
+
take-up refusals cannot fire — and nothing is journalled. The command says
|
|
94
|
+
so on stdout and in `evidence.runState`; it is not evidence that neither
|
|
95
|
+
exists. An attended gate run is exactly this shape, so read that line.
|
|
96
|
+
- The claim refusal reads the **branch diff**, so a claim record already on
|
|
97
|
+
the default branch, or written and not committed, is not seen.
|
|
98
|
+
- **`--base` is the sole authority for the verdict here**, the claim
|
|
99
|
+
comparison that would otherwise survive a wrong base being absent. Pass
|
|
100
|
+
the fetched `origin/<default>`, not a local copy and not `HEAD`.
|
|
101
|
+
- Nothing can prove an item does not exist. The rest is your word, recorded
|
|
102
|
+
as such, with `ticket: null` and no invented id.
|
|
103
|
+
|
|
104
|
+
Pinned in the generator's `test/template/owner-directed-revalidation.test.ts`
|
|
105
|
+
(absent in a generated rig) › "refuses when this run carries an unresolved
|
|
106
|
+
revalidation hold", › "refuses when the declared run already carries a
|
|
107
|
+
take-up", › "refuses when the branch RENAMES a claim record — the case
|
|
108
|
+
--diff-filter=AM could not see" and › "says out loud that an undeclared run
|
|
109
|
+
checked neither the hold nor the take-up".
|
|
110
|
+
|
|
111
|
+
**Exit 2 here is a HOLD with the same shape as the ticketed one**, and the
|
|
112
|
+
same two-step remedy: re-read the default branch on each named path, then
|
|
113
|
+
record what the re-read concluded — `node .claude/scripts/revalidate.mjs
|
|
114
|
+
outcome --point BEFORE_PR --owner-directed --action-changed <true | false>
|
|
115
|
+
--note '…'` — and come back through step 0. The owner-directed detection
|
|
116
|
+
carries no ticket, so `--owner-directed` is how the outcome addresses it;
|
|
117
|
+
the ticketed `--ticket <item-id>` form below cannot match it and is refused.
|
|
118
|
+
|
|
119
|
+
The ticketed path runs the existing revalidation chain against the tracked, versioned
|
|
67
120
|
`.rig/claims/<item-id>.json`: the content-blind `scope` fingerprint set is
|
|
68
121
|
authoritative here, while `takeUps` / `updatedAt` remain evidence only. It
|
|
69
122
|
also names what the default branch changed since this branch forked on paths
|
|
@@ -71,14 +124,18 @@ blockers.
|
|
|
71
124
|
journals one `revalidation` event at `point: BEFORE_PR`; **exit code 2 is a HOLD**, with one blocker per named source: re-read the item, or the default
|
|
72
125
|
branch on that path, record what the re-read concluded —
|
|
73
126
|
`node .claude/scripts/revalidate.mjs outcome --point BEFORE_PR --ticket <item-id> --action-changed <true | false> --note '…'`
|
|
74
|
-
— and come back through step 0.
|
|
75
|
-
|
|
127
|
+
— and come back through step 0. **That `--ticket` form is this path's, not
|
|
128
|
+
both paths'** — the owner-directed detection carries no ticket for it to
|
|
129
|
+
name, and its own `--owner-directed` outcome is written out above. A hold
|
|
130
|
+
with no outcome, in either mode, is counted by the report as a re-read the
|
|
131
|
+
run skipped. A missing, untracked, unreadable or
|
|
76
132
|
unsupported claim is `UNVERIFIABLE`, exits 2, and stops automatic progress;
|
|
77
133
|
so is a tracker whose adapter the command cannot READ, which means the
|
|
78
134
|
question was never put rather than that the claim record is unreadable.
|
|
79
135
|
Neither is ever read as a pass. Exit 1 is the command refusing (unknown
|
|
80
|
-
point,
|
|
81
|
-
not resolve
|
|
136
|
+
point, neither mode or both, a base that is not a revision, or a queue config
|
|
137
|
+
that does not resolve — plus, on the owner-directed path, the four refusals
|
|
138
|
+
above): fix the call or the config — the message says which. Its limits are its own header's; the
|
|
82
139
|
cited-path set is a labelled assumption, not a recorded fact. Pinned in the
|
|
83
140
|
generator's `test/template/revalidate.test.ts` (absent in a generated rig) ›
|
|
84
141
|
"continues when only updatedAt moved and still reports the marker evidence"
|
|
@@ -182,9 +239,13 @@ blockers.
|
|
|
182
239
|
**Whatever you launch, pass it the text of the queue item this branch
|
|
183
240
|
implements.** A reviewer given only a diff cannot check the change against
|
|
184
241
|
what was asked: a cold context has no way to know, and reconstructing it from
|
|
185
|
-
the PR description would mean trusting the run under review.
|
|
186
|
-
item — owner-directed work, a hotfix —
|
|
187
|
-
|
|
242
|
+
the PR description would mean trusting the run under review. When there is
|
|
243
|
+
no item — owner-directed work, a hotfix — launch every reviewer with the
|
|
244
|
+
words **`no item — owner-directed`** instead. That skips the item-contract
|
|
245
|
+
check, openly, and **nothing else**: the checks, the routing, the security,
|
|
246
|
+
code and prose/governance reviews, the coverage check and the DoD all still
|
|
247
|
+
run. It is one check narrower than a ticketed fan-out, not a cheaper gate,
|
|
248
|
+
and the same words are what step 1's `--owner-directed` call records.
|
|
188
249
|
|
|
189
250
|
🔴 **The triggers below are lane-independent and may only ADD.** They read
|
|
190
251
|
*what the code does*; the router reads *paths*, and a path cannot say that a
|