create-agent-rig 0.4.0 → 0.6.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 +316 -11
- package/README.md +58 -19
- package/package.json +2 -1
- package/packages/cli/dist/commands/create.js +8 -3
- package/packages/cli/dist/commands/init.js +85 -34
- package/packages/cli/dist/commands/upgrade.js +112 -30
- package/packages/cli/dist/index.js +81 -19
- package/packages/cli/dist/lib/copy-tree.js +35 -6
- package/packages/cli/dist/lib/init-settings.js +12 -0
- package/packages/cli/dist/lib/install-set.js +6 -8
- package/packages/cli/dist/lib/manifest.js +31 -9
- package/packages/cli/dist/lib/safe-path.js +30 -0
- package/scripts/prepare.mjs +1 -1
- package/templates/agent-os/init/AGENTS.md +199 -0
- package/templates/agent-os/init/CLAUDE.md +69 -9
- package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +105 -0
- package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +117 -0
- package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +41 -3
- package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +1 -1
- package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +41 -0
- package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +4 -0
- package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +29 -0
- package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +236 -0
- package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +958 -0
- package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +102 -0
- package/templates/agent-os/universal/.agents/skills/new-invariant/guard-invariant.example.mjs +78 -0
- package/templates/agent-os/universal/.agents/skills/new-invariant/guard-invariant.example.test.mjs +89 -0
- package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +396 -0
- package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +73 -0
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +40 -1
- package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +71 -1
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +40 -0
- package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +301 -26
- package/templates/agent-os/universal/.claude/hooks/guard-bash.mjs +43 -5
- package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +25 -11
- package/templates/agent-os/universal/.claude/hooks/guard-rulebook.mjs +127 -0
- package/templates/agent-os/universal/.claude/hooks/guard-secret-file.mjs +180 -0
- package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +32 -13
- package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +175 -7
- package/templates/agent-os/universal/.claude/hooks/lib/edit-input.mjs +503 -0
- package/templates/agent-os/universal/.claude/rules/autonomy.md +69 -1
- package/templates/agent-os/universal/.claude/rules/invariants.md +104 -19
- package/templates/agent-os/universal/.claude/rules/workflow.md +55 -5
- package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1260 -0
- package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +65 -2
- package/templates/agent-os/universal/.claude/scripts/doctor.mjs +351 -0
- package/templates/agent-os/universal/.claude/scripts/git-env.mjs +49 -0
- package/templates/agent-os/universal/.claude/scripts/lib/gate-coverage.mjs +306 -0
- package/templates/agent-os/universal/.claude/scripts/lib/revalidation-points.mjs +28 -0
- package/templates/agent-os/universal/.claude/scripts/lib/secrets.mjs +490 -0
- package/templates/agent-os/universal/.claude/scripts/lib/verdict.mjs +462 -0
- package/templates/agent-os/universal/.claude/scripts/preflight.mjs +33 -17
- package/templates/agent-os/universal/.claude/scripts/queue/as-of.mjs +51 -0
- package/templates/agent-os/universal/.claude/scripts/queue/checkout.mjs +149 -0
- package/templates/agent-os/universal/.claude/scripts/queue/core.mjs +809 -28
- package/templates/agent-os/universal/.claude/scripts/queue/gate-rounds.mjs +160 -0
- package/templates/agent-os/universal/.claude/scripts/queue/github-issues.mjs +95 -16
- package/templates/agent-os/universal/.claude/scripts/queue/index.mjs +543 -15
- package/templates/agent-os/universal/.claude/scripts/queue/jira.mjs +457 -47
- package/templates/agent-os/universal/.claude/scripts/queue/plan-md.mjs +289 -25
- package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +197 -0
- package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +316 -0
- package/templates/agent-os/universal/.claude/scripts/revalidation-report.mjs +180 -0
- package/templates/agent-os/universal/.claude/scripts/run-journal.mjs +435 -0
- package/templates/agent-os/universal/.claude/scripts/run-state.mjs +539 -0
- package/templates/agent-os/universal/.claude/scripts/stop-flag.mjs +15 -8
- package/templates/agent-os/universal/.claude/scripts/unattended-flag.mjs +239 -0
- package/templates/agent-os/universal/.claude/scripts/verdict.mjs +198 -0
- package/templates/agent-os/universal/.claude/settings.json +11 -2
- package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +118 -7
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +667 -38
- package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +359 -26
- package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +4 -0
- package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +4 -0
- package/templates/agent-os/universal/.codex/agents/security-scanner.toml +4 -0
- package/templates/agent-os/universal/.codex/agents/test-writer.toml +4 -0
- package/templates/agent-os/universal/.codex/hooks.json +70 -0
- package/templates/agent-os/universal/AGENTS.md +166 -0
- package/templates/agent-os/universal/CLAUDE.md +49 -14
- package/templates/agent-os/universal/PLAN.md +7 -40
- package/templates/agent-os/universal/docs/decisions/closing-a-task.md +98 -0
- package/templates/agent-os/universal/docs/decisions/codex-adapter.md +108 -0
- package/templates/agent-os/universal/docs/decisions/fail-open-guards.md +43 -0
- package/templates/agent-os/universal/docs/decisions/gate-coverage.md +83 -0
- package/templates/agent-os/universal/docs/decisions/review-lanes.md +72 -0
- package/templates/agent-os/universal/docs/decisions/run-directory.md +44 -0
- package/templates/agent-os/universal/docs/decisions/spacing-rations-mechanisms.md +125 -0
- package/templates/agent-os/universal/docs/decisions/stop-conditions-in-a-file.md +46 -0
- package/templates/agent-os/universal/docs/decisions/two-empty-endings.md +74 -0
- package/templates/agent-os/universal/journal/README.md +101 -0
- package/templates/agent-os/universal/layers.json +45 -2
- package/templates/hash-history.json +310 -49
- package/templates/release-ledger.json +9 -0
- package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +34 -1
- package/templates/skeleton/aws-serverless/README.md +91 -9
- package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +9 -2
- package/templates/skeleton/aws-serverless/gitignore +37 -0
- package/templates/skeleton/aws-serverless/infra/bin/app.ts +73 -13
- package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +136 -4
- package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +14 -1
- package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +301 -0
- package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +137 -0
- package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +36 -20
- package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +15 -12
- package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +33 -6
- package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +18 -5
- package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +11 -0
- package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +120 -1
- package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +72 -1
- package/templates/skeleton/node-service/README.md +11 -1
- package/templates/skeleton/node-service/gitignore +34 -0
- package/templates/skeleton/node-service/packages/db/src/note-store.ts +47 -10
- package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +20 -0
- package/templates/skeleton/node-service/services/api/src/main.ts +2 -9
- package/templates/skeleton/node-service/services/api/src/server.ts +93 -10
- package/templates/skeleton/node-service/services/api/src/static-dir.ts +20 -0
- package/templates/skeleton/node-service/services/api/test/server.test.ts +98 -13
- package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +28 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many times a branch has entered the review gate.
|
|
3
|
+
*
|
|
4
|
+
* 🔴 **Its own file, and that is the whole design.** The item that asked for this
|
|
5
|
+
* named `queue.state.json` — the file holding `lastCompletedTier` — as the place to
|
|
6
|
+
* put the count. A review round measured what that costs: `recordCompletedTier`
|
|
7
|
+
* writes that file whole, so either the two writers merge or the first close of any
|
|
8
|
+
* item deletes the round count of every branch still in gate. Merging was
|
|
9
|
+
* implemented, and then it turned out to be worse: a state file that fails to parse
|
|
10
|
+
* reads as `{}` to a merging writer, which then writes its snapshot back and the
|
|
11
|
+
* tier is **silently gone** — and a missing tier is the permissive value that lets a
|
|
12
|
+
* second elevated item straight through. A lost round costs one extra review; a lost
|
|
13
|
+
* tier disables the ration the queue is spaced by.
|
|
14
|
+
*
|
|
15
|
+
* Two files, one writer each. The counter cannot damage the ration and `state.mjs`
|
|
16
|
+
* goes back to owning its file outright — so a lost round is now the worst this
|
|
17
|
+
* mechanism can do, where before it could lose the tier.
|
|
18
|
+
*
|
|
19
|
+
* ⚠ **What that does NOT fix, measured rather than reasoned:** `recordGateRound` is
|
|
20
|
+
* read-modify-write with no lock. Eight concurrent calls on one counter recorded
|
|
21
|
+
* **four** rounds — the file is shared across every worktree of a repo by design
|
|
22
|
+
* (`gateRoundsPathFor`), so this is reachable whenever two loops run in one
|
|
23
|
+
* repository at once, not only in one directory. Each lost increment buys one extra
|
|
24
|
+
* allowed round. A lock is not worth it for that: the loop is sequential per task,
|
|
25
|
+
* and the failure is bounded and in the generous direction. What was worth fixing is
|
|
26
|
+
* the crash it came with — a fixed temp filename made the losers of that race fail
|
|
27
|
+
* with `ENOENT` on rename, reporting "could not run" for a condition nothing named.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { renameSync, readFileSync, writeFileSync } from 'node:fs';
|
|
31
|
+
import { join } from 'node:path';
|
|
32
|
+
|
|
33
|
+
import { mainCheckoutRoot } from './checkout.mjs';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Where the counter lives, unless a caller names the file.
|
|
37
|
+
*
|
|
38
|
+
* The default lands in the MAIN checkout even when the gate runs inside a worktree
|
|
39
|
+
* — the same rule `state.mjs` follows, and for the same reason: a task's worktree is
|
|
40
|
+
* shorter-lived than the task.
|
|
41
|
+
*/
|
|
42
|
+
export const gateRoundsPathFor = (projectRoot) =>
|
|
43
|
+
join(mainCheckoutRoot(projectRoot), '.claude', 'gate-rounds.json');
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The counts, keyed by branch.
|
|
47
|
+
*
|
|
48
|
+
* An absent file is zero rounds — the normal state of a fresh checkout. A file that
|
|
49
|
+
* exists and does not parse is **refused**, not read as zero: reading it as zero
|
|
50
|
+
* hands the branch a full cap again, and it does so at the exact moment something is
|
|
51
|
+
* already wrong with the file.
|
|
52
|
+
*/
|
|
53
|
+
const readRounds = (file) => {
|
|
54
|
+
let raw;
|
|
55
|
+
try {
|
|
56
|
+
raw = readFileSync(file, 'utf8');
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') return Object.create(null);
|
|
59
|
+
throw new Error(
|
|
60
|
+
`${file} exists but could not be read (${error?.code ?? 'unknown error'}), so the ` +
|
|
61
|
+
'gate rounds for this branch are unknown. Refusing rather than starting the ' +
|
|
62
|
+
'count again, which would hand this branch a full cap.',
|
|
63
|
+
{ cause: error },
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = JSON.parse(raw);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`${file} is not valid JSON, so the gate rounds cannot be read: ` +
|
|
73
|
+
`${String(error?.message ?? error).split('\n')[0]}. Delete the file to start ` +
|
|
74
|
+
'the count over — deliberately, rather than by accident.',
|
|
75
|
+
{ cause: error },
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`${file} is valid JSON but not an object of branch counts, so the gate rounds ` +
|
|
81
|
+
'cannot be read. Expected `{ "<branch>": <positive integer> }`. Delete the ' +
|
|
82
|
+
'file to start the count over.',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// A single malformed entry is dropped rather than refused: one bad key costs that
|
|
87
|
+
// branch a fresh cap, while refusing would block every branch in the checkout.
|
|
88
|
+
//
|
|
89
|
+
// 🔴 `Object.create(null)`, not `{}`. A branch named `__proto__` or `constructor`
|
|
90
|
+
// otherwise resolves to an inherited value rather than a count, so the increment
|
|
91
|
+
// produced `"[object Object]1"` — a string, which every numeric comparison against
|
|
92
|
+
// the cap answers `false`, i.e. unlimited rounds. Absurd branch name, real class of
|
|
93
|
+
// defect, and the failure was silent.
|
|
94
|
+
const clean = Object.create(null);
|
|
95
|
+
for (const [branch, count] of Object.entries(parsed)) {
|
|
96
|
+
if (Number.isInteger(count) && count > 0) clean[branch] = count;
|
|
97
|
+
}
|
|
98
|
+
return clean;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The branch a count belongs to.
|
|
103
|
+
*
|
|
104
|
+
* 🔴 `HEAD` is refused, and it is the case this check exists for. On a detached
|
|
105
|
+
* checkout — mid-rebase, after `gh pr checkout` of a fork, in CI —
|
|
106
|
+
* `git rev-parse --abbrev-ref HEAD` prints the literal string `HEAD`, which is a
|
|
107
|
+
* non-empty string and would key one shared counter for every task in the
|
|
108
|
+
* directory. A review round found exactly that hole under a comment claiming it was
|
|
109
|
+
* closed.
|
|
110
|
+
*/
|
|
111
|
+
const requireBranch = (branch) => {
|
|
112
|
+
if (typeof branch !== 'string' || branch.trim() === '') {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`a gate round is counted per branch, and the branch was ${JSON.stringify(branch)}.`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (branch.trim() === 'HEAD') {
|
|
118
|
+
throw new Error(
|
|
119
|
+
'the branch is the literal "HEAD", which is what a detached checkout reports. ' +
|
|
120
|
+
'Counting under it would share one budget across every task in this ' +
|
|
121
|
+
'directory. Check out the branch under review, or pass its name explicitly.',
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return branch.trim();
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** How many rounds this branch has recorded. Zero when the file is absent. */
|
|
128
|
+
export const gateRoundsFor = ({ branch, projectRoot, roundsPath } = {}) =>
|
|
129
|
+
readRounds(roundsPath ?? gateRoundsPathFor(projectRoot))[requireBranch(branch)] ?? 0;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Count one round for this branch and return the new total.
|
|
133
|
+
*
|
|
134
|
+
* Write-then-rename, because a plain write leaves a window where the file on disk is
|
|
135
|
+
* half a JSON document — and the reader above refuses such a file, which would turn
|
|
136
|
+
* a crash mid-write into a gate nobody can run. `run-state.mjs` reached the same
|
|
137
|
+
* conclusion about the same class of file.
|
|
138
|
+
*
|
|
139
|
+
* The temp name carries the pid. A fixed `${file}.tmp` was measured failing: under
|
|
140
|
+
* concurrent calls the first rename consumed the shared temp file and the rest died
|
|
141
|
+
* with `ENOENT` on rename, which this command reports as "could not run" — a
|
|
142
|
+
* diagnosis for a cause nothing named. Per-pid temps make a concurrent loser lose
|
|
143
|
+
* only its increment, which is the bounded failure the docblock above states.
|
|
144
|
+
*/
|
|
145
|
+
export const recordGateRound = ({ branch, projectRoot, roundsPath } = {}) => {
|
|
146
|
+
const key = requireBranch(branch);
|
|
147
|
+
const file = roundsPath ?? gateRoundsPathFor(projectRoot);
|
|
148
|
+
const rounds = readRounds(file);
|
|
149
|
+
const next = (rounds[key] ?? 0) + 1;
|
|
150
|
+
|
|
151
|
+
// Spread into a null-prototype object for the same reason `readRounds` builds one:
|
|
152
|
+
// `{ ...rounds }` would give the result a prototype again, and `JSON.stringify`
|
|
153
|
+
// then writes a `__proto__` key that the next read cannot see as its own.
|
|
154
|
+
const updated = Object.assign(Object.create(null), rounds, { [key]: next });
|
|
155
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
156
|
+
writeFileSync(temp, `${JSON.stringify(updated, null, 2)}\n`);
|
|
157
|
+
renameSync(temp, file);
|
|
158
|
+
|
|
159
|
+
return { rounds: next };
|
|
160
|
+
};
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
// be a snapshot that nothing updates when the blocker lands; this cannot go stale,
|
|
15
15
|
// because it is re-resolved from the blockers themselves on every selection.
|
|
16
16
|
import { execFileSync } from 'node:child_process';
|
|
17
|
-
import { duplicateOf, fingerprintOf, validateProposal } from './core.mjs';
|
|
17
|
+
import { duplicateOf, fingerprintOf, lifecycleOf, ownerOfLabels, validateProposal } from './core.mjs';
|
|
18
|
+
import { withAsOf } from './as-of.mjs';
|
|
19
|
+
import { recordEscalation, recordTakeUp } from '../run-state.mjs';
|
|
18
20
|
|
|
19
21
|
export const name = 'github-issues';
|
|
20
22
|
|
|
@@ -74,6 +76,11 @@ export const toTicket = (issue, states = {}) => {
|
|
|
74
76
|
blocks: [],
|
|
75
77
|
priority: priorityLabel ? Number(priorityLabel[1]) : 999,
|
|
76
78
|
createdAt: issue.createdAt ?? null,
|
|
79
|
+
// The take-up marker for revalidation at SELECT: the tracker's own
|
|
80
|
+
// last-modified field, whose contract (moves on edits, comments and state
|
|
81
|
+
// changes) is assumed and not checked here. `null` when the listing did not
|
|
82
|
+
// carry it.
|
|
83
|
+
updatedAt: issue.updatedAt ?? null,
|
|
77
84
|
// The body travels on the neutral shape so the hygiene checks live in one
|
|
78
85
|
// place (core.mjs) instead of once per adapter. This adapter also parses it
|
|
79
86
|
// internally for blocker links — the two readings are independent on
|
|
@@ -86,6 +93,10 @@ export const toTicket = (issue, states = {}) => {
|
|
|
86
93
|
: labels.includes('trigger-human')
|
|
87
94
|
? 'human'
|
|
88
95
|
: null,
|
|
96
|
+
// The repository this item belongs to (AR-132): `owner-<name>`, or null.
|
|
97
|
+
owner: ownerOfLabels(labels),
|
|
98
|
+
// The lifecycle and the scheduling flag (AR-144), read above the seam.
|
|
99
|
+
...lifecycleOf(labels),
|
|
89
100
|
};
|
|
90
101
|
};
|
|
91
102
|
|
|
@@ -119,7 +130,7 @@ const ghText = (args) =>
|
|
|
119
130
|
|
|
120
131
|
const ghJson = (args) => JSON.parse(ghText(args));
|
|
121
132
|
|
|
122
|
-
const FIELDS = 'number,title,body,state,labels,url,createdAt';
|
|
133
|
+
const FIELDS = 'number,title,body,state,labels,url,createdAt,updatedAt';
|
|
123
134
|
|
|
124
135
|
// --- the adapter contract ------------------------------------------------------
|
|
125
136
|
|
|
@@ -145,21 +156,63 @@ export const listEligible = ({ limit = 100, issues = null } = {}) => {
|
|
|
145
156
|
export const resolveBlockers = (ticket) => (ticket.blockedBy ?? []).filter((b) => !b.resolved);
|
|
146
157
|
|
|
147
158
|
/** `To Do → In Progress` before the first file is edited, not when the PR opens. */
|
|
159
|
+
/**
|
|
160
|
+
* Re-record the item's marker after a write of this adapter's own (AR-140) —
|
|
161
|
+
* the same rule and the same limit as `jira.mjs` › rebaseline: only writes
|
|
162
|
+
* made through this adapter re-baseline, and a read-back that fails is
|
|
163
|
+
* announced, never thrown.
|
|
164
|
+
*/
|
|
165
|
+
const rebaseline = (ticket) => {
|
|
166
|
+
const runDir = process.env.RIG_RUN_DIR;
|
|
167
|
+
if (!runDir) return;
|
|
168
|
+
try {
|
|
169
|
+
const after = ghJson(['issue', 'view', ticket.id, '--json', 'updatedAt']);
|
|
170
|
+
recordTakeUp(runDir, { id: ticket.id, updatedAt: after?.updatedAt ?? null });
|
|
171
|
+
} catch (error) {
|
|
172
|
+
process.stderr.write(
|
|
173
|
+
`#${ticket.id}: the write landed, but its marker was NOT re-recorded in ${runDir} — ` +
|
|
174
|
+
`${error.message}\n`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
148
179
|
export const claim = (ticket) => {
|
|
149
180
|
ghText(['issue', 'edit', ticket.id, '--add-label', 'in-progress']);
|
|
181
|
+
rebaseline(ticket);
|
|
150
182
|
return { ok: true };
|
|
151
183
|
};
|
|
152
184
|
|
|
185
|
+
/**
|
|
186
|
+
* One issue by number, closed included — `gh issue view` sees every state,
|
|
187
|
+
* where `listEligible` drops CLOSED for selection's sake. The offline `issues`
|
|
188
|
+
* seam is honoured. `blocks` is empty here: the cross-index needs the whole
|
|
189
|
+
* list, and a single view does not carry it.
|
|
190
|
+
*/
|
|
191
|
+
export const find = (id, { issues = null } = {}) => {
|
|
192
|
+
const raw = issues
|
|
193
|
+
? (issues.find((issue) => String(issue.number) === String(id)) ?? null)
|
|
194
|
+
: ghJson(['issue', 'view', String(id), '--json', FIELDS]);
|
|
195
|
+
return raw ? toTicket(raw, {}) : null;
|
|
196
|
+
};
|
|
197
|
+
|
|
153
198
|
export const close = (ticket, { prUrl = null } = {}) => {
|
|
154
199
|
const note = prUrl ? `Landed in ${prUrl}.` : 'Closed by the run.';
|
|
155
200
|
ghText(['issue', 'comment', ticket.id, '--body', note]);
|
|
156
201
|
ghText(['issue', 'close', ticket.id]);
|
|
157
|
-
|
|
158
|
-
|
|
202
|
+
// Read back, never inferred: `gh issue close` exits 0 on an issue that was
|
|
203
|
+
// already closed, or that a workflow reopened a moment later (AR-135). The
|
|
204
|
+
// claim label goes only once the read-back says CLOSED — a close that did
|
|
205
|
+
// not land leaves the item claimed, exactly as it was found.
|
|
206
|
+
const after = ghJson(['issue', 'view', ticket.id, '--json', 'state']);
|
|
207
|
+
const transitioned = String(after?.state ?? '').toUpperCase() === 'CLOSED';
|
|
208
|
+
if (transitioned) ghText(['issue', 'edit', ticket.id, '--remove-label', 'in-progress']);
|
|
209
|
+
rebaseline(ticket);
|
|
210
|
+
return { ok: true, transitioned };
|
|
159
211
|
};
|
|
160
212
|
|
|
161
213
|
export const comment = (ticket, body) => {
|
|
162
214
|
ghText(['issue', 'comment', ticket.id, '--body', body]);
|
|
215
|
+
rebaseline(ticket);
|
|
163
216
|
return { ok: true };
|
|
164
217
|
};
|
|
165
218
|
|
|
@@ -168,9 +221,13 @@ export const comment = (ticket, body) => {
|
|
|
168
221
|
* selection cannot pick it up again. It stays OPEN and stays claimed — moving it
|
|
169
222
|
* back to a selectable state is how one stuck task gets worked three times.
|
|
170
223
|
*/
|
|
171
|
-
export const escalate = (ticket, diagnosis) => {
|
|
224
|
+
export const escalate = (ticket, diagnosis, { env = process.env } = {}) => {
|
|
172
225
|
ghText(['issue', 'comment', ticket.id, '--body', diagnosis]);
|
|
173
226
|
ghText(['issue', 'edit', ticket.id, '--add-label', 'escalated']);
|
|
227
|
+
rebaseline(ticket);
|
|
228
|
+
// Counted through the one recorder, never a counter of this adapter's own —
|
|
229
|
+
// "twice in a row" has to mean the same thing on every tracker.
|
|
230
|
+
recordEscalation(env.RIG_RUN_DIR);
|
|
174
231
|
return { ok: true };
|
|
175
232
|
};
|
|
176
233
|
|
|
@@ -192,8 +249,10 @@ export const triageItemFor = (proposal) => {
|
|
|
192
249
|
`- **part to change** — ${proposal.part}`,
|
|
193
250
|
`- **proposed change** — ${proposal.change}`,
|
|
194
251
|
`- **how the next run proves it** — ${proposal.proof}`,
|
|
252
|
+
...(proposal.measured ? [`- **measured** — ${proposal.measured}`, `- **inferred** — ${proposal.inferred}`] : []),
|
|
195
253
|
'',
|
|
196
254
|
`fingerprint: ${fingerprint}`,
|
|
255
|
+
...(proposal.asOf ? [`asOf: ${proposal.asOf}`] : []),
|
|
197
256
|
'',
|
|
198
257
|
'The loop proposes; the owner patches. Self-applying a change to its own',
|
|
199
258
|
'rulebook is how an unattended run drifts irreversibly — and it collides with',
|
|
@@ -208,18 +267,21 @@ export const triageItemFor = (proposal) => {
|
|
|
208
267
|
/**
|
|
209
268
|
* File the proposal — or increment the one already there.
|
|
210
269
|
*
|
|
211
|
-
* Under a scheduler against a finite queue the most common
|
|
270
|
+
* Under a scheduler against a finite queue the most common stops are the two that
|
|
271
|
+
* hand out nothing — "queue empty" and "nothing selectable";
|
|
212
272
|
* twenty such stops must produce one proposal with a count of twenty.
|
|
213
273
|
*/
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
274
|
+
/** The proposals on file, as `{ id, body }` — every `triage`-labelled issue. */
|
|
275
|
+
export const listProposals = ({ existing = null } = {}) =>
|
|
276
|
+
(
|
|
217
277
|
existing ??
|
|
218
|
-
ghJson(['issue', 'list', '--label', 'triage', '--state', 'all', '--limit', '100', '--json', FIELDS])
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
);
|
|
278
|
+
ghJson(['issue', 'list', '--label', 'triage', '--state', 'all', '--limit', '100', '--json', FIELDS])
|
|
279
|
+
).map((issue) => ({ id: String(issue.number), body: issue.body }));
|
|
280
|
+
|
|
281
|
+
export const proposeTriage = (rawProposal, { existing = null } = {}) => {
|
|
282
|
+
const proposal = withAsOf(rawProposal);
|
|
283
|
+
const item = triageItemFor(proposal);
|
|
284
|
+
const duplicate = duplicateOf(item, listProposals({ existing }));
|
|
223
285
|
|
|
224
286
|
if (duplicate) {
|
|
225
287
|
ghText([
|
|
@@ -232,6 +294,23 @@ export const proposeTriage = (proposal, { existing = null } = {}) => {
|
|
|
232
294
|
return { ok: true, incremented: String(duplicate.number), item };
|
|
233
295
|
}
|
|
234
296
|
|
|
235
|
-
ghText(['issue', 'create', '--title', item.title, '--body', item.body, '--label', 'triage']);
|
|
236
|
-
|
|
297
|
+
const url = ghText(['issue', 'create', '--title', item.title, '--body', item.body, '--label', 'triage']);
|
|
298
|
+
// The proposal's own baseline (AR-138), as in jira.mjs: `gh issue create`
|
|
299
|
+
// prints the new issue's URL, whose last segment is its number; the marker
|
|
300
|
+
// is read back with `gh issue view`. No run directory → nothing recorded.
|
|
301
|
+
// Best-effort for the same reason as there: the issue exists by now, and a
|
|
302
|
+
// throw would make the caller file it again.
|
|
303
|
+
const id = /\/(\d+)\s*$/.exec(String(url ?? ''))?.[1] ?? null;
|
|
304
|
+
if (id && process.env.RIG_RUN_DIR) {
|
|
305
|
+
try {
|
|
306
|
+
const after = ghJson(['issue', 'view', id, '--json', 'updatedAt']);
|
|
307
|
+
recordTakeUp(process.env.RIG_RUN_DIR, { id, updatedAt: after?.updatedAt ?? null });
|
|
308
|
+
} catch (error) {
|
|
309
|
+
process.stderr.write(
|
|
310
|
+
`proposeTriage: #${id} is filed, but its baseline was NOT recorded in ` +
|
|
311
|
+
`${process.env.RIG_RUN_DIR} — ${error.message}\n`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return { ok: true, filed: item.title, id, item };
|
|
237
316
|
};
|