mandrel 2.8.0 → 2.10.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/.markdownlint.json +4 -0
- package/.agents/agents/acceptance-critic.md +30 -5
- package/.agents/agents/auditor.md +36 -19
- package/.agents/agents/plan-critic.md +31 -5
- package/.agents/agents/story-worker.md +91 -100
- package/.agents/docs/configuration.md +39 -1
- package/.agents/docs/execution-reference.md +13 -0
- package/.agents/docs/workflows.md +1 -1
- package/.agents/instructions.md +131 -265
- package/.agents/rules/git-conventions.md +47 -83
- package/.agents/rules/orchestration-error-handling.md +28 -0
- package/.agents/schemas/agentrc.schema.json +44 -1
- package/.agents/schemas/validation-evidence.schema.json +3 -1
- package/.agents/scripts/acceptance-eval.js +1 -1
- package/.agents/scripts/apply-quality-bootstrap.js +1 -1
- package/.agents/scripts/audit-to-stories.js +51 -0
- package/.agents/scripts/check-test-temp-hygiene.js +438 -0
- package/.agents/scripts/deliver-recover.js +23 -6
- package/.agents/scripts/lib/audit-suite/index.js +5 -0
- package/.agents/scripts/lib/audit-suite/lens-diff-floor.js +179 -0
- package/.agents/scripts/lib/audit-suite/selector.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +120 -55
- package/.agents/scripts/lib/config/temp-paths.js +121 -1
- package/.agents/scripts/lib/config-settings-schema-delivery.js +30 -0
- package/.agents/scripts/lib/config-settings-schema.js +32 -0
- package/.agents/scripts/lib/findings/semantic-issue-search.js +43 -5
- package/.agents/scripts/lib/observability/metrics-ledger.js +217 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +7 -0
- package/.agents/scripts/lib/observability/terse-result.js +114 -0
- package/.agents/scripts/lib/orchestration/complexity-gate.js +318 -0
- package/.agents/scripts/lib/orchestration/deliver-recover.js +137 -10
- package/.agents/scripts/lib/orchestration/merge-block-class.js +36 -15
- package/.agents/scripts/lib/orchestration/merge-poll.js +213 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +60 -0
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +182 -9
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +29 -2
- package/.agents/scripts/lib/orchestration/plan-metrics.js +31 -82
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +102 -2
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +215 -14
- package/.agents/scripts/lib/orchestration/resolve-stories.js +7 -0
- package/.agents/scripts/lib/orchestration/review-providers/native.js +34 -16
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +221 -8
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +8 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +230 -79
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +55 -14
- package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +9 -3
- package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +89 -1
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +73 -0
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +4 -1
- package/.agents/scripts/lib/orchestration/task-body-validator.js +13 -40
- package/.agents/scripts/lib/story-body/body-format-lints.js +215 -0
- package/.agents/scripts/lib/story-body/story-body.js +18 -2
- package/.agents/scripts/lib/templates/decomposer-prompts.js +29 -6
- package/.agents/scripts/lib/test-env.js +65 -0
- package/.agents/scripts/plan-context.js +66 -9
- package/.agents/scripts/plan-critics.js +115 -3
- package/.agents/scripts/plan-persist.js +11 -1
- package/.agents/scripts/plan-run-epilogue.js +1 -1
- package/.agents/scripts/providers/github/issues.js +54 -7
- package/.agents/scripts/providers/github/search-budget.js +124 -0
- package/.agents/scripts/providers/github/search-query.js +71 -0
- package/.agents/scripts/single-story-confirm-merge.js +79 -10
- package/.agents/scripts/single-story-init.js +19 -3
- package/.agents/scripts/stories-wave-tick.js +1 -1
- package/.agents/scripts/sync-branch-from-base.js +9 -3
- package/.agents/workflows/deliver.md +86 -230
- package/.agents/workflows/helpers/deliver-reference.md +167 -0
- package/.agents/workflows/helpers/deliver-story-reference.md +203 -0
- package/.agents/workflows/helpers/deliver-story.md +114 -422
- package/.agents/workflows/helpers/plan-reference.md +211 -0
- package/.agents/workflows/plan.md +107 -279
- package/docs/CHANGELOG.md +47 -0
- package/package.json +1 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Provider — `/search/issues` fan-out budget (Story #4678).
|
|
3
|
+
*
|
|
4
|
+
* The 30-requests-per-minute cap is a property of GitHub's `/search/issues`
|
|
5
|
+
* endpoint, not of any one caller. A single audit-to-stories scan issues ~2
|
|
6
|
+
* search calls per finding; a 22-group scan therefore blows well past the cap
|
|
7
|
+
* with no throttle of any kind, and every exhausted call then spends its whole
|
|
8
|
+
* transient-retry budget re-issuing a request against an already-empty window.
|
|
9
|
+
*
|
|
10
|
+
* This module owns the throttle at the endpoint seam: a pure, injectable token
|
|
11
|
+
* bucket that `IssuesGateway#searchIssues` awaits before every call, so every
|
|
12
|
+
* caller (audit-to-stories dedup, `lib/duplicate-search.js`, the tickets
|
|
13
|
+
* gateway's `_searchIssues`) inherits one shared budget for free.
|
|
14
|
+
*
|
|
15
|
+
* `now` and `sleep` are injected so unit tests drive the bucket deterministically
|
|
16
|
+
* without wall-clock time.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Default budget: 30 tokens per 60s window, matching GitHub's authenticated
|
|
21
|
+
* Search API cap. When a rate limit is reported with no readable reset, the
|
|
22
|
+
* bucket pauses for one full window before the next `take()` resolves.
|
|
23
|
+
*/
|
|
24
|
+
const SEARCH_BUDGET_DEFAULTS = Object.freeze({
|
|
25
|
+
capacity: 30,
|
|
26
|
+
windowMs: 60_000,
|
|
27
|
+
cooldownMs: 60_000,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a token-bucket search budget.
|
|
32
|
+
*
|
|
33
|
+
* `take()` resolves once a token is available, consuming it; it awaits an
|
|
34
|
+
* accruing token (and any active rate-limit cooldown) rather than failing.
|
|
35
|
+
* `noteRateLimited(resetAtMs)` drains the bucket and blocks every subsequent
|
|
36
|
+
* `take()` until the reported reset (or a fixed cooldown when no reset is
|
|
37
|
+
* readable), so the whole batch pauses **once** instead of each call retrying
|
|
38
|
+
* independently into the empty window.
|
|
39
|
+
*
|
|
40
|
+
* @param {object} [opts]
|
|
41
|
+
* @param {number} [opts.capacity] — max tokens (and burst size).
|
|
42
|
+
* @param {number} [opts.windowMs] — window over which `capacity` tokens accrue.
|
|
43
|
+
* @param {number} [opts.cooldownMs] — pause applied when a rate limit reports
|
|
44
|
+
* no readable reset time.
|
|
45
|
+
* @param {() => number} [opts.now] — millisecond clock (injected for tests).
|
|
46
|
+
* @param {(ms: number) => Promise<void>} [opts.sleep] — delay primitive.
|
|
47
|
+
* @returns {{ take: () => Promise<void>, noteRateLimited: (resetAtMs?: number) => void }}
|
|
48
|
+
*/
|
|
49
|
+
export function createSearchBudget({
|
|
50
|
+
capacity = SEARCH_BUDGET_DEFAULTS.capacity,
|
|
51
|
+
windowMs = SEARCH_BUDGET_DEFAULTS.windowMs,
|
|
52
|
+
cooldownMs = SEARCH_BUDGET_DEFAULTS.cooldownMs,
|
|
53
|
+
now = () => Date.now(),
|
|
54
|
+
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
55
|
+
} = {}) {
|
|
56
|
+
const refillPerMs = capacity / windowMs;
|
|
57
|
+
let tokens = capacity;
|
|
58
|
+
let lastRefillAt = now();
|
|
59
|
+
let blockedUntil = 0;
|
|
60
|
+
|
|
61
|
+
function refill() {
|
|
62
|
+
const at = now();
|
|
63
|
+
const elapsed = at - lastRefillAt;
|
|
64
|
+
if (elapsed > 0) {
|
|
65
|
+
tokens = Math.min(capacity, tokens + elapsed * refillPerMs);
|
|
66
|
+
lastRefillAt = at;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function take() {
|
|
71
|
+
for (;;) {
|
|
72
|
+
const at = now();
|
|
73
|
+
if (blockedUntil > at) {
|
|
74
|
+
await sleep(blockedUntil - at);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
refill();
|
|
78
|
+
if (tokens >= 1) {
|
|
79
|
+
tokens -= 1;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Sleep just long enough for one token to accrue, then re-check.
|
|
83
|
+
const waitMs = Math.max(1, Math.ceil((1 - tokens) / refillPerMs));
|
|
84
|
+
await sleep(waitMs);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function noteRateLimited(resetAtMs) {
|
|
89
|
+
const at = now();
|
|
90
|
+
tokens = 0;
|
|
91
|
+
lastRefillAt = at;
|
|
92
|
+
const until =
|
|
93
|
+
typeof resetAtMs === 'number' && resetAtMs > at
|
|
94
|
+
? resetAtMs
|
|
95
|
+
: at + cooldownMs;
|
|
96
|
+
if (until > blockedUntil) blockedUntil = until;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { take, noteRateLimited };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Process-wide singleton shared by every `searchIssues` caller. Per-process,
|
|
104
|
+
* matching the one-scan-per-checkout model — there is deliberately no
|
|
105
|
+
* cross-process budget.
|
|
106
|
+
*/
|
|
107
|
+
export const searchBudget = createSearchBudget();
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Best-effort extract of a rate-limit reset time (epoch ms) from a thrown
|
|
111
|
+
* error's stderr. GitHub surfaces the reset as an `x-ratelimit-reset` epoch
|
|
112
|
+
* (seconds) header; `gh` echoes response headers onto stderr on failure.
|
|
113
|
+
* Returns `undefined` when no reset is readable so the bucket falls back to a
|
|
114
|
+
* fixed cooldown. Pure — no I/O.
|
|
115
|
+
*
|
|
116
|
+
* @param {unknown} err
|
|
117
|
+
* @returns {number|undefined} reset time in epoch milliseconds, or undefined.
|
|
118
|
+
*/
|
|
119
|
+
export function parseRateLimitResetMs(err) {
|
|
120
|
+
const haystack = [err?.stderr, err?.message].filter(Boolean).join('\n');
|
|
121
|
+
const match = haystack.match(/x-ratelimit-reset:\s*(\d{10})/i);
|
|
122
|
+
if (!match) return undefined;
|
|
123
|
+
return Number.parseInt(match[1], 10) * 1000;
|
|
124
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Provider — composed `/search/issues` query bound (Story #4678).
|
|
3
|
+
*
|
|
4
|
+
* `IssuesGateway#searchIssues` is the only place that knows the *composed* `q`
|
|
5
|
+
* — the caller's free text plus the `repo:<owner>/<repo> type:issue` qualifiers
|
|
6
|
+
* it appends. GitHub Search rejects a query over 256 characters with HTTP 422,
|
|
7
|
+
* which is neither transient nor caught, so an over-long title over a deep path
|
|
8
|
+
* would abort the whole scan. This module owns the defensive guard: it truncates
|
|
9
|
+
* the free-text portion on a whole-token boundary until the composed `q` fits.
|
|
10
|
+
*
|
|
11
|
+
* Pure and unit-testable — no I/O.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* GitHub Search's documented maximum query length, in characters. Module-private
|
|
16
|
+
* — `composeBoundedQuery` is the only supported way to apply it, so the bound
|
|
17
|
+
* cannot drift between call sites.
|
|
18
|
+
*/
|
|
19
|
+
const GITHUB_SEARCH_MAX_QUERY = 256;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Compose a `/search/issues` query from free text plus fixed qualifiers,
|
|
23
|
+
* truncating the free-text portion on a whole-token boundary so the whole
|
|
24
|
+
* composed string is at most `max` characters. Qualifiers are never dropped —
|
|
25
|
+
* they are the load-bearing scope (`repo:` / `type:`) — so when even the
|
|
26
|
+
* qualifiers alone exceed the budget the qualifier string is returned as-is.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} freeText — the caller's free-text search term(s).
|
|
29
|
+
* @param {string[]} qualifiers — fixed qualifier tokens (e.g. `repo:o/r`).
|
|
30
|
+
* @param {number} [max] — character ceiling for the composed query.
|
|
31
|
+
* @returns {string} the composed query, at most `max` characters.
|
|
32
|
+
*/
|
|
33
|
+
export function composeBoundedQuery(
|
|
34
|
+
freeText,
|
|
35
|
+
qualifiers,
|
|
36
|
+
max = GITHUB_SEARCH_MAX_QUERY,
|
|
37
|
+
) {
|
|
38
|
+
const quals = qualifiers.join(' ');
|
|
39
|
+
const free = String(freeText ?? '').trim();
|
|
40
|
+
const full = `${free} ${quals}`.trim();
|
|
41
|
+
if (full.length <= max) return full;
|
|
42
|
+
|
|
43
|
+
// Reserve space for the qualifiers (and the joining space) and fit as many
|
|
44
|
+
// leading free-text tokens as the remaining budget allows.
|
|
45
|
+
const reserve = quals.length + (quals.length > 0 ? 1 : 0);
|
|
46
|
+
const budget = max - reserve;
|
|
47
|
+
const bounded = fitTokens(free.split(/\s+/).filter(Boolean), budget);
|
|
48
|
+
return bounded.length > 0 ? `${bounded} ${quals}`.trim() : quals;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Join as many leading `tokens` as fit within `budget` characters, on a
|
|
53
|
+
* whole-token boundary (space-separated). Returns '' when the budget cannot fit
|
|
54
|
+
* even the first token. Module-private — exercised through
|
|
55
|
+
* {@link composeBoundedQuery}, the only caller.
|
|
56
|
+
*
|
|
57
|
+
* @param {string[]} tokens
|
|
58
|
+
* @param {number} budget
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
function fitTokens(tokens, budget) {
|
|
62
|
+
const kept = [];
|
|
63
|
+
let length = 0;
|
|
64
|
+
for (const token of tokens) {
|
|
65
|
+
const cost = kept.length === 0 ? token.length : token.length + 1;
|
|
66
|
+
if (length + cost > budget) break;
|
|
67
|
+
kept.push(token);
|
|
68
|
+
length += cost;
|
|
69
|
+
}
|
|
70
|
+
return kept.join(' ');
|
|
71
|
+
}
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
* short-circuits to a `noop` envelope.
|
|
28
28
|
*
|
|
29
29
|
* Usage:
|
|
30
|
-
* node single-story-confirm-merge.js --story <STORY_ID> [--pr <n>]
|
|
30
|
+
* node single-story-confirm-merge.js --story <STORY_ID> [--pr <n>] [--wait]
|
|
31
|
+
* [--max-wait-seconds <n>]
|
|
31
32
|
* [--cwd <main-repo>]
|
|
32
33
|
*
|
|
33
34
|
* Exit codes: 0 ok (merged, pending, or noop), 1 error.
|
|
@@ -40,11 +41,13 @@ import { parseSprintArgs } from './lib/cli-args.js';
|
|
|
40
41
|
import { runAsCli } from './lib/cli-utils.js';
|
|
41
42
|
import { resolveConfig } from './lib/config-resolver.js';
|
|
42
43
|
import { formatCliError } from './lib/error-redactor.js';
|
|
43
|
-
import {
|
|
44
|
+
import { createGh } from './lib/gh-exec.js';
|
|
44
45
|
import { getStoryBranch } from './lib/git-utils.js';
|
|
45
46
|
import { Logger } from './lib/Logger.js';
|
|
46
47
|
import { emitTerminalFriction } from './lib/observability/runtime-friction.js';
|
|
48
|
+
import { emitTerseResult } from './lib/observability/terse-result.js';
|
|
47
49
|
import { MERGED_FLIP_FAILED_BLOCK_CLASS } from './lib/orchestration/lifecycle/emit-merge-flip-failed.js';
|
|
50
|
+
import { MERGE_WAIT_GH_TIMEOUT_MS } from './lib/orchestration/merge-poll.js';
|
|
48
51
|
import { parsePrNumber } from './lib/orchestration/single-story-close/phases/code-review.js';
|
|
49
52
|
import { runConfirmMergePhase as defaultRunConfirmMergePhase } from './lib/orchestration/single-story-close/phases/confirm-merge.js';
|
|
50
53
|
import { parseCloseOptions } from './lib/orchestration/single-story-close/phases/options.js';
|
|
@@ -63,6 +66,25 @@ const progress = Logger.createProgress('single-story-confirm-merge', {
|
|
|
63
66
|
stderr: true,
|
|
64
67
|
});
|
|
65
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Default `gh` facade for this CLI, bound to the merge wait's spawn-level
|
|
71
|
+
* timeout (Story #4710). This CLI is the resume surface async mode hands the
|
|
72
|
+
* merge wait to — a background invocation with no host tool ceiling — so an
|
|
73
|
+
* un-timeboxed `gh pr list` / `gh pr view` here could strand the resume the
|
|
74
|
+
* same way an un-timeboxed probe stranded the in-close wait.
|
|
75
|
+
*/
|
|
76
|
+
const defaultGh = createGh(undefined, { timeoutMs: MERGE_WAIT_GH_TIMEOUT_MS });
|
|
77
|
+
|
|
78
|
+
/** One usage string for the throw path and `--help` (Story #4710). */
|
|
79
|
+
const USAGE =
|
|
80
|
+
'Usage: node single-story-confirm-merge.js --story <STORY_ID> [--pr <n>] [--wait] ' +
|
|
81
|
+
'[--max-wait-seconds <n>] [--cwd <main-repo>]\n\n' +
|
|
82
|
+
' --wait resume the bounded merge wait instead of probing once\n' +
|
|
83
|
+
' --max-wait-seconds per-invocation wait bound override, threaded to\n' +
|
|
84
|
+
' resolveMergeWaitConfig exactly as the close does (wins\n' +
|
|
85
|
+
' over delivery.mergeWatch.maxWaitSeconds and the async\n' +
|
|
86
|
+
' probe-window cap; only meaningful with --wait)';
|
|
87
|
+
|
|
66
88
|
/**
|
|
67
89
|
* Read the `--pr <n>` flag from `process.argv` for the direct-CLI path.
|
|
68
90
|
* Injection callers pass `pr` directly and never reach this. Returns the
|
|
@@ -105,6 +127,33 @@ function readWaitFlag() {
|
|
|
105
127
|
}
|
|
106
128
|
}
|
|
107
129
|
|
|
130
|
+
/**
|
|
131
|
+
* `--max-wait-seconds <n>`: per-invocation wait-bound override for the
|
|
132
|
+
* `--wait` resume path (Story #4710). The close CLI already accepted this
|
|
133
|
+
* flag, but the resume CLI — the exact command async mode's `pending`
|
|
134
|
+
* terminal hands off to — did not, so the documented per-run override was
|
|
135
|
+
* unreachable where it mattered most and a slow-CI landing depended on an
|
|
136
|
+
* unbounded chain of short invocations. Threaded to `runConfirmMergePhase`
|
|
137
|
+
* (and thence `resolveMergeWaitConfig`) exactly as close threads its own
|
|
138
|
+
* flag. Returns `undefined` when absent or not a positive integer — the
|
|
139
|
+
* phase's config/default resolution owns that case.
|
|
140
|
+
*
|
|
141
|
+
* @returns {number|undefined}
|
|
142
|
+
*/
|
|
143
|
+
function readMaxWaitSecondsFlag() {
|
|
144
|
+
try {
|
|
145
|
+
const { values } = parseArgs({
|
|
146
|
+
args: process.argv.slice(2),
|
|
147
|
+
options: { 'max-wait-seconds': { type: 'string' } },
|
|
148
|
+
strict: false,
|
|
149
|
+
});
|
|
150
|
+
const parsed = Number.parseInt(String(values['max-wait-seconds']), 10);
|
|
151
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
152
|
+
} catch {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
108
157
|
/**
|
|
109
158
|
* Resolve the PR number for the Story branch when one was not passed on
|
|
110
159
|
* the CLI. Probes `gh pr list --head <branch> --state all` (the merged PR
|
|
@@ -142,11 +191,19 @@ async function resolvePrNumber({ cwd, storyBranch, gh }) {
|
|
|
142
191
|
* exits via `process.exit` the moment `main` resolves.
|
|
143
192
|
*/
|
|
144
193
|
async function logConfirmResult(result, terminal, config) {
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
194
|
+
// Story #4685 — full detail to a temp log; the agent acts on the terminal
|
|
195
|
+
// envelope emitted below. The summary line keeps the at-a-glance fields.
|
|
196
|
+
emitTerseResult({
|
|
197
|
+
label: 'CONFIRM MERGE RESULT',
|
|
198
|
+
result,
|
|
199
|
+
scope: result?.storyId,
|
|
200
|
+
summary: {
|
|
201
|
+
storyId: result?.storyId,
|
|
202
|
+
action: result?.action,
|
|
203
|
+
reason: result?.reason,
|
|
204
|
+
status: terminal?.status,
|
|
205
|
+
},
|
|
206
|
+
});
|
|
150
207
|
emitTerminalEnvelope(terminal);
|
|
151
208
|
await emitTerminalFriction({ envelope: terminal, config });
|
|
152
209
|
return { success: terminal.status !== 'failed', result, terminal };
|
|
@@ -253,6 +310,7 @@ export async function runConfirmMerge({
|
|
|
253
310
|
cwd: cwdParam,
|
|
254
311
|
pr: prParam,
|
|
255
312
|
wait: waitParam,
|
|
313
|
+
maxWaitSeconds: maxWaitSecondsParam,
|
|
256
314
|
injectedProvider,
|
|
257
315
|
injectedConfig,
|
|
258
316
|
injectedGh,
|
|
@@ -266,11 +324,10 @@ export async function runConfirmMerge({
|
|
|
266
324
|
});
|
|
267
325
|
|
|
268
326
|
if (!storyId) {
|
|
269
|
-
throw new Error(
|
|
270
|
-
'Usage: node single-story-confirm-merge.js --story <STORY_ID> [--pr <n>] [--wait] [--cwd <main-repo>]',
|
|
271
|
-
);
|
|
327
|
+
throw new Error(USAGE);
|
|
272
328
|
}
|
|
273
329
|
const wait = waitParam ?? readWaitFlag();
|
|
330
|
+
const maxWaitSeconds = maxWaitSecondsParam ?? readMaxWaitSecondsFlag();
|
|
274
331
|
|
|
275
332
|
const startedAtMs = Date.now();
|
|
276
333
|
const config = injectedConfig || resolveConfig({ cwd });
|
|
@@ -337,6 +394,10 @@ export async function runConfirmMerge({
|
|
|
337
394
|
// The close already armed it; this CLI is resuming that wait, not
|
|
338
395
|
// deciding whether to arm.
|
|
339
396
|
autoMergeEnabled: true,
|
|
397
|
+
// The per-run override (`--max-wait-seconds`), resolved by
|
|
398
|
+
// `resolveMergeWaitConfig` exactly as the close resolves its own flag —
|
|
399
|
+
// it wins over the config value and the async probe-window cap.
|
|
400
|
+
maxWaitSeconds,
|
|
340
401
|
provider,
|
|
341
402
|
config,
|
|
342
403
|
progress,
|
|
@@ -441,6 +502,14 @@ export async function runConfirmMerge({
|
|
|
441
502
|
* or none at all; "none at all" is what Story #4543 removes.
|
|
442
503
|
*/
|
|
443
504
|
async function main() {
|
|
505
|
+
if (process.argv.includes('--help')) {
|
|
506
|
+
// Print usage (including --max-wait-seconds) and exit cleanly — the
|
|
507
|
+
// resume-path override is only discoverable if the CLI can say it exists.
|
|
508
|
+
// `process.stdout.write` (not console.log) keeps the CLI within the
|
|
509
|
+
// no-console repo invariant while still writing help to stdout.
|
|
510
|
+
process.stdout.write(`${USAGE}\n`);
|
|
511
|
+
return 0;
|
|
512
|
+
}
|
|
444
513
|
try {
|
|
445
514
|
const outcome = await runConfirmMerge();
|
|
446
515
|
return exitCodeForTerminal(outcome?.terminal ?? { status: 'failed' });
|
|
@@ -55,6 +55,7 @@ import { getStoryBranch, gitSpawn, gitSync } from './lib/git-utils.js';
|
|
|
55
55
|
import { Logger } from './lib/Logger.js';
|
|
56
56
|
import { TYPE_LABELS } from './lib/label-constants.js';
|
|
57
57
|
import { setActiveStoryEnv } from './lib/observability/active-story-env.js';
|
|
58
|
+
import { emitTerseResult } from './lib/observability/terse-result.js';
|
|
58
59
|
import {
|
|
59
60
|
executeFastForward,
|
|
60
61
|
planFastForward,
|
|
@@ -769,9 +770,24 @@ export async function runSingleStoryInit({
|
|
|
769
770
|
}
|
|
770
771
|
}
|
|
771
772
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
773
|
+
// Story #4685 — route the full result to a temp log and emit a single-line
|
|
774
|
+
// summary carrying the fields the orchestrating agent acts on (workCwd,
|
|
775
|
+
// remoteVerified). The `## Spec` names this the hot-path stdout to quiet.
|
|
776
|
+
emitTerseResult({
|
|
777
|
+
label: 'STORY INIT RESULT',
|
|
778
|
+
result,
|
|
779
|
+
scope: storyId,
|
|
780
|
+
logDir: path.join(cwd, 'temp', 'orchestration'),
|
|
781
|
+
summary: {
|
|
782
|
+
storyId,
|
|
783
|
+
storyBranch,
|
|
784
|
+
workCwd,
|
|
785
|
+
worktreeCreated,
|
|
786
|
+
dependenciesInstalled,
|
|
787
|
+
remoteVerified: result.remoteVerified,
|
|
788
|
+
dryRun,
|
|
789
|
+
},
|
|
790
|
+
});
|
|
775
791
|
progress(
|
|
776
792
|
'DONE',
|
|
777
793
|
dryRun
|
|
@@ -34,6 +34,7 @@ import { runAsCli } from './lib/cli-utils.js';
|
|
|
34
34
|
import { syncBranchFromBase } from './lib/git/sync-from-base.js';
|
|
35
35
|
import { gitSpawn, gitSync } from './lib/git-utils.js';
|
|
36
36
|
import { Logger } from './lib/Logger.js';
|
|
37
|
+
import { emitTerseResult } from './lib/observability/terse-result.js';
|
|
37
38
|
import { PROJECT_ROOT } from './lib/project-root.js';
|
|
38
39
|
|
|
39
40
|
const progress = Logger.createProgress('sync-branch-from-base', {
|
|
@@ -93,9 +94,14 @@ export async function runSyncBranchFromBase(opts = {}) {
|
|
|
93
94
|
gitSpawn,
|
|
94
95
|
});
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
// Story #4685 — full detail to a temp log; emit a single summary line.
|
|
98
|
+
emitTerseResult({
|
|
99
|
+
label: 'SYNC RESULT',
|
|
100
|
+
result,
|
|
101
|
+
scope: branch,
|
|
102
|
+
logDir: path.join(cwd, 'temp', 'orchestration'),
|
|
103
|
+
summary: { branch, base, synced: result.synced, kind: result.kind },
|
|
104
|
+
});
|
|
99
105
|
|
|
100
106
|
if (!result.synced) {
|
|
101
107
|
const detail =
|