mandrel 2.54.0 → 2.56.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/.agents/agents/story-worker.md +24 -23
- package/.agents/audit-checklists/accessibility.md +0 -3
- package/.agents/audit-checklists/mobile.md +0 -4
- package/.agents/docs/agentrc-reference.json +8 -2
- package/.agents/docs/configuration.md +5 -0
- package/.agents/rules/ci-remediation.md +39 -21
- package/.agents/schemas/agentrc.schema.json +34 -1
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +2 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -0
- package/.agents/scripts/audit-to-stories.js +374 -76
- package/.agents/scripts/check-audit-attribution.js +119 -62
- package/.agents/scripts/check-test-portability.js +512 -0
- package/.agents/scripts/coverage-capture.js +17 -10
- package/.agents/scripts/evidence-gate.js +31 -4
- package/.agents/scripts/file-ci-gap.js +306 -0
- package/.agents/scripts/generate-workflows-doc.js +65 -14
- package/.agents/scripts/git-cleanup.js +4 -0
- package/.agents/scripts/lib/ITicketingProvider.js +78 -0
- package/.agents/scripts/lib/audit-advisories.js +195 -0
- package/.agents/scripts/lib/audit-attribution.js +22 -0
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +25 -1
- package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +80 -29
- package/.agents/scripts/lib/audit-to-stories/finding-adapter.js +5 -1
- package/.agents/scripts/lib/audit-to-stories/issue-corpus.js +162 -0
- package/.agents/scripts/lib/audit-to-stories/issue-index.js +83 -0
- package/.agents/scripts/lib/audit-to-stories/issues-file.js +121 -0
- package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +61 -115
- package/.agents/scripts/lib/audit-to-stories/ledger-pr.js +347 -0
- package/.agents/scripts/lib/audit-to-stories/ledger-record.js +126 -0
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +169 -44
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +11 -0
- package/.agents/scripts/lib/baselines/merge-envelopes.js +298 -32
- package/.agents/scripts/lib/bootstrap/baseline-merge-driver.js +180 -14
- package/.agents/scripts/lib/cli-args.js +26 -0
- package/.agents/scripts/lib/close-validation/gates.js +113 -7
- package/.agents/scripts/lib/close-validation/process.js +7 -3
- package/.agents/scripts/lib/close-validation/runner.js +62 -11
- package/.agents/scripts/lib/config/ci.js +28 -9
- package/.agents/scripts/lib/config-settings-schema-delivery.js +7 -0
- package/.agents/scripts/lib/config-settings-schema.js +52 -1
- package/.agents/scripts/lib/coverage-capture-fullscope.js +23 -11
- package/.agents/scripts/lib/coverage-capture-incremental.js +22 -16
- package/.agents/scripts/lib/coverage-capture-usage.js +5 -1
- package/.agents/scripts/lib/coverage-capture.js +77 -3
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +53 -13
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +71 -25
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +18 -25
- package/.agents/scripts/lib/{audit-to-stories/ledger.js → findings/audit-ledger.js} +131 -24
- package/.agents/scripts/lib/findings/route-finding.js +42 -2
- package/.agents/scripts/lib/full-suite-lock.js +232 -6
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/git/sync-from-base.js +130 -13
- package/.agents/scripts/lib/github/framework-repo.js +148 -2
- package/.agents/scripts/lib/label-constants.js +6 -1
- package/.agents/scripts/lib/observability/source-classifier.js +2 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +10 -2
- package/.agents/scripts/lib/orchestration/check-baselines/phases/refresh-ack.js +75 -15
- package/.agents/scripts/lib/orchestration/ci-gap-intake.js +605 -0
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +13 -8
- package/.agents/scripts/lib/orchestration/deliver-recover.js +82 -43
- package/.agents/scripts/lib/orchestration/dependency-candidates.js +8 -4
- package/.agents/scripts/lib/orchestration/epic-candidates.js +9 -4
- package/.agents/scripts/lib/orchestration/epic-container.js +66 -4
- package/.agents/scripts/lib/orchestration/epic-rollup.js +233 -84
- package/.agents/scripts/lib/orchestration/file-assumptions.js +218 -16
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +93 -7
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +22 -6
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/parse-args.js +26 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +13 -2
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +35 -5
- package/.agents/scripts/lib/orchestration/merge-block-class.js +18 -3
- package/.agents/scripts/lib/orchestration/merge-poll.js +284 -40
- package/.agents/scripts/lib/orchestration/plan-persist/audit-provenance.js +197 -0
- package/.agents/scripts/lib/orchestration/plan-persist/epic-adoption.js +49 -2
- package/.agents/scripts/lib/orchestration/plan-persist/epic-ops.js +43 -7
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +39 -3
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +5 -0
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +3 -0
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +63 -0
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +110 -0
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +130 -40
- package/.agents/scripts/lib/orchestration/resolve-stories.js +44 -1
- package/.agents/scripts/lib/orchestration/review-providers/native.js +31 -11
- package/.agents/scripts/lib/orchestration/review-providers/scoped-lint.js +27 -24
- package/.agents/scripts/lib/orchestration/run-epilogue.js +63 -42
- package/.agents/scripts/lib/orchestration/single-story-close/close-note.js +81 -0
- package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +40 -51
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +10 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +101 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +351 -28
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +27 -6
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +117 -22
- package/.agents/scripts/lib/orchestration/story-close/baseline-upward-writeback.js +94 -12
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +6 -1
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +32 -20
- package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -14
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +30 -0
- package/.agents/scripts/lib/orchestration/verify-credit.js +37 -0
- package/.agents/scripts/lib/pinned-override-notes.js +41 -53
- package/.agents/scripts/lib/pinned-override-resolve.js +212 -0
- package/.agents/scripts/lib/qa/resolve-qa-contract.js +18 -0
- package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +173 -9
- package/.agents/scripts/lib/skills/walk-skill-files.js +24 -7
- package/.agents/scripts/lib/test-temp.js +167 -30
- package/.agents/scripts/lib/validation-evidence.js +37 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +167 -14
- package/.agents/scripts/lib/wave-runner/live-probe.js +7 -1
- package/.agents/scripts/lib/wave-runner/ready-set.js +1 -1
- package/.agents/scripts/merge-baseline.js +175 -21
- package/.agents/scripts/pr-watch-with-update.js +3 -2
- package/.agents/scripts/providers/github/errors.js +22 -1
- package/.agents/scripts/providers/github/issues.js +106 -1
- package/.agents/scripts/providers/github/sub-issue-add.js +18 -1
- package/.agents/scripts/providers/github.js +6 -0
- package/.agents/scripts/resolve-stories.js +44 -34
- package/.agents/scripts/single-story-close.js +5 -0
- package/.agents/scripts/stories-wave-tick.js +37 -13
- package/.agents/templates/docs/audit-sweep-runbook.md +41 -7
- package/.agents/workflows/audit-accessibility.md +16 -31
- package/.agents/workflows/audit-mobile.md +20 -37
- package/.agents/workflows/audit-to-stories.md +63 -27
- package/.agents/workflows/git-cleanup.md +17 -3
- package/.agents/workflows/helpers/audit-lens-core.md +45 -0
- package/.agents/workflows/helpers/deliver-digest.md +7 -6
- package/.agents/workflows/helpers/deliver-reference.md +35 -14
- package/.agents/workflows/helpers/deliver-story-reference.md +26 -8
- package/.agents/workflows/helpers/deliver-story.md +15 -12
- package/.agents/workflows/helpers/plan-reference.md +30 -0
- package/.agents/workflows/mandrel-plan.md +10 -13
- package/.agents/workflows/memory-consolidate.md +14 -9
- package/docs/CHANGELOG.md +37 -0
- package/lib/cli/registry.js +64 -21
- package/lib/cli/sync.js +27 -2
- package/package.json +7 -4
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/audit-to-stories/issue-index.js — a local index of the audit Issues a
|
|
3
|
+
* sweep must dedupe against.
|
|
4
|
+
*
|
|
5
|
+
* Dedup used to answer every finding with a **search** round-trip: one
|
|
6
|
+
* `findIssuesByFingerprint(sha)` per finding, plus a meaning-first semantic
|
|
7
|
+
* search on top. GitHub's search endpoint is rate-limited an order of magnitude
|
|
8
|
+
* harder than the list endpoint, so a full-scope sweep — hundreds of findings —
|
|
9
|
+
* spent its whole budget re-discovering the same few dozen Issues, and then
|
|
10
|
+
* degraded the rest of the run to `create`, which is how a sweep opens
|
|
11
|
+
* duplicates of Issues it already filed.
|
|
12
|
+
*
|
|
13
|
+
* The Issues that can possibly match are exactly those carrying an `audit::*`
|
|
14
|
+
* label, and there are tens of them, not hundreds. Listing them **once per run**
|
|
15
|
+
* and indexing their provenance footers answers every exact-fingerprint lookup
|
|
16
|
+
* locally, for free. The search API is then spent only where it is the only
|
|
17
|
+
* thing that can help: a finding with no exact hit, whose fingerprint may have
|
|
18
|
+
* drifted under a rewording.
|
|
19
|
+
*
|
|
20
|
+
* Pure: the caller injects the list port and this module performs no I/O.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
parseFingerprintFooter,
|
|
25
|
+
parseSemanticKeyFooter,
|
|
26
|
+
} from '../findings/route-finding.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Add `record` to the list `map` keys under `key`.
|
|
30
|
+
*
|
|
31
|
+
* @param {Map<string, object[]>} map
|
|
32
|
+
* @param {string} key
|
|
33
|
+
* @param {object} record
|
|
34
|
+
*/
|
|
35
|
+
function push(map, key, record) {
|
|
36
|
+
const bucket = map.get(key);
|
|
37
|
+
if (bucket) bucket.push(record);
|
|
38
|
+
else map.set(key, [record]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Index issues by both provenance footers the audit filers stamp.
|
|
43
|
+
*
|
|
44
|
+
* An issue carrying neither footer is indexed under nothing — it can never
|
|
45
|
+
* confirm a match, exactly as it could not when it came back from a search.
|
|
46
|
+
*
|
|
47
|
+
* @param {Array<{ number: number, state: string, body?: string }>} issues
|
|
48
|
+
* @returns {{ byFingerprint: Map<string, object[]>, bySemanticKey: Map<string, object[]>, size: number }}
|
|
49
|
+
*/
|
|
50
|
+
export function buildIssueIndex(issues) {
|
|
51
|
+
const byFingerprint = new Map();
|
|
52
|
+
const bySemanticKey = new Map();
|
|
53
|
+
const records = (issues ?? []).filter(
|
|
54
|
+
(issue) => typeof issue?.number === 'number',
|
|
55
|
+
);
|
|
56
|
+
for (const issue of records) {
|
|
57
|
+
for (const sha of parseFingerprintFooter(issue.body)) {
|
|
58
|
+
push(byFingerprint, sha, issue);
|
|
59
|
+
}
|
|
60
|
+
for (const key of parseSemanticKeyFooter(issue.body)) {
|
|
61
|
+
push(bySemanticKey, key, issue);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { byFingerprint, bySemanticKey, size: records.length };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The union of the two local lookups for one finding, fingerprint hits first.
|
|
69
|
+
*
|
|
70
|
+
* Order matters downstream: `routeFinding` keeps the first record contributed
|
|
71
|
+
* for an issue number, and the record retrieved by exact identity is the one
|
|
72
|
+
* that should survive into confirmation.
|
|
73
|
+
*
|
|
74
|
+
* @param {{ byFingerprint: Map, bySemanticKey: Map }} index
|
|
75
|
+
* @param {string} sha
|
|
76
|
+
* @param {string} semanticKey
|
|
77
|
+
* @returns {{ exact: object[], pool: object[] }}
|
|
78
|
+
*/
|
|
79
|
+
export function lookupLocally(index, sha, semanticKey) {
|
|
80
|
+
const exact = index.byFingerprint.get(sha) ?? [];
|
|
81
|
+
const byKey = semanticKey ? (index.bySemanticKey.get(semanticKey) ?? []) : [];
|
|
82
|
+
return { exact, pool: [...exact, ...byKey] };
|
|
83
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/audit-to-stories/issues-file.js — the issue corpus dedup checks against,
|
|
3
|
+
* and the one normaliser every source of it goes through.
|
|
4
|
+
*
|
|
5
|
+
* Dedup needs exactly one thing from GitHub: the list of Issues carrying an
|
|
6
|
+
* `audit::*` label. Until Story #5301 the only way to get it was the provider,
|
|
7
|
+
* which spawns `gh`, so a host without a `gh` CLI — a Claude Code cloud
|
|
8
|
+
* sandbox, where `gh` is absent and direct API access is disabled but the
|
|
9
|
+
* GitHub MCP tools work fine — could not dedup at all. Every group classified
|
|
10
|
+
* `create` and a scheduled sweep re-filed findings it had already filed.
|
|
11
|
+
*
|
|
12
|
+
* `--issues-file <path>` breaks that coupling: the host fetches the list by
|
|
13
|
+
* whatever access path it has and hands over a JSON array. Nothing here knows
|
|
14
|
+
* how it was fetched, so MCP, REST, and a cached dump are all equally valid.
|
|
15
|
+
*
|
|
16
|
+
* A file that cannot be read as an issue array is a **hard error**, never a
|
|
17
|
+
* degrade. Falling back would mean "dedup did not run" on precisely the
|
|
18
|
+
* invocation whose whole purpose is that it does — and a create-only plan the
|
|
19
|
+
* operator reads as checked is how the duplicate-filing loop starts.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Flatten one raw issue onto the `{ number, state, title, body }` shape the
|
|
26
|
+
* dedupe module reads, collapsing every closed-ish state spelling (`CLOSED`,
|
|
27
|
+
* `state_reason: not_planned`, …) onto `'closed'`.
|
|
28
|
+
*
|
|
29
|
+
* Shared by both corpus sources — the provider's `searchIssues` hits and the
|
|
30
|
+
* `--issues-file` array — so a host can hand over a raw `list_issues` result
|
|
31
|
+
* verbatim without knowing which spelling this repo's dedup expects.
|
|
32
|
+
*
|
|
33
|
+
* @param {object} hit
|
|
34
|
+
* @returns {{ number: number, state: 'open'|'closed', title: string, body: string }}
|
|
35
|
+
*/
|
|
36
|
+
export function normaliseIssueHit(hit) {
|
|
37
|
+
return {
|
|
38
|
+
number: hit.number,
|
|
39
|
+
state: (hit.state ?? hit.state_reason ?? 'open')
|
|
40
|
+
.toString()
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.includes('closed')
|
|
43
|
+
? 'closed'
|
|
44
|
+
: 'open',
|
|
45
|
+
title: hit.title ?? '',
|
|
46
|
+
body: hit.body ?? '',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Read and normalise a host-supplied issue corpus.
|
|
52
|
+
*
|
|
53
|
+
* `state` is read from either `state` or `state_reason`, so a closed-as-
|
|
54
|
+
* not-planned issue is recognised however the host's API spelled it. Only
|
|
55
|
+
* `number` and `body` are load-bearing: the number identifies the match and
|
|
56
|
+
* the body carries the provenance footers dedup confirms identity against.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} filePath — path to a JSON array of issues.
|
|
59
|
+
* @param {{ readFileSyncImpl?: typeof fs.readFileSync }} [seams]
|
|
60
|
+
* @returns {Array<{ number: number, state: string, title: string, body: string }>}
|
|
61
|
+
* @throws {Error} when the file is missing, unreadable, not JSON, or not an array.
|
|
62
|
+
*/
|
|
63
|
+
export function loadIssuesFile(
|
|
64
|
+
filePath,
|
|
65
|
+
{ readFileSyncImpl = fs.readFileSync } = {},
|
|
66
|
+
) {
|
|
67
|
+
let raw;
|
|
68
|
+
try {
|
|
69
|
+
raw = readFileSyncImpl(filePath, 'utf8');
|
|
70
|
+
} catch (err) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`--issues-file: cannot read "${filePath}" (${err.message}). Dedup needs ` +
|
|
73
|
+
'the issue corpus to check against; running without it would classify ' +
|
|
74
|
+
'every group "create" and re-file findings already tracked.',
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = JSON.parse(raw);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`--issues-file: "${filePath}" is not valid JSON (${err.message}). ` +
|
|
83
|
+
'Expected a JSON array of issues, e.g. the result of listing every ' +
|
|
84
|
+
'issue labelled audit::* with state "all".',
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (!Array.isArray(parsed)) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`--issues-file: "${filePath}" holds ${describeShape(parsed)}, not a JSON ` +
|
|
90
|
+
'array of issues. Pass the issue list itself, not the envelope wrapping it.',
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return parsed.filter(isIssueLike).map(normaliseIssueHit);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Whether one array entry can possibly identify an issue. An entry without a
|
|
98
|
+
* numeric `number` can never confirm a match — exactly as it could not when it
|
|
99
|
+
* came back from a search — so it is dropped rather than indexed under nothing.
|
|
100
|
+
*
|
|
101
|
+
* @param {unknown} entry
|
|
102
|
+
* @returns {boolean}
|
|
103
|
+
*/
|
|
104
|
+
function isIssueLike(entry) {
|
|
105
|
+
return (
|
|
106
|
+
Boolean(entry) &&
|
|
107
|
+
typeof entry === 'object' &&
|
|
108
|
+
typeof entry.number === 'number'
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Name what a non-array payload actually was, so the error points at the fix.
|
|
114
|
+
* @param {unknown} value
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
function describeShape(value) {
|
|
118
|
+
if (value === null) return 'null';
|
|
119
|
+
if (Array.isArray(value)) return 'an array';
|
|
120
|
+
return `a JSON ${typeof value}`;
|
|
121
|
+
}
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* This module closes that hole from both ends:
|
|
13
13
|
*
|
|
14
14
|
* - {@link runLedgerCommit} (`--auto --ledger-commit`) commits the changed
|
|
15
|
-
* ledger onto a
|
|
16
|
-
* and opens a PR against
|
|
15
|
+
* ledger onto a `chore/audit-ledger-<YYYY-MM-DD>-<shortsha>` branch cut
|
|
16
|
+
* from `origin/<base>`, pushes it, and opens a PR against
|
|
17
|
+
* `project.baseBranch` through the `gh` wrapper.
|
|
17
18
|
* Auto-merge is never requested: a ledger PR records machine-derived state
|
|
18
19
|
* a human should glance at, so landing it stays an operator decision.
|
|
19
20
|
* - {@link resolveLedgerSummary} answers the question the *unflagged* sweep
|
|
@@ -27,9 +28,10 @@
|
|
|
27
28
|
* complexity budget does not absorb a git driver.
|
|
28
29
|
*/
|
|
29
30
|
|
|
31
|
+
import { DEFAULT_LEDGER_PATH } from '../findings/audit-ledger.js';
|
|
30
32
|
import { gh as defaultGh } from '../gh-exec.js';
|
|
31
33
|
import { gitSync } from '../git-utils.js';
|
|
32
|
-
import {
|
|
34
|
+
import { openLedgerPullRequest, probeGit } from './ledger-pr.js';
|
|
33
35
|
|
|
34
36
|
/** Fallback base branch when config carries no `project.baseBranch`. */
|
|
35
37
|
const DEFAULT_BASE_BRANCH = 'main';
|
|
@@ -64,43 +66,6 @@ async function resolveBaseBranch(explicit) {
|
|
|
64
66
|
return DEFAULT_BASE_BRANCH;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
/**
|
|
68
|
-
* Run a read-only git probe that must never throw: a checkout with no commits
|
|
69
|
-
* (or no repository at all) is a legitimate answer of "nothing to report",
|
|
70
|
-
* not a crash. The write path below uses {@link runStep} instead, where a
|
|
71
|
-
* failure IS fatal.
|
|
72
|
-
* @param {(cwd: string, ...args: string[]) => string} git
|
|
73
|
-
* @param {string} cwd
|
|
74
|
-
* @param {string[]} args
|
|
75
|
-
* @returns {string} trimmed stdout, or `''` when git failed.
|
|
76
|
-
*/
|
|
77
|
-
function probeGit(git, cwd, args) {
|
|
78
|
-
try {
|
|
79
|
-
const out = git(cwd, ...args);
|
|
80
|
-
return typeof out === 'string' ? out.trim() : '';
|
|
81
|
-
} catch (_) {
|
|
82
|
-
return '';
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Wrap one write step so a git or `gh` failure surfaces as a fatal error that
|
|
88
|
-
* names the step that broke. Accepts sync and async steps alike.
|
|
89
|
-
* @param {string} name
|
|
90
|
-
* @param {() => unknown} fn
|
|
91
|
-
* @returns {Promise<unknown>}
|
|
92
|
-
*/
|
|
93
|
-
async function runStep(name, fn) {
|
|
94
|
-
try {
|
|
95
|
-
return await fn();
|
|
96
|
-
} catch (error) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
`--ledger-commit failed at step "${name}": ${error?.message ?? error}`,
|
|
99
|
-
{ cause: error },
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
69
|
/**
|
|
105
70
|
* Inspect whether the ledger changed and whether this checkout could persist
|
|
106
71
|
* it at all. Module-local: the two exported entry points below are the whole
|
|
@@ -127,14 +92,10 @@ async function assessLedgerPersistence({
|
|
|
127
92
|
git = gitSync,
|
|
128
93
|
} = {}) {
|
|
129
94
|
const base = await resolveBaseBranch(baseBranch);
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
const hasOrigin =
|
|
133
|
-
|
|
134
|
-
.map((line) => line.trim())
|
|
135
|
-
.includes('origin');
|
|
136
|
-
const headBranch = probeGit(git, cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
137
|
-
const onBaseBranch = headBranch === base;
|
|
95
|
+
const probe = (args) => probeGit(git, cwd, args);
|
|
96
|
+
const changed = ledgerIsDirty(probe, ledgerPath);
|
|
97
|
+
const hasOrigin = hasOriginRemote(probe);
|
|
98
|
+
const headBranch = headBranchOf(probe);
|
|
138
99
|
|
|
139
100
|
return {
|
|
140
101
|
ledgerPath,
|
|
@@ -142,11 +103,46 @@ async function assessLedgerPersistence({
|
|
|
142
103
|
changed,
|
|
143
104
|
hasOrigin,
|
|
144
105
|
headBranch,
|
|
145
|
-
onBaseBranch,
|
|
146
|
-
unpersisted: changed && (!hasOrigin ||
|
|
106
|
+
onBaseBranch: headBranch === base,
|
|
107
|
+
unpersisted: changed && (!hasOrigin || headBranch !== base),
|
|
147
108
|
};
|
|
148
109
|
}
|
|
149
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Has the sweep actually written new memory? Scoped to the ledger pathspec, so
|
|
113
|
+
* unrelated dirt in the checkout is never mistaken for it.
|
|
114
|
+
* @param {(args: string[]) => string} probe
|
|
115
|
+
* @param {string} ledgerPath
|
|
116
|
+
* @returns {boolean}
|
|
117
|
+
*/
|
|
118
|
+
function ledgerIsDirty(probe, ledgerPath) {
|
|
119
|
+
return probe(['status', '--porcelain', '--', ledgerPath]).length > 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Is there an `origin` to push to at all? The ephemeral-clone shape that makes
|
|
124
|
+
* a sweep amnesiac usually has none.
|
|
125
|
+
* @param {(args: string[]) => string} probe
|
|
126
|
+
* @returns {boolean}
|
|
127
|
+
*/
|
|
128
|
+
function hasOriginRemote(probe) {
|
|
129
|
+
return probe(['remote'])
|
|
130
|
+
.split('\n')
|
|
131
|
+
.map((line) => line.trim())
|
|
132
|
+
.includes('origin');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The branch HEAD is on, or `''` when the checkout is detached or has no
|
|
137
|
+
* commits — both of which read as "not the base branch", which is the answer
|
|
138
|
+
* the callers need.
|
|
139
|
+
* @param {(args: string[]) => string} probe
|
|
140
|
+
* @returns {string}
|
|
141
|
+
*/
|
|
142
|
+
function headBranchOf(probe) {
|
|
143
|
+
return probe(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
144
|
+
}
|
|
145
|
+
|
|
150
146
|
/**
|
|
151
147
|
* Warn that the reconciled ledger has nowhere to go. Names the file, because
|
|
152
148
|
* "state will be lost" is unactionable without knowing which state.
|
|
@@ -196,34 +192,13 @@ export async function resolveLedgerSummary({
|
|
|
196
192
|
}
|
|
197
193
|
|
|
198
194
|
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* @param {string} ledgerPath
|
|
202
|
-
* @param {string} date
|
|
203
|
-
* @returns {string}
|
|
204
|
-
*/
|
|
205
|
-
function pullRequestBody(ledgerPath, date) {
|
|
206
|
-
return [
|
|
207
|
-
`Reconciles the cross-run audit ledger (\`${ledgerPath}\`) written by the`,
|
|
208
|
-
`unattended \`audit-to-stories --auto\` sweep on ${date}.`,
|
|
209
|
-
'',
|
|
210
|
-
'Ledger-only change — no source, workflow or documentation file is touched.',
|
|
211
|
-
'Merging it is what gives the next sweep a memory: without it the ledger',
|
|
212
|
-
'dies with the checkout and every later run re-proposes findings this one',
|
|
213
|
-
'already filed, and re-surfaces findings a human already rejected.',
|
|
214
|
-
'',
|
|
215
|
-
'Auto-merge is deliberately not requested: the ledger records machine-derived',
|
|
216
|
-
'lifecycle state, and a human glance before it lands is the point.',
|
|
217
|
-
].join('\n');
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Commit the changed ledger onto a dated branch and open a PR for it.
|
|
195
|
+
* Commit the changed ledger onto a unique branch cut from the remote base and
|
|
196
|
+
* open a PR for it.
|
|
222
197
|
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
* caller runs this *after* printing the run summary, so a broken
|
|
226
|
-
* costs the operator the sweep's findings.
|
|
198
|
+
* Assesses the checkout, then hands the whole write sequence to
|
|
199
|
+
* {@link openLedgerPullRequest}. Every git/`gh` failure is fatal and names its
|
|
200
|
+
* step; the caller runs this *after* printing the run summary, so a broken
|
|
201
|
+
* remote never costs the operator the sweep's findings.
|
|
227
202
|
*
|
|
228
203
|
* @param {object} [params]
|
|
229
204
|
* @param {string} [params.ledgerPath]
|
|
@@ -233,7 +208,8 @@ function pullRequestBody(ledgerPath, date) {
|
|
|
233
208
|
* @param {{ pr: { create: (flags: string[]) => Promise<unknown> } }} [params.gh]
|
|
234
209
|
* @param {Date|string|number} [params.now]
|
|
235
210
|
* @returns {Promise<{ committed: boolean, reason?: string, branch?: string,
|
|
236
|
-
* subject?: string, baseBranch?: string,
|
|
211
|
+
* subject?: string, baseBranch?: string, prUrl?: string|null,
|
|
212
|
+
* resumed?: boolean, ledgerPath: string }>}
|
|
237
213
|
*/
|
|
238
214
|
export async function runLedgerCommit({
|
|
239
215
|
ledgerPath = DEFAULT_LEDGER_PATH,
|
|
@@ -249,42 +225,12 @@ export async function runLedgerCommit({
|
|
|
249
225
|
cwd,
|
|
250
226
|
git,
|
|
251
227
|
});
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const date = isoDate(now);
|
|
257
|
-
const branch = `chore/audit-ledger-${date}`;
|
|
258
|
-
const subject = `chore(audit): reconcile audit ledger ${date}`;
|
|
259
|
-
|
|
260
|
-
await runStep('create-branch', () => git(cwd, 'checkout', '-b', branch));
|
|
261
|
-
await runStep('stage-ledger', () => git(cwd, 'add', '--', ledgerPath));
|
|
262
|
-
// The `-- <path>` pathspec is what keeps the commit ledger-only even when
|
|
263
|
-
// the sweep's checkout carries unrelated dirt.
|
|
264
|
-
await runStep('commit-ledger', () =>
|
|
265
|
-
git(cwd, 'commit', '-m', subject, '--', ledgerPath),
|
|
266
|
-
);
|
|
267
|
-
await runStep('push-branch', () =>
|
|
268
|
-
git(cwd, 'push', '--set-upstream', 'origin', branch),
|
|
269
|
-
);
|
|
270
|
-
await runStep('open-pull-request', () =>
|
|
271
|
-
gh.pr.create([
|
|
272
|
-
'--base',
|
|
273
|
-
state.baseBranch,
|
|
274
|
-
'--head',
|
|
275
|
-
branch,
|
|
276
|
-
'--title',
|
|
277
|
-
subject,
|
|
278
|
-
'--body',
|
|
279
|
-
pullRequestBody(ledgerPath, date),
|
|
280
|
-
]),
|
|
281
|
-
);
|
|
282
|
-
|
|
283
|
-
return {
|
|
284
|
-
committed: true,
|
|
285
|
-
branch,
|
|
286
|
-
subject,
|
|
287
|
-
baseBranch: state.baseBranch,
|
|
228
|
+
return openLedgerPullRequest({
|
|
229
|
+
state,
|
|
288
230
|
ledgerPath,
|
|
289
|
-
|
|
231
|
+
cwd,
|
|
232
|
+
git,
|
|
233
|
+
gh,
|
|
234
|
+
date: isoDate(now),
|
|
235
|
+
});
|
|
290
236
|
}
|