create-agent-rig 0.9.0 → 0.9.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/CHANGELOG.md +47 -0
- package/README.md +8 -2
- package/package.json +1 -1
- package/packages/cli/dist/commands/init.js +43 -7
- package/packages/cli/dist/commands/memory.js +63 -4
- package/packages/cli/dist/commands/upgrade.js +22 -4
- package/packages/cli/dist/index.js +6 -3
- package/packages/cli/dist/lib/manifest.js +23 -5
- package/templates/agent-os/universal/.claude/scripts/queue/core.mjs +43 -0
- package/templates/agent-os/universal/.claude/scripts/queue/jira.mjs +18 -1
- package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +269 -32
- package/templates/hash-history.json +66 -21
- package/templates/release-ledger.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,53 @@ second recorded departure; its own entry states the direction and the reason,
|
|
|
14
14
|
and this paragraph deliberately does not restate them — a numbering rule with
|
|
15
15
|
two copies of its exceptions is the shape 0.8.0 exists to remove.
|
|
16
16
|
|
|
17
|
+
## 0.9.1
|
|
18
|
+
|
|
19
|
+
**A patch, numbered by the owner.** Every entry corrects existing behaviour;
|
|
20
|
+
two of them (RP-182, RP-59) do it by recording or reporting something new.
|
|
21
|
+
Nothing is added to what `create`, `init` or `upgrade` install beyond the
|
|
22
|
+
corrected files, the contract version stays `1.0`, and the application
|
|
23
|
+
skeletons remain deprecated and scheduled for removal in 0.10.0 exactly as
|
|
24
|
+
0.9.0 announced.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **`init` records the files it kept.** When `init` finds a payload path
|
|
29
|
+
already present and leaves it alone, the manifest now records that file's
|
|
30
|
+
hash under `kept`, so a later `upgrade` can say whether the file is
|
|
31
|
+
unchanged or edited since `init` found it instead of reporting it with no
|
|
32
|
+
history (RP-182). A manifest with nothing kept is written exactly as
|
|
33
|
+
before.
|
|
34
|
+
- **`memory load` passes Memory a deadline of its own.** A `load` that names
|
|
35
|
+
no `--timeout-ms` gets `--timeout-ms 45000` appended, below the rig's 60 s
|
|
36
|
+
kill. A well-formed `--timeout-ms <value>` pair raises that kill to the
|
|
37
|
+
value plus 15 s when that is above 60 s, capped at Node's timer limit of
|
|
38
|
+
2 147 483 647 ms; any other spelling passes through unchanged. `doctor`'s
|
|
39
|
+
arguments are untouched. The README example adds `--cwd .`, which the Memory
|
|
40
|
+
owner names as part of Memory's own contract (RP-183).
|
|
41
|
+
- **`reconcile-external-prs.mjs` works on a `gh` that does not serve
|
|
42
|
+
`authorAssociation`** from `gh pr list`: the association is fetched
|
|
43
|
+
separately and kept only when it answers for the same pull request, and a
|
|
44
|
+
failed lookup leaves those pull requests untrusted rather than failing the
|
|
45
|
+
sweep. A failure now names the one cause the evidence shows, with terminal
|
|
46
|
+
control sequences and credential shapes removed from the detail (RP-99).
|
|
47
|
+
- **The queue reports a Blocks link the ticket body says was removed but the
|
|
48
|
+
tracker still carries** — a body line of the form
|
|
49
|
+
`The Blocks link A -> B is removed` — as a `link-contradicted-by-body`
|
|
50
|
+
hygiene finding (RP-59).
|
|
51
|
+
- **A Jira triage proposal with a long `change` is filed, not refused by the
|
|
52
|
+
tracker.** Jira rejects a summary over 255 characters; the summary is now
|
|
53
|
+
cut to fit and the full text stays in the description (RP-121).
|
|
54
|
+
|
|
55
|
+
### Generator CI (not a rig-facing change)
|
|
56
|
+
|
|
57
|
+
Test-fixture cleanup goes through one bounded-retry helper, and an audit holds
|
|
58
|
+
every other recursive removal and every in-repository fixture to a written
|
|
59
|
+
reason. The four child-process time bounds are measured inside the child, and
|
|
60
|
+
the package-manager CLI-start cases carry their own measured budget. The e2e
|
|
61
|
+
installs run in a vitest group of their own, so they no longer compete with
|
|
62
|
+
the template tests on the Windows full-suite runner (RP-158).
|
|
63
|
+
|
|
17
64
|
## 0.9.0
|
|
18
65
|
|
|
19
66
|
**The harness ↔ Memory boundary is executable, and the rig is a consumer of
|
package/README.md
CHANGED
|
@@ -65,7 +65,13 @@ it does not. Each conflict names the file, why it was kept, and the path to the
|
|
|
65
65
|
new version so you can diff it yourself.
|
|
66
66
|
|
|
67
67
|
How it knows: `create` and `init` write `.claude/.rig-manifest.json` — the rig
|
|
68
|
-
version plus a hash per installed file.
|
|
68
|
+
version plus a hash per installed file. A file `init` found already in place and
|
|
69
|
+
left alone is recorded separately, under `kept`, with the hash of the bytes it
|
|
70
|
+
found, and never as the rig's (`packages/cli/test/init.test.ts` › "records what
|
|
71
|
+
it kept, with the sha256 of the bytes actually on disk — never in `files`"). An
|
|
72
|
+
`upgrade` conflict on such a file says it was kept by init and whether it was
|
|
73
|
+
edited since (`packages/cli/test/upgrade.test.ts` › "says "edited since"
|
|
74
|
+
instead, once the disk sha no longer matches what init recorded"). **Commit it**; without it in the
|
|
69
75
|
repository the command is blind on CI and on a colleague's machine. Rigs
|
|
70
76
|
installed before 0.4.0 have no manifest, so the package also carries the hashes
|
|
71
77
|
of every release whose published commit is on record (0.2.0 onward — 0.1.0's
|
|
@@ -112,7 +118,7 @@ Both bins answer the same handshake, and the rig consumes Memory only through it
|
|
|
112
118
|
```sh
|
|
113
119
|
npx create-agent-rig@latest --version --json # {"schemaVersion":1,"name":"create-agent-rig","version":"…","contractVersion":"1.0"}
|
|
114
120
|
npx create-agent-rig@latest memory doctor --json # handshake first, then Memory's doctor, answer passed through unchanged
|
|
115
|
-
npx create-agent-rig@latest memory load --json
|
|
121
|
+
npx create-agent-rig@latest memory load --json --cwd . # same, for load (`--cwd`: Memory's own contract, per its owner — RP-183); arguments pass to Memory verbatim, plus `--timeout-ms 45000` when you name none
|
|
116
122
|
```
|
|
117
123
|
|
|
118
124
|
`memory` reads the manifest above, runs Memory's `--version --json`, and only
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { access, lstat, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { settingsForInstalledHooks } from '../lib/init-settings.js';
|
|
4
4
|
import { mapConcurrent } from '../lib/copy-tree.js';
|
|
@@ -158,17 +158,40 @@ export async function initProject(repoDir, options) {
|
|
|
158
158
|
const written = actions.filter(({ verdict }) => verdict === 'written').map(({ rel }) => rel);
|
|
159
159
|
const skipped = actions.filter(({ verdict }) => verdict === 'skipped').map(({ rel }) => rel);
|
|
160
160
|
if (!options.dryRun)
|
|
161
|
-
await recordInstall(repoDir, written, contents);
|
|
161
|
+
await recordInstall(repoDir, written, skipped, contents);
|
|
162
162
|
return { written, skipped, plannedCount };
|
|
163
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* The text of a regular file, or `null` for anything else at that path — a
|
|
166
|
+
* directory, a symlink, a file this process cannot read. `kept` records only
|
|
167
|
+
* bytes that are the file itself: hashing through a link would put the hash of
|
|
168
|
+
* something outside the repository into a committed manifest, and a read that
|
|
169
|
+
* throws here would abort the install after every other file was written.
|
|
170
|
+
*/
|
|
171
|
+
async function readRegularFile(abs) {
|
|
172
|
+
try {
|
|
173
|
+
if (!(await lstat(abs)).isFile())
|
|
174
|
+
return null;
|
|
175
|
+
return await readFile(abs, 'utf8');
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
164
181
|
/**
|
|
165
182
|
* Record what was installed, so a later `upgrade` can tell a file it wrote
|
|
166
183
|
* from a file the user owns.
|
|
167
184
|
*
|
|
168
|
-
* Only files actually **written** are recorded
|
|
169
|
-
* somebody else's — claiming it
|
|
170
|
-
* user's own document with the rig's.
|
|
171
|
-
*
|
|
185
|
+
* Only files actually **written** are recorded in `files`. A file `init` kept
|
|
186
|
+
* is somebody else's — claiming it there would let the next upgrade replace a
|
|
187
|
+
* user's own document with the rig's. It is recorded in `kept` instead, with
|
|
188
|
+
* the sha256 of the bytes found on disk (RP-182): not a claim of ownership, a
|
|
189
|
+
* record that the rig saw the file and left it — so the next upgrade can say
|
|
190
|
+
* "kept by init, unchanged since" or "edited since" rather than only "not a
|
|
191
|
+
* version this rig ever released". A path already in `files` is never moved to
|
|
192
|
+
* `kept` by a later run that skips it: the rig wrote those bytes. Earlier
|
|
193
|
+
* entries are preserved: a re-run writes nothing and must not therefore
|
|
194
|
+
* un-remember everything.
|
|
172
195
|
*
|
|
173
196
|
* 🔴 **`kind`, `project` and `stacks` are preserved, not rewritten.** Reached
|
|
174
197
|
* inside a rig `create` produced, this used to stamp `kind: 'init'`,
|
|
@@ -196,12 +219,24 @@ export async function initProject(repoDir, options) {
|
|
|
196
219
|
* leaves is a `create` rig whose `CLAUDE.md` was deleted, and this function is
|
|
197
220
|
* what makes that case safe.
|
|
198
221
|
*/
|
|
199
|
-
async function recordInstall(repoDir, written, contents) {
|
|
222
|
+
async function recordInstall(repoDir, written, skipped, contents) {
|
|
200
223
|
const previous = await readManifest(repoDir);
|
|
201
224
|
const name = projectNameFor(repoDir);
|
|
202
225
|
const files = { ...(previous?.files ?? {}) };
|
|
203
226
|
for (const rel of written)
|
|
204
227
|
files[rel] = sha256(contents.get(rel) ?? '');
|
|
228
|
+
const kept = { ...(previous?.kept ?? {}) };
|
|
229
|
+
for (const rel of written)
|
|
230
|
+
delete kept[rel];
|
|
231
|
+
for (const rel of skipped) {
|
|
232
|
+
if (files[rel] !== undefined)
|
|
233
|
+
continue; // the rig wrote it once; still its bytes to vouch for
|
|
234
|
+
const found = await readRegularFile(path.join(repoDir, rel));
|
|
235
|
+
if (found === null)
|
|
236
|
+
delete kept[rel];
|
|
237
|
+
else
|
|
238
|
+
kept[rel] = sha256(found);
|
|
239
|
+
}
|
|
205
240
|
const manifest = {
|
|
206
241
|
version: await packageVersion(),
|
|
207
242
|
kind: previous?.kind ?? 'init',
|
|
@@ -211,6 +246,7 @@ async function recordInstall(repoDir, written, contents) {
|
|
|
211
246
|
project: previous?.project ?? { name, scope: name, region: '' },
|
|
212
247
|
stacks: previous?.stacks ?? [],
|
|
213
248
|
files,
|
|
249
|
+
...(Object.keys(kept).length > 0 ? { kept } : {}),
|
|
214
250
|
};
|
|
215
251
|
await writeManifest(repoDir, manifest);
|
|
216
252
|
}
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
// resolves the executable from the machine subsystem manifest `setup` wrote
|
|
4
4
|
// (RP-147), performs `--version --json` first, refuses a foreign contract
|
|
5
5
|
// major with exit 4 before doctor/load ever run, and otherwise hands the verb
|
|
6
|
-
// and its arguments to Memory
|
|
6
|
+
// and its arguments to Memory and returns Memory's answer unchanged. The one
|
|
7
|
+
// thing it adds to the arguments: a `load` that names no `--timeout-ms` gets
|
|
8
|
+
// Memory's default internal deadline appended (`deadlinesFor`, RP-183);
|
|
9
|
+
// everything the caller wrote passes through verbatim, and `doctor` is never
|
|
10
|
+
// touched.
|
|
7
11
|
//
|
|
8
12
|
// What it never does, by construction: import Memory code, read Memory's
|
|
9
13
|
// storage tree, or reinterpret a doctor/load payload — the only Memory bytes
|
|
@@ -22,8 +26,62 @@
|
|
|
22
26
|
import { execFileRunner } from './setup.js';
|
|
23
27
|
import { MEMORY_CONTRACT_MAJOR, SubsystemsError, handshake, readSubsystemsManifest, subsystemsManifestPath, } from '../lib/subsystems.js';
|
|
24
28
|
const MEMORY_VERBS = ['doctor', 'load'];
|
|
25
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* The outer deadline: the rig kills the Memory child after this many ms. It is
|
|
31
|
+
* the consumer's bound, and it should not be the one that fires first — a kill
|
|
32
|
+
* leaves no JSON answer behind. So `load` always carries an INTERNAL deadline
|
|
33
|
+
* as well (RP-183): Memory's own `--timeout-ms`, which per the Memory owner
|
|
34
|
+
* (RP-183, Jira) it answers with a typed result instead of being killed.
|
|
35
|
+
* `doctor` accepts no such flag per the same source — its args are never
|
|
36
|
+
* touched. Pinned in packages/cli/test/memory.test.ts, "RP-183".
|
|
37
|
+
*/
|
|
26
38
|
const VERB_TIMEOUT_MS = 60_000;
|
|
39
|
+
/** Memory's default internal deadline when the caller names none. */
|
|
40
|
+
const DEFAULT_MEMORY_TIMEOUT_MS = 45_000;
|
|
41
|
+
/**
|
|
42
|
+
* Headroom the outer deadline keeps above an explicit internal one. Sized per
|
|
43
|
+
* the Memory owner (RP-183, Jira): Memory's Windows cleanup may return up to
|
|
44
|
+
* 10 s after its own deadline fired; the rest is spawn overhead. The rig does
|
|
45
|
+
* not measure that lag itself.
|
|
46
|
+
*/
|
|
47
|
+
const OUTER_MARGIN_MS = 15_000;
|
|
48
|
+
/**
|
|
49
|
+
* The largest delay `execFile`'s timer can hold: Node clamps a larger
|
|
50
|
+
* `setTimeout` to ONE millisecond, which would make the outer kill fire first
|
|
51
|
+
* — the inversion this whole helper exists to prevent. A caller who asks for
|
|
52
|
+
* more than ~24.8 days gets this ceiling as the outer deadline.
|
|
53
|
+
*/
|
|
54
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
55
|
+
const TIMEOUT_FLAG = '--timeout-ms';
|
|
56
|
+
/** Memory's own grammar for the value: a positive integer, milliseconds. */
|
|
57
|
+
const TIMEOUT_VALUE = /^[1-9][0-9]*$/;
|
|
58
|
+
/**
|
|
59
|
+
* The verb's argument list and outer deadline. `load` with no `--timeout-ms`
|
|
60
|
+
* gets the default appended; a caller-owned flag — any spelling — passes
|
|
61
|
+
* through verbatim, and only a well-formed `--timeout-ms <value>` pair can
|
|
62
|
+
* raise the outer deadline above it. Memory owns the refusal of a malformed
|
|
63
|
+
* value, so the rig neither corrects nor drops one.
|
|
64
|
+
*/
|
|
65
|
+
const deadlinesFor = (verb, args) => {
|
|
66
|
+
if (verb !== 'load')
|
|
67
|
+
return { args: [...args], timeoutMs: VERB_TIMEOUT_MS };
|
|
68
|
+
const at = args.findIndex((arg) => arg === TIMEOUT_FLAG || arg.startsWith(`${TIMEOUT_FLAG}=`));
|
|
69
|
+
if (at === -1) {
|
|
70
|
+
return {
|
|
71
|
+
args: [...args, TIMEOUT_FLAG, String(DEFAULT_MEMORY_TIMEOUT_MS)],
|
|
72
|
+
timeoutMs: VERB_TIMEOUT_MS,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const value = args[at] === TIMEOUT_FLAG ? args[at + 1] : undefined;
|
|
76
|
+
const internal = value !== undefined && TIMEOUT_VALUE.test(value) ? Number(value) : null;
|
|
77
|
+
// A digit string too long for a safe integer — or for a double at all —
|
|
78
|
+
// still lands on the ceiling: `Number` yields a huge float or Infinity, and
|
|
79
|
+
// `Math.min` with the ceiling holds either.
|
|
80
|
+
const timeoutMs = internal !== null
|
|
81
|
+
? Math.min(Math.max(VERB_TIMEOUT_MS, internal + OUTER_MARGIN_MS), MAX_TIMER_MS)
|
|
82
|
+
: VERB_TIMEOUT_MS;
|
|
83
|
+
return { args: [...args], timeoutMs };
|
|
84
|
+
};
|
|
27
85
|
const jsonLine = (payload) => `${JSON.stringify(payload)}\n`;
|
|
28
86
|
const isVerb = (verb) => MEMORY_VERBS.includes(verb);
|
|
29
87
|
export async function runMemory(options) {
|
|
@@ -106,8 +164,9 @@ export async function runMemory(options) {
|
|
|
106
164
|
stderr: `memory: the handshake did not answer as the manifest promised (${result.reason}); ${options.verb} was not run\n`,
|
|
107
165
|
};
|
|
108
166
|
const [command, script] = entry.invocation;
|
|
109
|
-
const
|
|
110
|
-
|
|
167
|
+
const deadlines = deadlinesFor(options.verb, options.args);
|
|
168
|
+
const answer = await run(command, [script, options.verb, ...deadlines.args], {
|
|
169
|
+
timeoutMs: deadlines.timeoutMs,
|
|
111
170
|
});
|
|
112
171
|
if (answer.spawnError)
|
|
113
172
|
return {
|
|
@@ -315,17 +315,34 @@ export async function planUpgrade(repoDir, options = {}) {
|
|
|
315
315
|
nextFiles[file.rel] = recorded;
|
|
316
316
|
}
|
|
317
317
|
else {
|
|
318
|
+
// A path `init` found and left alone carries its provenance in `kept`
|
|
319
|
+
// (RP-182): the reason can then say what happened to the file since,
|
|
320
|
+
// instead of only that no release ever shipped these bytes. It names no
|
|
321
|
+
// version — `manifest.version` is rewritten by every upgrade, so it is
|
|
322
|
+
// not the version of the init that kept the file.
|
|
323
|
+
const kept = recorded === undefined ? manifest?.kept?.[file.rel] : undefined;
|
|
324
|
+
const keptReason = (since) => `kept by init (already here, not the rig's bytes), ${since} since — treated as yours`;
|
|
318
325
|
actions.push({
|
|
319
326
|
rel: file.rel,
|
|
320
327
|
verdict: 'conflict',
|
|
321
|
-
reason:
|
|
322
|
-
?
|
|
323
|
-
:
|
|
328
|
+
reason: kept !== undefined
|
|
329
|
+
? keptReason(sha256(current) === kept ? 'unchanged' : 'edited')
|
|
330
|
+
: recorded === undefined
|
|
331
|
+
? 'not a version this rig ever released — treated as yours'
|
|
332
|
+
: 'edited since it was installed',
|
|
324
333
|
templatePath: file.source,
|
|
325
334
|
});
|
|
326
|
-
// deliberately NOT recorded
|
|
335
|
+
// deliberately NOT recorded in `files`: the rig does not own these bytes
|
|
327
336
|
}
|
|
328
337
|
}
|
|
338
|
+
// `kept` travels forward untouched, minus every path the plan now vouches
|
|
339
|
+
// for in `files` — a kept file that turned out to be a released version, or
|
|
340
|
+
// is byte-identical to this release, has become the rig's to manage.
|
|
341
|
+
const nextKept = {};
|
|
342
|
+
for (const [rel, hash] of Object.entries(manifest?.kept ?? {})) {
|
|
343
|
+
if (nextFiles[rel] === undefined)
|
|
344
|
+
nextKept[rel] = hash;
|
|
345
|
+
}
|
|
329
346
|
// With no manifest, "there is a rig here" has to be *recognised*, not
|
|
330
347
|
// assumed from a file existing: `CLAUDE.md` and `.claude/settings.json` are
|
|
331
348
|
// in the install set and in nearly every repository ever opened by an agent.
|
|
@@ -352,6 +369,7 @@ export async function planUpgrade(repoDir, options = {}) {
|
|
|
352
369
|
project,
|
|
353
370
|
stacks: [...stacks],
|
|
354
371
|
files: nextFiles,
|
|
372
|
+
...(Object.keys(nextKept).length > 0 ? { kept: nextKept } : {}),
|
|
355
373
|
},
|
|
356
374
|
};
|
|
357
375
|
}
|
|
@@ -48,8 +48,10 @@ Also: create-agent-rig setup --memory-root <checkout> [--memory-ref <sha>] [--dr
|
|
|
48
48
|
Also: create-agent-rig memory <doctor|load> [args…]
|
|
49
49
|
Run a Memory verb through the registered executable: the --version --json
|
|
50
50
|
handshake first (a foreign contract major exits 4 and the verb never runs),
|
|
51
|
-
then the verb and its arguments
|
|
52
|
-
|
|
51
|
+
then the verb and its arguments, Memory's answer passed through unchanged.
|
|
52
|
+
Your arguments go to Memory as written; a load that names no --timeout-ms
|
|
53
|
+
gets --timeout-ms 45000 appended (Memory's own deadline, kept under the rig's
|
|
54
|
+
60 s bound). No manifest answers unsupported/absent (exit 0); an invalid
|
|
53
55
|
invocation exits 2.`;
|
|
54
56
|
async function runSetup(rawArgs) {
|
|
55
57
|
let values;
|
|
@@ -318,7 +320,8 @@ async function main() {
|
|
|
318
320
|
}
|
|
319
321
|
if (process.argv[2] === 'memory') {
|
|
320
322
|
// The consumer path of the RP-19 handshake: manifest → `--version --json`
|
|
321
|
-
// → exit 4 on a foreign major → doctor/load passed through
|
|
323
|
+
// → exit 4 on a foreign major → doctor/load passed through (`load` gains a
|
|
324
|
+
// default `--timeout-ms` when the caller names none — commands/memory.ts).
|
|
322
325
|
const [verb = '', ...args] = process.argv.slice(3);
|
|
323
326
|
const result = await runMemory({ verb, args });
|
|
324
327
|
process.stdout.write(result.stdout);
|
|
@@ -91,20 +91,38 @@ export function parseManifest(raw) {
|
|
|
91
91
|
return null;
|
|
92
92
|
if (!isStringRecord(m.files))
|
|
93
93
|
return null;
|
|
94
|
+
// Present in a shape this reader does not accept voids the manifest, exactly
|
|
95
|
+
// as `files` does; absent is every manifest written before the field existed.
|
|
96
|
+
if (m.kept !== undefined && !isStringRecord(m.kept))
|
|
97
|
+
return null;
|
|
94
98
|
return {
|
|
95
99
|
version: m.version,
|
|
96
100
|
kind: m.kind,
|
|
97
101
|
project: { name: project.name, scope: project.scope, region: project.region },
|
|
98
102
|
stacks: [...m.stacks],
|
|
99
103
|
files: { ...m.files },
|
|
104
|
+
...(m.kept !== undefined ? { kept: { ...m.kept } } : {}),
|
|
100
105
|
};
|
|
101
106
|
}
|
|
102
|
-
|
|
107
|
+
const sortedRecord = (record) => {
|
|
108
|
+
const sorted = {};
|
|
109
|
+
for (const rel of Object.keys(record).sort())
|
|
110
|
+
sorted[rel] = record[rel];
|
|
111
|
+
return sorted;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Stable bytes: sorted paths, so a re-run produces no diff of its own. An
|
|
115
|
+
* empty `kept` is omitted, not written as `{}` — a manifest that kept nothing
|
|
116
|
+
* must serialise byte-identical to one written before the key existed.
|
|
117
|
+
*/
|
|
103
118
|
export function serializeManifest(manifest) {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
const { kept, ...rest } = manifest;
|
|
120
|
+
const body = {
|
|
121
|
+
...rest,
|
|
122
|
+
files: sortedRecord(manifest.files),
|
|
123
|
+
...(kept !== undefined && Object.keys(kept).length > 0 ? { kept: sortedRecord(kept) } : {}),
|
|
124
|
+
};
|
|
125
|
+
return `${JSON.stringify(body, null, 2)}\n`;
|
|
108
126
|
}
|
|
109
127
|
export async function readManifest(repoDir) {
|
|
110
128
|
try {
|
|
@@ -485,6 +485,18 @@ export const hygieneOf = (ticket, { owner = null } = {}) => {
|
|
|
485
485
|
};
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
+
const removed = removedDependencyIn(body, ticket);
|
|
489
|
+
if (removed) {
|
|
490
|
+
return {
|
|
491
|
+
kind: 'link-contradicted-by-body',
|
|
492
|
+
id: ticket.id,
|
|
493
|
+
why:
|
|
494
|
+
`body line ${removed.line} declares "The Blocks link ${removed.source} -> ` +
|
|
495
|
+
`${removed.target} is removed", but that dependency is still carried in ` +
|
|
496
|
+
`${removed.direction}. Reconcile the declaration and link before relying on either`,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
488
500
|
return null;
|
|
489
501
|
};
|
|
490
502
|
|
|
@@ -502,6 +514,37 @@ export const hygieneOf = (ticket, { owner = null } = {}) => {
|
|
|
502
514
|
*/
|
|
503
515
|
const BLOCKER_IN_BODY = /^[-*\t ]{0,4}(?:blocked by|depends on|blocker)[ \t:]{0,8}[#A-Za-z0-9]/im;
|
|
504
516
|
|
|
517
|
+
// See test/template/queue.test.ts (absent in a generated rig) ›
|
|
518
|
+
// "reports its carried outgoing Blocks link that its own body says was removed".
|
|
519
|
+
const REMOVAL_ID = '(?:[A-Z][A-Z0-9_]{0,31}-[0-9]{1,16}|#[0-9]{1,16})';
|
|
520
|
+
const REMOVED_DEPENDENCY = new RegExp(
|
|
521
|
+
`^[ \\t]{0,4}(?:[-*][ \\t]{1,4})?The[ \\t]{1,8}Blocks[ \\t]{1,8}link[ \\t]{1,8}` +
|
|
522
|
+
`(${REMOVAL_ID})[ \\t]{1,8}->[ \\t]{1,8}(${REMOVAL_ID})[ \\t]{1,8}is[ \\t]{1,8}removed` +
|
|
523
|
+
'(?=[ \\t]{0,8}(?:$|[.;(]))',
|
|
524
|
+
'i',
|
|
525
|
+
);
|
|
526
|
+
const dependencyId = (value) => String(value).replace(/^#/, '').toUpperCase();
|
|
527
|
+
const removedDependencyIn = (body, ticket) => {
|
|
528
|
+
const ownId = dependencyId(ticket.id);
|
|
529
|
+
const outgoingIds = new Set((ticket.blocks ?? []).map(dependencyId));
|
|
530
|
+
const incomingIds = new Set((ticket.blockedBy ?? []).map((link) => dependencyId(link.id)));
|
|
531
|
+
const lines = body.split(/\r?\n/);
|
|
532
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
533
|
+
const match = REMOVED_DEPENDENCY.exec(lines[index]);
|
|
534
|
+
if (!match) continue;
|
|
535
|
+
const [, source, target] = match;
|
|
536
|
+
const outgoing =
|
|
537
|
+
dependencyId(source) === ownId &&
|
|
538
|
+
outgoingIds.has(dependencyId(target));
|
|
539
|
+
const incoming =
|
|
540
|
+
dependencyId(target) === ownId &&
|
|
541
|
+
incomingIds.has(dependencyId(source));
|
|
542
|
+
if (outgoing || incoming)
|
|
543
|
+
return { source, target, line: index + 1, direction: outgoing ? 'blocks' : 'blockedBy' };
|
|
544
|
+
}
|
|
545
|
+
return null;
|
|
546
|
+
};
|
|
547
|
+
|
|
505
548
|
/** The item saying, in its own words, that it was broken into other items. */
|
|
506
549
|
const SPLIT_IN_BODY = /\b(?:split into|split up into|broken into|broken up into|superseded by|subtasks?:)/i;
|
|
507
550
|
|
|
@@ -680,6 +680,23 @@ export const escalate = async (ticket, diagnosis, { env = process.env } = {}) =>
|
|
|
680
680
|
return { ok: true };
|
|
681
681
|
};
|
|
682
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Jira refuses a summary longer than this, and creates nothing: `400
|
|
685
|
+
* {"errors":{"summary":"Summary can't exceed 255 characters."}}` (RP-121). The
|
|
686
|
+
* summary is therefore a prefix; the full `change` stays in the description,
|
|
687
|
+
* where the fingerprint lives too, so the dedupe does not depend on the cut.
|
|
688
|
+
* The cut never leaves half of a surrogate pair behind. Pinned in
|
|
689
|
+
* `test/template/queue-jira.test.ts` › "bounds the POSTed summary so a
|
|
690
|
+
* 2186-character change still files, keeping the full text in the body".
|
|
691
|
+
*/
|
|
692
|
+
const MAX_SUMMARY_LENGTH = 255;
|
|
693
|
+
|
|
694
|
+
const boundedSummary = (text) => {
|
|
695
|
+
if (text.length <= MAX_SUMMARY_LENGTH) return text;
|
|
696
|
+
const cut = text.slice(0, MAX_SUMMARY_LENGTH);
|
|
697
|
+
return /[\uD800-\uDBFF]$/.test(cut) ? cut.slice(0, -1) : cut;
|
|
698
|
+
};
|
|
699
|
+
|
|
683
700
|
/**
|
|
684
701
|
* 🔴 INVARIANT 2: the agent never creates its own work. A proposal is labelled
|
|
685
702
|
* `triage`, which `buildJql` excludes explicitly, and it never receives a ready
|
|
@@ -689,7 +706,7 @@ export const triageItemFor = (proposal) => {
|
|
|
689
706
|
validateProposal(proposal);
|
|
690
707
|
const fingerprint = fingerprintOf(proposal);
|
|
691
708
|
return {
|
|
692
|
-
title: `proposal: ${proposal.change}
|
|
709
|
+
title: boundedSummary(`proposal: ${proposal.change}`),
|
|
693
710
|
body: [
|
|
694
711
|
`- finding — ${proposal.finding}`,
|
|
695
712
|
`- part to change — ${proposal.part}`,
|
|
@@ -26,6 +26,9 @@ import { dirname, join } from 'node:path';
|
|
|
26
26
|
// so this sweep sees exactly what the gate sweep sees — including the paths a
|
|
27
27
|
// stack layer contributes for its own shape.
|
|
28
28
|
import { elevatedPathsIn, laneOf, readDeclaredPaths } from './detect-missed-gate.mjs';
|
|
29
|
+
// The one credential vocabulary (`guard-secret-file`, the commit sweep and this
|
|
30
|
+
// diagnostic all read it): a second list of token shapes here would drift.
|
|
31
|
+
import { SECRET_VALUE_PATTERNS } from './lib/secrets.mjs';
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
34
|
* The audit trail for work that already happened: a record born **closed**.
|
|
@@ -210,37 +213,261 @@ const parseArgs = (argv) => {
|
|
|
210
213
|
|
|
211
214
|
const daysAgo = (n) => new Date(Date.now() - n * 86_400_000).toISOString().slice(0, 10);
|
|
212
215
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
216
|
+
// --- Acquisition (RP-99) --------------------------------------------------------
|
|
217
|
+
//
|
|
218
|
+
// Two calls, not one. `gh pr list --json` does not know `authorAssociation` on
|
|
219
|
+
// every supported `gh` (2.71.2 answers `Unknown JSON field`), so the BASE
|
|
220
|
+
// projection asks only for fields every `gh` has, and the association — the
|
|
221
|
+
// trust signal — is fetched afterwards through one GraphQL query. When that
|
|
222
|
+
// second step fails for any reason the base list is returned WITHOUT the field
|
|
223
|
+
// and `reconcile()` treats those PRs as untrusted, and an answer is kept only
|
|
224
|
+
// when it names the same pull request URL the base list did — see the
|
|
225
|
+
// generator's test/template/reconcile-acquisition.test.ts (absent in a generated rig)
|
|
226
|
+
// › "reconcile() treats a PR that lost authorAssociation to a failed enrichment as untrusted when it crosses an elevated path"
|
|
227
|
+
// and › "drops an association whose answer names a different pull request URL — another host or repository never becomes trust".
|
|
228
|
+
|
|
229
|
+
/** The base projection: fields every supported `gh` serves. */
|
|
230
|
+
const BASE_FIELDS = ['number', 'title', 'body', 'headRefName', 'mergedAt', 'url', 'files', 'changedFiles'];
|
|
231
|
+
/** What GitHub returns for `authorAssociation`: an upper-case enum word. */
|
|
232
|
+
const ASSOCIATION_SHAPE = /^[A-Z_]{1,32}$/;
|
|
233
|
+
const MAX_PRS = 100;
|
|
234
|
+
|
|
235
|
+
/** The default runner: one `gh` child, stdout as text, a thrown error otherwise. */
|
|
236
|
+
const execGh = (file, args) =>
|
|
237
|
+
execFileSync(file, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
238
|
+
|
|
239
|
+
/** A base record the rest of this script can read: an object with a positive PR number. */
|
|
240
|
+
const isPrRecord = (row) =>
|
|
241
|
+
row !== null &&
|
|
242
|
+
typeof row === 'object' &&
|
|
243
|
+
!Array.isArray(row) &&
|
|
244
|
+
Number.isSafeInteger(row.number) &&
|
|
245
|
+
row.number > 0;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The merged PRs since `since`, plus the warnings the acquisition produced.
|
|
249
|
+
*
|
|
250
|
+
* Throws only from the BASE list — the caller classifies that with
|
|
251
|
+
* `classifyAcquisitionFailure`. A parsed base list that is not an array is
|
|
252
|
+
* thrown with `notAnArray: true` (JSON.parse succeeded, so a SyntaxError would
|
|
253
|
+
* misdescribe it). Enrichment never throws: it degrades to a warning.
|
|
254
|
+
*/
|
|
255
|
+
export const fetchMergedPrs = (since, { exec = execGh } = {}) => {
|
|
256
|
+
const raw = exec('gh', [
|
|
257
|
+
'pr',
|
|
258
|
+
'list',
|
|
259
|
+
'--state',
|
|
260
|
+
'merged',
|
|
261
|
+
'--limit',
|
|
262
|
+
String(MAX_PRS),
|
|
263
|
+
'--search',
|
|
264
|
+
`merged:>=${since}`,
|
|
265
|
+
'--json',
|
|
266
|
+
BASE_FIELDS.join(','),
|
|
267
|
+
]);
|
|
268
|
+
const parsed = JSON.parse(raw);
|
|
269
|
+
if (!Array.isArray(parsed)) {
|
|
270
|
+
throw Object.assign(new Error('gh pr list answered with something other than a JSON array'), {
|
|
271
|
+
notAnArray: true,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const warnings = [];
|
|
276
|
+
const prs = [];
|
|
277
|
+
for (const row of parsed.slice(0, MAX_PRS)) {
|
|
278
|
+
if (!isPrRecord(row)) {
|
|
279
|
+
warnings.push('a merged-PR record without a positive numeric `number` was dropped from the sweep');
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
// Copy the base fields only; whatever else `gh` returned does not travel.
|
|
283
|
+
const record = {};
|
|
284
|
+
for (const field of BASE_FIELDS) if (row[field] !== undefined) record[field] = row[field];
|
|
285
|
+
prs.push(record);
|
|
286
|
+
}
|
|
287
|
+
if (prs.length === 0) return { prs, warnings };
|
|
288
|
+
|
|
289
|
+
const associations = fetchAuthorAssociations(prs, exec);
|
|
290
|
+
if (associations === null) {
|
|
291
|
+
warnings.push(
|
|
292
|
+
'author association unavailable for this window — every external merge below is treated as untrusted origin',
|
|
241
293
|
);
|
|
242
|
-
|
|
294
|
+
return { prs, warnings };
|
|
295
|
+
}
|
|
296
|
+
for (const pr of prs) {
|
|
297
|
+
const answer = associations.get(pr.number);
|
|
298
|
+
if (answer === undefined) continue;
|
|
299
|
+
// `gh api graphql` asks its own default host, not the remote's: an answer
|
|
300
|
+
// about another pull request must never lend its association to this one.
|
|
301
|
+
if (typeof pr.url !== 'string' || answer.url !== pr.url) {
|
|
302
|
+
warnings.push(`PR #${pr.number}: an author association whose pull request URL does not match the listed one was dropped`);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const value = answer.authorAssociation;
|
|
306
|
+
if (typeof value === 'string' && ASSOCIATION_SHAPE.test(value)) pr.authorAssociation = value;
|
|
307
|
+
else warnings.push(`PR #${pr.number}: an author association of an unexpected shape was dropped`);
|
|
243
308
|
}
|
|
309
|
+
return { prs, warnings };
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* `number → { url, authorAssociation }` for the listed PRs through one GraphQL
|
|
314
|
+
* query, or `null` when the answer cannot be trusted: a failed call, unusable
|
|
315
|
+
* JSON, a repository name of an unexpected shape. Never throws.
|
|
316
|
+
*/
|
|
317
|
+
const fetchAuthorAssociations = (prs, exec) => {
|
|
318
|
+
try {
|
|
319
|
+
const repo = JSON.parse(exec('gh', ['repo', 'view', '--json', 'nameWithOwner']));
|
|
320
|
+
const nameWithOwner = typeof repo?.nameWithOwner === 'string' ? repo.nameWithOwner : '';
|
|
321
|
+
const parts = nameWithOwner.split('/');
|
|
322
|
+
if (parts.length !== 2 || !parts.every((part) => /^[A-Za-z0-9_.-]{1,100}$/.test(part))) return null;
|
|
323
|
+
const [owner, name] = parts;
|
|
324
|
+
const fields = prs
|
|
325
|
+
.map((pr) => `pr${pr.number}: pullRequest(number: ${pr.number}) { url authorAssociation }`)
|
|
326
|
+
.join(' ');
|
|
327
|
+
const query = `query { repository(owner: "${owner}", name: "${name}") { ${fields} } }`;
|
|
328
|
+
const answer = JSON.parse(exec('gh', ['api', 'graphql', '-f', `query=${query}`]));
|
|
329
|
+
const repository = answer?.data?.repository;
|
|
330
|
+
if (repository === null || typeof repository !== 'object') return null;
|
|
331
|
+
const out = new Map();
|
|
332
|
+
for (const pr of prs) {
|
|
333
|
+
const node = repository[`pr${pr.number}`];
|
|
334
|
+
if (node === null || typeof node !== 'object' || node.authorAssociation === undefined) continue;
|
|
335
|
+
out.set(pr.number, { url: node.url, authorAssociation: node.authorAssociation });
|
|
336
|
+
}
|
|
337
|
+
return out;
|
|
338
|
+
} catch {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// --- Failure diagnostics (RP-99) -----------------------------------------------
|
|
344
|
+
|
|
345
|
+
const CAUSES = Object.freeze({
|
|
346
|
+
'gh-missing': 'the `gh` CLI could not be started (not installed, or not on PATH)',
|
|
347
|
+
'gh-unauthenticated': 'the `gh` CLI is not authenticated for this host — run `gh auth login`',
|
|
348
|
+
'api-unreachable': 'the GitHub API could not be reached',
|
|
349
|
+
'unsupported-projection': 'this `gh` version does not serve a field the sweep asked for',
|
|
350
|
+
'malformed-response': '`gh` answered, but not with the JSON array the sweep expects',
|
|
351
|
+
unknown: 'the cause is unknown',
|
|
352
|
+
});
|
|
353
|
+
const STDERR_UNAUTHENTICATED = /gh auth login|not logged in|HTTP 401|authentication required/i;
|
|
354
|
+
const STDERR_UNREACHABLE =
|
|
355
|
+
/dial tcp|ENOTFOUND|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|HTTP 5\d\d|could not connect|connection refused/i;
|
|
356
|
+
const STDERR_UNSUPPORTED = /Unknown JSON field/i;
|
|
357
|
+
// ESC-led sequences (CSI, OSC and the single-character escapes); the
|
|
358
|
+
// whitespace controls, which become a space where they sit next to whitespace
|
|
359
|
+
// and a mark where they sit inside a run (so they cannot split a token into
|
|
360
|
+
// pieces too short to match); and every other character a terminal would not
|
|
361
|
+
// print as itself — the remaining C0/C1 controls except the line feed the next
|
|
362
|
+
// step splits on, the invisible format characters (zero-width, bidi,
|
|
363
|
+
// byte-order mark), the line and paragraph separators, and U+E000, the mark
|
|
364
|
+
// they are all replaced with.
|
|
365
|
+
// eslint-disable-next-line no-control-regex -- the pattern exists to remove these very bytes
|
|
366
|
+
const TERMINAL_SEQUENCES = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-Z\\-_])/g;
|
|
367
|
+
const WHITESPACE_CONTROLS = /[\t\v\f\r]/g;
|
|
368
|
+
const WHITESPACE_CONTROLS_INSIDE_A_RUN = /(?<=\S)[\t\v\f\r]+(?=\S)/g;
|
|
369
|
+
// eslint-disable-next-line no-control-regex -- likewise
|
|
370
|
+
const HIDDEN_CHARACTERS = /[\x00-\x08\x0E-\x1F\x7F-\x9F\p{Cf}\p{Zl}\p{Zp}\uE000]/gu;
|
|
371
|
+
const MARK = '\uE000';
|
|
372
|
+
// No leading word boundary: a keyword glued to the word before must still be
|
|
373
|
+
// found, and over-redacting prose is the safe way to be wrong here.
|
|
374
|
+
const AUTHORIZATION_VALUE = /(?:Bearer|Basic|token)\s+[A-Za-z0-9._~+/=-]{8,}|Authorization:\s*[^\s]+(?:\s+[^\s]+)?/gi;
|
|
375
|
+
const DIAGNOSTIC_CAP = 200;
|
|
376
|
+
// Redaction runs on at most this much of the line, and the 200-character cut
|
|
377
|
+
// comes after it: cutting first can shorten a token below what its pattern
|
|
378
|
+
// needs and print the prefix. When the window itself cuts the line, the last
|
|
379
|
+
// whitespace-free run is dropped before redaction, so no token is ever
|
|
380
|
+
// shortened by a cut.
|
|
381
|
+
const REDACTION_WINDOW = 4096;
|
|
382
|
+
// A whitespace-free run that had a hidden character removed from it is masked
|
|
383
|
+
// whole once what is left is at least this long — the shortest value the
|
|
384
|
+
// vocabulary redacts, an authorization value. A removed character can glue a
|
|
385
|
+
// token to the word before it or split it into pieces too short to match, and
|
|
386
|
+
// neither can be told apart from honest text, so such a run is not trusted to
|
|
387
|
+
// the patterns at all.
|
|
388
|
+
const MARKED_RUN_MIN = 8;
|
|
389
|
+
// A masked run that ended in one of these keywords takes the next run with it:
|
|
390
|
+
// the value that followed the keyword can no longer be recognised on its own.
|
|
391
|
+
const AUTHORIZATION_KEYWORD_END = /(?:bearer|basic|token|authorization:)$/i;
|
|
392
|
+
|
|
393
|
+
/** `text` without its trailing whitespace-free run. One backward pass. */
|
|
394
|
+
const dropTrailingRun = (text) => {
|
|
395
|
+
let end = text.length;
|
|
396
|
+
while (end > 0 && !/\s/.test(text[end - 1])) end -= 1;
|
|
397
|
+
return text.slice(0, end);
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const redactPatterns = (text) => {
|
|
401
|
+
let out = text.replace(AUTHORIZATION_VALUE, '[redacted]');
|
|
402
|
+
for (const { pattern } of SECRET_VALUE_PATTERNS) {
|
|
403
|
+
out = out.replace(new RegExp(pattern.source, `${pattern.flags.replace('g', '')}g`), '[redacted]');
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* One printable line of a subprocess's stderr: no terminal sequences, control
|
|
410
|
+
* or invisible format characters, the first non-empty line only, every run a
|
|
411
|
+
* hidden character was removed from masked when it is long enough to be a
|
|
412
|
+
* credential, the credential shapes the shared vocabulary knows — plus HTTP
|
|
413
|
+
* authorization values — replaced by `[redacted]`, and then at most 200
|
|
414
|
+
* characters.
|
|
415
|
+
*/
|
|
416
|
+
export const sanitizeDiagnostic = (text) => {
|
|
417
|
+
const source = typeof text === 'string' ? text : String(text ?? '');
|
|
418
|
+
const marked = source
|
|
419
|
+
.replace(TERMINAL_SEQUENCES, MARK)
|
|
420
|
+
.replace(HIDDEN_CHARACTERS, MARK)
|
|
421
|
+
.replace(WHITESPACE_CONTROLS_INSIDE_A_RUN, MARK)
|
|
422
|
+
.replace(WHITESPACE_CONTROLS, ' ');
|
|
423
|
+
const line = marked.split('\n').find((candidate) => candidate.replaceAll(MARK, '').trim() !== '') ?? '';
|
|
424
|
+
const windowed = line.length > REDACTION_WINDOW ? dropTrailingRun(line.slice(0, REDACTION_WINDOW)) : line;
|
|
425
|
+
let maskNext = false;
|
|
426
|
+
const masked = windowed
|
|
427
|
+
.split(/(\s+)/)
|
|
428
|
+
.map((run) => {
|
|
429
|
+
if (run.trim() === '') return run;
|
|
430
|
+
const visible = run.replaceAll(MARK, '');
|
|
431
|
+
const hidden = run.includes(MARK) && visible.length >= MARKED_RUN_MIN;
|
|
432
|
+
const mask = hidden || maskNext;
|
|
433
|
+
maskNext = mask && AUTHORIZATION_KEYWORD_END.test(visible);
|
|
434
|
+
return mask ? '[redacted]' : visible;
|
|
435
|
+
})
|
|
436
|
+
.join('');
|
|
437
|
+
return redactPatterns(masked).trim().slice(0, DIAGNOSTIC_CAP);
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* The cause the evidence supports, and nothing more. `detail` is the sanitized
|
|
442
|
+
* first non-empty stderr line, whatever the cause, or `null` when there was none. An
|
|
443
|
+
* `unknown` failure with no stderr falls back to the error's own message; the
|
|
444
|
+
* other causes never do, because a parse error's message quotes `gh` output.
|
|
445
|
+
*/
|
|
446
|
+
export const classifyAcquisitionFailure = (error) => {
|
|
447
|
+
const stderr = typeof error?.stderr === 'string' ? error.stderr : '';
|
|
448
|
+
const detail = stderr.trim() === '' ? null : sanitizeDiagnostic(stderr);
|
|
449
|
+
if (error?.code === 'ENOENT') return { cause: 'gh-missing', detail };
|
|
450
|
+
if (error instanceof SyntaxError || error?.notAnArray === true) {
|
|
451
|
+
return { cause: 'malformed-response', detail };
|
|
452
|
+
}
|
|
453
|
+
if (STDERR_UNSUPPORTED.test(stderr)) return { cause: 'unsupported-projection', detail };
|
|
454
|
+
if (STDERR_UNAUTHENTICATED.test(stderr)) return { cause: 'gh-unauthenticated', detail };
|
|
455
|
+
if (STDERR_UNREACHABLE.test(stderr)) return { cause: 'api-unreachable', detail };
|
|
456
|
+
if (detail === null && typeof error?.message === 'string' && error.message.trim() !== '') {
|
|
457
|
+
return { cause: 'unknown', detail: sanitizeDiagnostic(error.message) };
|
|
458
|
+
}
|
|
459
|
+
return { cause: 'unknown', detail };
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
/** The message the CLI prints for a failed acquisition: one cause, named. */
|
|
463
|
+
export const acquisitionFailureMessage = ({ cause, detail }) => {
|
|
464
|
+
const named = CAUSES[cause] ?? CAUSES.unknown;
|
|
465
|
+
return (
|
|
466
|
+
`lane reconciliation: could not list merged PRs — ${named}. ` +
|
|
467
|
+
'No lane was reconciled; do not record an empty `external lane` block from this run. ' +
|
|
468
|
+
'Use --input <file> to work offline.' +
|
|
469
|
+
(detail ? `\n ${detail}` : '')
|
|
470
|
+
);
|
|
244
471
|
};
|
|
245
472
|
|
|
246
473
|
/**
|
|
@@ -267,9 +494,19 @@ const invokedDirectly = () => {
|
|
|
267
494
|
if (invokedDirectly()) {
|
|
268
495
|
const args = parseArgs(process.argv.slice(2));
|
|
269
496
|
const projectRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
497
|
+
let prs;
|
|
498
|
+
let warnings = [];
|
|
499
|
+
if (args.input) {
|
|
500
|
+
prs = readInput(args.input, 'lane reconciliation');
|
|
501
|
+
} else {
|
|
502
|
+
try {
|
|
503
|
+
({ prs, warnings } = fetchMergedPrs(args.since ?? daysAgo(7)));
|
|
504
|
+
} catch (error) {
|
|
505
|
+
process.stderr.write(`${acquisitionFailureMessage(classifyAcquisitionFailure(error))}\n`);
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
for (const warning of warnings) process.stderr.write(`lane reconciliation: ${warning}\n`);
|
|
273
510
|
// Lane sorting still works without a declaration; only the elevated marks go
|
|
274
511
|
// missing, and they render as "no elevated path crossed" rather than lying.
|
|
275
512
|
const elevatedPaths = readDeclaredPaths(projectRoot) ?? [];
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"0.6.2",
|
|
12
12
|
"0.7.0",
|
|
13
13
|
"0.7.1",
|
|
14
|
-
"0.8.0"
|
|
14
|
+
"0.8.0",
|
|
15
|
+
"0.9.0"
|
|
15
16
|
],
|
|
16
17
|
"files": {
|
|
17
18
|
".agents/skills/check-premises/SKILL.md": {
|
|
@@ -29,7 +30,8 @@
|
|
|
29
30
|
"57f6add0504db45e7577474f97705fe474310e4cd3310cdb348a2c64eabe88cd",
|
|
30
31
|
"18f4300de4c4c7ea380d726fb109fa224da950ef0a4cea5015ab1d408b823be5",
|
|
31
32
|
"0a1b6575f6df8139766c8e71ba7320e14d2a5a5ed8ab26b6b43f7a006153d37d",
|
|
32
|
-
"a0d11ca1745095cc5b68777915161f16a6d1602c21f6d273f475d1d07c4b8546"
|
|
33
|
+
"a0d11ca1745095cc5b68777915161f16a6d1602c21f6d273f475d1d07c4b8546",
|
|
34
|
+
"b47bdf0d7a2f6c5b2034b8f6d683ad2b3c83bd65d15baf7de9b58cc4689d0ab1"
|
|
33
35
|
]
|
|
34
36
|
},
|
|
35
37
|
".agents/skills/new-invariant/SKILL.md": {
|
|
@@ -83,7 +85,8 @@
|
|
|
83
85
|
"hashes": [
|
|
84
86
|
"2286fbc01c0342b9bac9909890876c079615db5b398e3499a0f2fddb1a334098",
|
|
85
87
|
"4c6241da56060b44e77f9b5f7a8cde7d2923bd605b8b9bae70d00329784c35be",
|
|
86
|
-
"03f87fbc976d8d65287b032ae3612511fc8f7e57772470b2282b660343fc32db"
|
|
88
|
+
"03f87fbc976d8d65287b032ae3612511fc8f7e57772470b2282b660343fc32db",
|
|
89
|
+
"5ec51bc08c9a934bfcbb2077fc7edce48c0e0cc3ed117d7dfc8a2895f2d9c01b"
|
|
87
90
|
]
|
|
88
91
|
},
|
|
89
92
|
".claude/agents/code-reviewer.md": {
|
|
@@ -93,7 +96,8 @@
|
|
|
93
96
|
"be3d2992d1f278dde46b555df1840b02f91403db1ec2fd4d489fb5efa940f019",
|
|
94
97
|
"ec403373ef6bc0575d61c6ca11f82792817b272221fad8317ef6c45388b52628",
|
|
95
98
|
"f125cfbce848cdaa91fd81e961808b4dbde452287959ec2cf10b905ee3ede789",
|
|
96
|
-
"8ede9297480c889ec6d6caf8d076e7d69111fb472933dcf5a48b9a8425fe50c2"
|
|
99
|
+
"8ede9297480c889ec6d6caf8d076e7d69111fb472933dcf5a48b9a8425fe50c2",
|
|
100
|
+
"f12cb04871578eca3c9c36a1c2d60f5ae15a0e9094ecfc7c704c341756de4e3e"
|
|
97
101
|
]
|
|
98
102
|
},
|
|
99
103
|
".claude/agents/prose-reviewer.md": {
|
|
@@ -102,7 +106,8 @@
|
|
|
102
106
|
"8d5762849afd328fdc1aabf053ad207f2e306e013665e3ff934b06cbb1bbda38",
|
|
103
107
|
"0e4307cd9cf8ef723d79e98b1af46a34c6912a6aa925e2179d939cb83c8f2a44",
|
|
104
108
|
"7e8eaa2b4af9d65e1d10bf50c32c1d138fb2f8b9ba6e6d0880cade0524324d9d",
|
|
105
|
-
"ed1c05d3938c0b4f277f1bf4af8641460def830569049ff0697c9bda14518b78"
|
|
109
|
+
"ed1c05d3938c0b4f277f1bf4af8641460def830569049ff0697c9bda14518b78",
|
|
110
|
+
"7aed3bfc955efc94bbeda3f866d25e2b253c88facaa2beed73939182a9f845b2"
|
|
106
111
|
]
|
|
107
112
|
},
|
|
108
113
|
".claude/agents/security-scanner.md": {
|
|
@@ -110,13 +115,15 @@
|
|
|
110
115
|
"hashes": [
|
|
111
116
|
"43e5d59315483e369ad19f3070175f146e88deabd3a12dc09339ac4fac0f2aee",
|
|
112
117
|
"e01467a38a0183be46ba45b05d44f663fbf4b139ceaecb91b5d096f3286a3fcd",
|
|
113
|
-
"844e215af7d80bd85f62ca937c79bea59c8d8cd7c97353937ce62f7747eb9df7"
|
|
118
|
+
"844e215af7d80bd85f62ca937c79bea59c8d8cd7c97353937ce62f7747eb9df7",
|
|
119
|
+
"7c0bc189521955f2d126bac0ff97ab4fa252b05067692a5a3bb6cda232ddfaa3"
|
|
114
120
|
]
|
|
115
121
|
},
|
|
116
122
|
".claude/agents/test-writer.md": {
|
|
117
123
|
"since": "0.2.0",
|
|
118
124
|
"hashes": [
|
|
119
|
-
"333feef12383dea51791ae4923c44ba542438de793f0792fb51d96aa15b8b1b8"
|
|
125
|
+
"333feef12383dea51791ae4923c44ba542438de793f0792fb51d96aa15b8b1b8",
|
|
126
|
+
"fce976938fdd1154641e405a557b67161daa34cf84de9ad35fa14287d13b7f9c"
|
|
120
127
|
]
|
|
121
128
|
},
|
|
122
129
|
".claude/hooks/block-no-verify.mjs": {
|
|
@@ -180,6 +187,12 @@
|
|
|
180
187
|
"5bf2896ec5e5f94fab20b52cd22418a4392778b40744602a087e320eeca3f688"
|
|
181
188
|
]
|
|
182
189
|
},
|
|
190
|
+
".claude/hooks/guard-subagent-model.mjs": {
|
|
191
|
+
"since": "0.9.0",
|
|
192
|
+
"hashes": [
|
|
193
|
+
"345cbd5f43171adb01cd46ad485fb61299f7135c00ff4217765993551aca3b38"
|
|
194
|
+
]
|
|
195
|
+
},
|
|
183
196
|
".claude/hooks/guard-web-boundary.mjs": {
|
|
184
197
|
"since": "0.2.0",
|
|
185
198
|
"hashes": [
|
|
@@ -202,7 +215,8 @@
|
|
|
202
215
|
"hashes": [
|
|
203
216
|
"4f836bc3ff75a868ffe035b179ac9e7dc1ad7715d520677ff9f6aa5bfdb200eb",
|
|
204
217
|
"0c191d326a7bb0bbfc110e2210f44b4aa39276e8db3779aaa4545cffd4e11863",
|
|
205
|
-
"572c671f8f2cad190fe2f144071954ac4d3b1357cab80a19bad584dd3b3768c8"
|
|
218
|
+
"572c671f8f2cad190fe2f144071954ac4d3b1357cab80a19bad584dd3b3768c8",
|
|
219
|
+
"6695698d2b38c79a12292601048dd0082a0cd18bf9e69c047d47679c2e6ecf67"
|
|
206
220
|
]
|
|
207
221
|
},
|
|
208
222
|
".claude/hooks/lib/hook-input.mjs": {
|
|
@@ -212,6 +226,12 @@
|
|
|
212
226
|
"3be3d4af4a25536d2a2bac0441a1370be7efafb5f8aa737b195bdce77db0091b"
|
|
213
227
|
]
|
|
214
228
|
},
|
|
229
|
+
".claude/hooks/warn-subagent-routing.mjs": {
|
|
230
|
+
"since": "0.9.0",
|
|
231
|
+
"hashes": [
|
|
232
|
+
"b95165243ebb60264c78b82ce01407669457fe33c0bf86ff2a5c117963c6bc6d"
|
|
233
|
+
]
|
|
234
|
+
},
|
|
215
235
|
".claude/queue.json": {
|
|
216
236
|
"since": "0.3.0",
|
|
217
237
|
"hashes": [
|
|
@@ -267,7 +287,8 @@
|
|
|
267
287
|
"hashes": [
|
|
268
288
|
"dbe8025e10583e03fee5056e88aa6b73e4399453711d9156d82528a02d6e0402",
|
|
269
289
|
"9e60d0300e856a9b3f81ab748bf2c0a9de27233ad0aa58a8c7141413fa0f3cae",
|
|
270
|
-
"c8e9849a8fccd4e1e60b1ddc82937eb15aceb7f7c2afbd151b05e1bc966bfd2a"
|
|
290
|
+
"c8e9849a8fccd4e1e60b1ddc82937eb15aceb7f7c2afbd151b05e1bc966bfd2a",
|
|
291
|
+
"aa34a1eab572964b328ffa0044e96acb3e1c31491df0183460f06e5e6bba3b76"
|
|
271
292
|
]
|
|
272
293
|
},
|
|
273
294
|
".claude/scripts/decision-router.mjs": {
|
|
@@ -352,7 +373,8 @@
|
|
|
352
373
|
"ceb6d35fa028f9c5e5e2d5965c627095d5ba80c37191a584627b4bc645b6be15",
|
|
353
374
|
"efa193dc543237d82dcb371fa1df6483ccd43c1db88d0e218f5d89769564974c",
|
|
354
375
|
"bed44df40412a7edac96914de21edc796df604ddc275ff1a02764dfcc4d4ede7",
|
|
355
|
-
"b3893c365de37ef6d9d04ab933296b9fcf09a0886197d7e12313fa63eacdbd89"
|
|
376
|
+
"b3893c365de37ef6d9d04ab933296b9fcf09a0886197d7e12313fa63eacdbd89",
|
|
377
|
+
"37eeb4bdb50d7f6b1427bd02ec9815f39ad9142f35691ffadd8c0b626c6cdb00"
|
|
356
378
|
]
|
|
357
379
|
},
|
|
358
380
|
".claude/scripts/queue/as-of.mjs": {
|
|
@@ -381,7 +403,8 @@
|
|
|
381
403
|
".claude/scripts/queue/gate-rounds.mjs": {
|
|
382
404
|
"since": "0.5.0",
|
|
383
405
|
"hashes": [
|
|
384
|
-
"7577674d5477cfc388594e4c301601e329d60741dfa0b2844be2cc94a3ab6283"
|
|
406
|
+
"7577674d5477cfc388594e4c301601e329d60741dfa0b2844be2cc94a3ab6283",
|
|
407
|
+
"deeb7a10772f906c7630bb3c295be1b61d7ebf0541b2a34debcb40c80e6d8f0e"
|
|
385
408
|
]
|
|
386
409
|
},
|
|
387
410
|
".claude/scripts/queue/github-issues.mjs": {
|
|
@@ -401,7 +424,8 @@
|
|
|
401
424
|
"7db3c17b781e6876b4822afd09e6affa97e46607b63d443975d987daf1f14464",
|
|
402
425
|
"9edd70b5960f367958e8e7500150b4edd285c99869cca803e40e0537d2d347b0",
|
|
403
426
|
"4cf917ac06708e1068dfe9f8bbebc9376c4748526bd669b9bea32929ba996d0a",
|
|
404
|
-
"b4a0789be7ad4b401de872edb06665bca349048178b4dd6c5db848ca44f7c977"
|
|
427
|
+
"b4a0789be7ad4b401de872edb06665bca349048178b4dd6c5db848ca44f7c977",
|
|
428
|
+
"19a04cdbdfd936213cea3a9a00a583a060a4a8d0356d6a6922ae1c2f593f16f1"
|
|
405
429
|
]
|
|
406
430
|
},
|
|
407
431
|
".claude/scripts/queue/jira.mjs": {
|
|
@@ -481,7 +505,8 @@
|
|
|
481
505
|
"hashes": [
|
|
482
506
|
"060b001927d8b471f1c1a8c8851fbb4266d3b2edd674cccbd6c16c44b57b91dd",
|
|
483
507
|
"7721ee19fd1bcf6902be3d359def40742d79b88f5d83e7e3ca22ba45ca3211aa",
|
|
484
|
-
"250292c9959916240bfc0fdf5987a864f6d54799de3d932dc6b85b776e62eaa9"
|
|
508
|
+
"250292c9959916240bfc0fdf5987a864f6d54799de3d932dc6b85b776e62eaa9",
|
|
509
|
+
"ad58b25f9ce30a1549b5047daa1e5b43c30352306aeceebacd638db86087e01f"
|
|
485
510
|
]
|
|
486
511
|
},
|
|
487
512
|
".claude/scripts/verdict.mjs": {
|
|
@@ -498,7 +523,8 @@
|
|
|
498
523
|
"55a6938b4d9967291a26e93301e34e5ffdef74a88f8c773a4d11b5d87d864fff",
|
|
499
524
|
"713718fbb2cda132f05665b29b8f1108a6305729b5e7c789d2053d117c13e08e",
|
|
500
525
|
"e588ab05bee707a099c80c9ea399008be31488e616f3db3c0598f66f3d1b3a52",
|
|
501
|
-
"a180578d6e6de6a21900ff2f8a8ade2242170d16d2dd95574e5004435d870b9b"
|
|
526
|
+
"a180578d6e6de6a21900ff2f8a8ade2242170d16d2dd95574e5004435d870b9b",
|
|
527
|
+
"44695f83e445c7da3257556a3decc70e79ef46d19da394d4d5dd4c9268c0b429"
|
|
502
528
|
]
|
|
503
529
|
},
|
|
504
530
|
".claude/skills/check-premises/SKILL.md": {
|
|
@@ -521,7 +547,8 @@
|
|
|
521
547
|
"57f6add0504db45e7577474f97705fe474310e4cd3310cdb348a2c64eabe88cd",
|
|
522
548
|
"18f4300de4c4c7ea380d726fb109fa224da950ef0a4cea5015ab1d408b823be5",
|
|
523
549
|
"0a1b6575f6df8139766c8e71ba7320e14d2a5a5ed8ab26b6b43f7a006153d37d",
|
|
524
|
-
"a0d11ca1745095cc5b68777915161f16a6d1602c21f6d273f475d1d07c4b8546"
|
|
550
|
+
"a0d11ca1745095cc5b68777915161f16a6d1602c21f6d273f475d1d07c4b8546",
|
|
551
|
+
"b47bdf0d7a2f6c5b2034b8f6d683ad2b3c83bd65d15baf7de9b58cc4689d0ab1"
|
|
525
552
|
]
|
|
526
553
|
},
|
|
527
554
|
".claude/skills/new-invariant/SKILL.md": {
|
|
@@ -577,14 +604,16 @@
|
|
|
577
604
|
"since": "0.5.0",
|
|
578
605
|
"hashes": [
|
|
579
606
|
"902dbace9532ec8e7a3c0cfd6ea0934227f087e345c0619923ec03512b26ba3e",
|
|
580
|
-
"55778d7fb9f778a33213590cc0a2dcf8e01026a1a93eed1c0f811ba4f1070721"
|
|
607
|
+
"55778d7fb9f778a33213590cc0a2dcf8e01026a1a93eed1c0f811ba4f1070721",
|
|
608
|
+
"5fbbb1431833692d56cd0d15249f1d024e53bb0ab63349842910a9fc90547f6a"
|
|
581
609
|
]
|
|
582
610
|
},
|
|
583
611
|
".codex/agents/code-reviewer.toml": {
|
|
584
612
|
"since": "0.5.0",
|
|
585
613
|
"hashes": [
|
|
586
614
|
"e0e3475404c95402f33d77ebb14001e289d26bb429205a0a73bdcef99b4a62c7",
|
|
587
|
-
"9ec92775791907f068d85ba58489236bd2bee69a8d769554ac530e8eadd5e374"
|
|
615
|
+
"9ec92775791907f068d85ba58489236bd2bee69a8d769554ac530e8eadd5e374",
|
|
616
|
+
"180c182cc909cc0b6c06cfa286f528f22b77189eee49cac067fdaaf2920f8bae"
|
|
588
617
|
]
|
|
589
618
|
},
|
|
590
619
|
".codex/agents/prose-reviewer.toml": {
|
|
@@ -592,20 +621,29 @@
|
|
|
592
621
|
"hashes": [
|
|
593
622
|
"80b4631502d95c619ad060ce02cc8702b12574758ba7df8337ed63efa15d764d",
|
|
594
623
|
"cfd516f72acdfd197c59ac3cbdd39d7b175976dbd3d9eef52825dfdc4f1e5b60",
|
|
595
|
-
"8687eacf0ad707a4f887e0714e82fca23fd005fa9ec8824faedfe99ca46333e1"
|
|
624
|
+
"8687eacf0ad707a4f887e0714e82fca23fd005fa9ec8824faedfe99ca46333e1",
|
|
625
|
+
"7c37e4dbd94980bee16396d0d46d97c5079e295d3eed4b3ed8f98d701e57b10d"
|
|
596
626
|
]
|
|
597
627
|
},
|
|
598
628
|
".codex/agents/security-scanner.toml": {
|
|
599
629
|
"since": "0.5.0",
|
|
600
630
|
"hashes": [
|
|
601
631
|
"9ee408789f237a0b55aac6a2472682a08f8696e1d1ff4918ea72766ac8535499",
|
|
602
|
-
"bc9aacca1ca3ed4ceac485f7cd1c6ff5a5405ac7be83419d6145499cc560ee91"
|
|
632
|
+
"bc9aacca1ca3ed4ceac485f7cd1c6ff5a5405ac7be83419d6145499cc560ee91",
|
|
633
|
+
"b529f02056948ff0ca179c96d9d47962c9c8064feccb68d3c970e7b1a2513fc9"
|
|
603
634
|
]
|
|
604
635
|
},
|
|
605
636
|
".codex/agents/test-writer.toml": {
|
|
606
637
|
"since": "0.5.0",
|
|
607
638
|
"hashes": [
|
|
608
|
-
"bb89d93f57d70885d98cfbe428caa524e9c00e1ce30da235ed3318c1ba3c9606"
|
|
639
|
+
"bb89d93f57d70885d98cfbe428caa524e9c00e1ce30da235ed3318c1ba3c9606",
|
|
640
|
+
"ec9e61b2c16c97ed26d5bdda2b0f715a41b80c0af549d0f2bbb96d6ca4c4d8d7"
|
|
641
|
+
]
|
|
642
|
+
},
|
|
643
|
+
".codex/config.toml": {
|
|
644
|
+
"since": "0.9.0",
|
|
645
|
+
"hashes": [
|
|
646
|
+
"c41e34ef5c3bb606852269adeaf5678ce172dc7a7937514ac879152bbddea7f2"
|
|
609
647
|
]
|
|
610
648
|
},
|
|
611
649
|
".codex/hooks.json": {
|
|
@@ -659,7 +697,8 @@
|
|
|
659
697
|
"since": "0.5.0",
|
|
660
698
|
"hashes": [
|
|
661
699
|
"30e55d866d9a2c7359e594181070c63b1974d9df36e000caf419229284303a2e",
|
|
662
|
-
"ebf0ef61407944373c862d37e6a80d4965ece72304b931428c6b3fcc5d9527c4"
|
|
700
|
+
"ebf0ef61407944373c862d37e6a80d4965ece72304b931428c6b3fcc5d9527c4",
|
|
701
|
+
"46b68f9130d16a96278e03d0ddb14d99b678c5a239e4667aa79733e632e67505"
|
|
663
702
|
]
|
|
664
703
|
},
|
|
665
704
|
"docs/decisions/content-blind-revalidation.md": {
|
|
@@ -705,6 +744,12 @@
|
|
|
705
744
|
"dc0c9f5988fc983eb5100a085e274691b1e88a3b54ab2797e00972c4d69d61c4"
|
|
706
745
|
]
|
|
707
746
|
},
|
|
747
|
+
"docs/decisions/subagent-routing.md": {
|
|
748
|
+
"since": "0.9.0",
|
|
749
|
+
"hashes": [
|
|
750
|
+
"97bb882dd066a65a740780a836199ecef8887392c1ece02a92670ecf153988d3"
|
|
751
|
+
]
|
|
752
|
+
},
|
|
708
753
|
"docs/decisions/two-empty-endings.md": {
|
|
709
754
|
"since": "0.5.0",
|
|
710
755
|
"hashes": [
|
|
@@ -11,5 +11,6 @@
|
|
|
11
11
|
"0.6.2": "2a1fc8e10cd2b65deb5c95d937fd7de39f2c92a2",
|
|
12
12
|
"0.7.0": "6589db36e1daa63a99ec595191db1cccf7373196",
|
|
13
13
|
"0.7.1": "52e879b6c103f6ba70493007b6a6466c57ea9824",
|
|
14
|
-
"0.8.0": "870f9a3ecae2881908ece8ec3e2ac13f84f505f5"
|
|
14
|
+
"0.8.0": "870f9a3ecae2881908ece8ec3e2ac13f84f505f5",
|
|
15
|
+
"0.9.0": "c27f391e7395accaf09354f255a07e1b9e4710c1"
|
|
15
16
|
}
|