mandrel 1.90.0 → 1.91.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/scripts/epic-deliver-preflight.js +37 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +32 -0
- package/.agents/scripts/lib/orchestration/remote-verifier.js +165 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +10 -0
- package/.agents/scripts/single-story-init.js +22 -0
- package/.agents/workflows/deliver.md +8 -0
- package/.agents/workflows/helpers/deliver-epic.md +11 -0
- package/.agents/workflows/helpers/single-story-deliver.md +8 -0
- package/docs/CHANGELOG.md +7 -0
- package/package.json +1 -1
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
* `storyCount`, `installCostSeconds`, `dependencyDepth`,
|
|
12
12
|
* `githubApiRequests`, `claudeQuotaTokens`, plus `breaches`
|
|
13
13
|
* (the non-empty subset of `delivery.preflight.max*` thresholds the
|
|
14
|
-
* estimate exceeds)
|
|
14
|
+
* estimate exceeds), plus `remoteVerified` / `remoteProbe` — the
|
|
15
|
+
* issue #4483 deterministic remote evidence (`git remote get-url
|
|
16
|
+
* origin` + bounded `git ls-remote origin HEAD`). On
|
|
17
|
+
* `remoteVerified: false` the workflow MUST flip the Epic to
|
|
18
|
+
* `agent::blocked` quoting `remoteProbe.detail` — inline delivery to
|
|
19
|
+
* local `main` is never a sanctioned fallback.
|
|
15
20
|
* 2. When `--post` is set (and `--dry-run` is not), an upserted
|
|
16
21
|
* `delivery-preflight` structured comment on the Epic ticket so
|
|
17
22
|
* reviewers reading the Epic discover the same numbers without
|
|
@@ -66,6 +71,7 @@ import {
|
|
|
66
71
|
computeBaseSha,
|
|
67
72
|
writePreflightCache,
|
|
68
73
|
} from './lib/orchestration/preflight-cache.js';
|
|
74
|
+
import { verifyRemote } from './lib/orchestration/remote-verifier.js';
|
|
69
75
|
import { upsertStructuredComment } from './lib/orchestration/ticketing.js';
|
|
70
76
|
import { createProvider } from './lib/provider-factory.js';
|
|
71
77
|
|
|
@@ -218,10 +224,23 @@ export function renderPreflightBody({
|
|
|
218
224
|
estimate,
|
|
219
225
|
breaches,
|
|
220
226
|
thresholds,
|
|
227
|
+
remote,
|
|
221
228
|
}) {
|
|
222
229
|
const lines = [];
|
|
223
230
|
lines.push(`### 🛫 Delivery preflight — Epic #${epicId}`);
|
|
224
231
|
lines.push('');
|
|
232
|
+
// Issue #4483 — verified remote evidence at entry. Rendered before the
|
|
233
|
+
// metric table so a reviewer (and the orchestrating agent) sees the
|
|
234
|
+
// land-or-block fact first. Omitted when the caller has no probe result
|
|
235
|
+
// (legacy callers / tests that only exercise the estimate math).
|
|
236
|
+
if (remote) {
|
|
237
|
+
lines.push(
|
|
238
|
+
remote.remoteVerified
|
|
239
|
+
? `✅ **remoteVerified: true** — ${remote.detail}`
|
|
240
|
+
: `⛔ **remoteVerified: false** — ${remote.detail} — \`/deliver\` MUST transition the Epic to \`agent::blocked\` quoting this probe; inline delivery to local \`main\` is forbidden.`,
|
|
241
|
+
);
|
|
242
|
+
lines.push('');
|
|
243
|
+
}
|
|
225
244
|
lines.push('| Metric | Estimate | Threshold |');
|
|
226
245
|
lines.push('| --- | ---: | ---: |');
|
|
227
246
|
const rows = [
|
|
@@ -276,6 +295,7 @@ export function renderPreflightBody({
|
|
|
276
295
|
* perStoryClaudeTokens?: number,
|
|
277
296
|
* injectedProvider?: object,
|
|
278
297
|
* injectedConfig?: object,
|
|
298
|
+
* verifyRemoteFn?: typeof verifyRemote,
|
|
279
299
|
* }} args
|
|
280
300
|
*/
|
|
281
301
|
export async function runPreflight({
|
|
@@ -288,6 +308,7 @@ export async function runPreflight({
|
|
|
288
308
|
perStoryClaudeTokens,
|
|
289
309
|
injectedProvider,
|
|
290
310
|
injectedConfig,
|
|
311
|
+
verifyRemoteFn = verifyRemote,
|
|
291
312
|
}) {
|
|
292
313
|
if (!Number.isInteger(epicId) || epicId <= 0) {
|
|
293
314
|
throw new TypeError('runPreflight: --epic must be a positive integer');
|
|
@@ -297,6 +318,13 @@ export async function runPreflight({
|
|
|
297
318
|
const provider = injectedProvider ?? createProvider(config);
|
|
298
319
|
const thresholds = getPreflight(config);
|
|
299
320
|
|
|
321
|
+
// Issue #4483 — deterministic remote evidence at entry. Probe BEFORE any
|
|
322
|
+
// provider work so the envelope always carries verified fact (not the
|
|
323
|
+
// agent's perception) about whether a live, pushable `origin` exists.
|
|
324
|
+
// The CLI records; the `/deliver` workflow owns the `agent::blocked`
|
|
325
|
+
// transition on `remoteVerified: false` (same split as breach handling).
|
|
326
|
+
const remote = verifyRemoteFn({ cwd });
|
|
327
|
+
|
|
300
328
|
// Compose the same two phases /deliver Phase 1 runs so the
|
|
301
329
|
// preflight numbers match the actual dispatch plan.
|
|
302
330
|
const ctx = { epicId, provider };
|
|
@@ -348,6 +376,13 @@ export async function runPreflight({
|
|
|
348
376
|
thresholds,
|
|
349
377
|
baseSha,
|
|
350
378
|
cacheWritten,
|
|
379
|
+
// Issue #4483 — verified remote evidence. `remoteVerified: false`
|
|
380
|
+
// REQUIRES the workflow to block explicitly (never build inline).
|
|
381
|
+
remoteVerified: remote.remoteVerified,
|
|
382
|
+
remoteProbe: {
|
|
383
|
+
remoteUrl: remote.remoteUrl,
|
|
384
|
+
detail: remote.detail,
|
|
385
|
+
},
|
|
351
386
|
};
|
|
352
387
|
|
|
353
388
|
if (post && !dryRun) {
|
|
@@ -356,6 +391,7 @@ export async function runPreflight({
|
|
|
356
391
|
estimate,
|
|
357
392
|
breaches,
|
|
358
393
|
thresholds,
|
|
394
|
+
remote,
|
|
359
395
|
});
|
|
360
396
|
await upsertStructuredComment(provider, epicId, 'delivery-preflight', body);
|
|
361
397
|
envelope.commentUpserted = true;
|
|
@@ -82,6 +82,7 @@ import {
|
|
|
82
82
|
openOrLocatePr as defaultOpenOrLocatePr,
|
|
83
83
|
} from '../../finalize/open-or-locate-pr.js';
|
|
84
84
|
import { postHandoffComment as defaultPostHandoffComment } from '../../finalize/post-handoff-comment.js';
|
|
85
|
+
import { probeRemoteBranch as defaultProbeRemoteBranch } from '../../remote-verifier.js';
|
|
85
86
|
|
|
86
87
|
/**
|
|
87
88
|
* Build the production default `runFinalizeFn` that composes the
|
|
@@ -103,12 +104,22 @@ import { postHandoffComment as defaultPostHandoffComment } from '../../finalize/
|
|
|
103
104
|
* contract is identical and `markPrReady` is a no-op on an already-ready
|
|
104
105
|
* PR, so replay stays idempotent.
|
|
105
106
|
*
|
|
107
|
+
* Issue #4483 — deterministic land-or-block backstop. Before opening (or
|
|
108
|
+
* readying) the PR, finalize asserts the delivery branch `epic/<id>`
|
|
109
|
+
* actually exists on origin. A delivery that was never pushed — e.g. an
|
|
110
|
+
* agent that built the Epic inline on local `main` and skipped the
|
|
111
|
+
* orchestration — MUST surface as an explicit
|
|
112
|
+
* `delivery-branch-missing-on-origin` blocker (which keeps the Epic at
|
|
113
|
+
* `agent::blocked`), never a declared success. The probe is bounded
|
|
114
|
+
* (timeout + SIGKILL) so a hung remote degrades to a blocker too.
|
|
115
|
+
*
|
|
106
116
|
* @param {{
|
|
107
117
|
* provider?: object|null,
|
|
108
118
|
* earlyPr?: boolean,
|
|
109
119
|
* openOrLocatePrFn?: typeof defaultOpenOrLocatePr,
|
|
110
120
|
* markPrReadyFn?: typeof defaultMarkPrReady,
|
|
111
121
|
* postHandoffCommentFn?: typeof defaultPostHandoffComment,
|
|
122
|
+
* probeRemoteBranchFn?: typeof defaultProbeRemoteBranch,
|
|
112
123
|
* }} deps
|
|
113
124
|
*/
|
|
114
125
|
export function composeBusOwnedFinalize(deps = {}) {
|
|
@@ -116,6 +127,8 @@ export function composeBusOwnedFinalize(deps = {}) {
|
|
|
116
127
|
const markPrReadyFn = deps.markPrReadyFn ?? defaultMarkPrReady;
|
|
117
128
|
const postHandoffCommentFn =
|
|
118
129
|
deps.postHandoffCommentFn ?? defaultPostHandoffComment;
|
|
130
|
+
const probeRemoteBranchFn =
|
|
131
|
+
deps.probeRemoteBranchFn ?? defaultProbeRemoteBranch;
|
|
119
132
|
const provider = deps.provider ?? null;
|
|
120
133
|
const earlyPr = deps.earlyPr !== false;
|
|
121
134
|
|
|
@@ -128,6 +141,25 @@ export function composeBusOwnedFinalize(deps = {}) {
|
|
|
128
141
|
},
|
|
129
142
|
};
|
|
130
143
|
}
|
|
144
|
+
|
|
145
|
+
// Issue #4483 backstop — the delivery branch MUST be on origin before
|
|
146
|
+
// finalize declares any success. A never-pushed branch is the silent
|
|
147
|
+
// local-main failure shape; block explicitly with the probe detail.
|
|
148
|
+
let branchProbe;
|
|
149
|
+
try {
|
|
150
|
+
branchProbe = probeRemoteBranchFn({ branch: `epic/${epicId}`, cwd });
|
|
151
|
+
} catch (err) {
|
|
152
|
+
branchProbe = { exists: false, detail: err?.message ?? String(err) };
|
|
153
|
+
}
|
|
154
|
+
if (!branchProbe.exists) {
|
|
155
|
+
return {
|
|
156
|
+
blocker: {
|
|
157
|
+
reason: 'delivery-branch-missing-on-origin',
|
|
158
|
+
detail: `epic/${epicId} is not on origin — the delivery was never pushed; refusing to finalize (issue #4483). Probe: ${branchProbe.detail}`,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
131
163
|
let openResult;
|
|
132
164
|
try {
|
|
133
165
|
openResult = await openOrLocatePrFn({
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// .agents/scripts/lib/orchestration/remote-verifier.js
|
|
2
|
+
/**
|
|
3
|
+
* remote-verifier.js — deterministic "is there a live, pushable remote?"
|
|
4
|
+
* evidence for the delivery entry seams. Issue #4483.
|
|
5
|
+
*
|
|
6
|
+
* `/deliver` could silently shortcut the entire orchestration — building
|
|
7
|
+
* the delivery inline and committing to local `main` without pushing —
|
|
8
|
+
* when the driving agent *perceived* the environment had no live GitHub
|
|
9
|
+
* remote. The judgment was vibes, not fact. This module gives the entry
|
|
10
|
+
* seams (`epic-deliver-preflight.js`, `single-story-init.js`) a verified
|
|
11
|
+
* probe result to record in their envelopes so the workflow can branch on
|
|
12
|
+
* `remoteVerified: true|false` deterministically: use the remote, or
|
|
13
|
+
* transition to `agent::blocked` quoting the probe output — never a
|
|
14
|
+
* silent local build.
|
|
15
|
+
*
|
|
16
|
+
* Two probes, both bounded (a hung git spawn must not park the entry
|
|
17
|
+
* seam — mirrors the `ghPrListHead` timeout contract in `finalizer.js`):
|
|
18
|
+
*
|
|
19
|
+
* 1. `git remote get-url origin` — is an `origin` remote configured?
|
|
20
|
+
* 2. `git ls-remote origin HEAD` — is it reachable with current auth?
|
|
21
|
+
*
|
|
22
|
+
* `remoteVerified` is true only when BOTH succeed. The CLI callers do
|
|
23
|
+
* NOT flip labels on a false result — the workflow owns the
|
|
24
|
+
* `agent::blocked` transition (same division of labour as the preflight
|
|
25
|
+
* breach handling).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { spawnSync } from 'node:child_process';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Bounded timeout for each git probe. `ls-remote` is a network call;
|
|
32
|
+
* SIGKILL at the bound so an unreachable or hanging remote degrades to a
|
|
33
|
+
* deterministic `remoteVerified: false` instead of a stuck entry seam.
|
|
34
|
+
*/
|
|
35
|
+
export const REMOTE_PROBE_TIMEOUT_MS = 30_000;
|
|
36
|
+
|
|
37
|
+
function runProbe({ args, cwd, spawnFn, timeoutMs }) {
|
|
38
|
+
const result = spawnFn('git', args, {
|
|
39
|
+
cwd,
|
|
40
|
+
encoding: 'utf-8',
|
|
41
|
+
shell: false,
|
|
42
|
+
timeout: timeoutMs,
|
|
43
|
+
killSignal: 'SIGKILL',
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
args: ['git', ...args].join(' '),
|
|
47
|
+
status: result.status ?? 1,
|
|
48
|
+
stdout: (result.stdout ?? '').trim(),
|
|
49
|
+
stderr: (result.stderr ?? '').trim(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Probe the `origin` remote for existence + reachability.
|
|
55
|
+
*
|
|
56
|
+
* @param {{
|
|
57
|
+
* cwd?: string,
|
|
58
|
+
* spawnFn?: typeof spawnSync,
|
|
59
|
+
* timeoutMs?: number,
|
|
60
|
+
* }} [opts]
|
|
61
|
+
* @returns {{
|
|
62
|
+
* remoteVerified: boolean,
|
|
63
|
+
* remoteUrl: string|null,
|
|
64
|
+
* detail: string,
|
|
65
|
+
* probes: {
|
|
66
|
+
* getUrl: { args: string, status: number, stdout: string, stderr: string },
|
|
67
|
+
* lsRemote: { args: string, status: number, stdout: string, stderr: string }|null,
|
|
68
|
+
* },
|
|
69
|
+
* }}
|
|
70
|
+
*/
|
|
71
|
+
export function verifyRemote({
|
|
72
|
+
cwd = process.cwd(),
|
|
73
|
+
spawnFn = spawnSync,
|
|
74
|
+
timeoutMs = REMOTE_PROBE_TIMEOUT_MS,
|
|
75
|
+
} = {}) {
|
|
76
|
+
const getUrl = runProbe({
|
|
77
|
+
args: ['remote', 'get-url', 'origin'],
|
|
78
|
+
cwd,
|
|
79
|
+
spawnFn,
|
|
80
|
+
timeoutMs,
|
|
81
|
+
});
|
|
82
|
+
if (getUrl.status !== 0) {
|
|
83
|
+
return {
|
|
84
|
+
remoteVerified: false,
|
|
85
|
+
remoteUrl: null,
|
|
86
|
+
detail: `no 'origin' remote configured — \`${getUrl.args}\` exited ${getUrl.status}: ${getUrl.stderr || '(no stderr)'}`,
|
|
87
|
+
probes: { getUrl, lsRemote: null },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const remoteUrl = getUrl.stdout;
|
|
91
|
+
|
|
92
|
+
const lsRemote = runProbe({
|
|
93
|
+
args: ['ls-remote', 'origin', 'HEAD'],
|
|
94
|
+
cwd,
|
|
95
|
+
spawnFn,
|
|
96
|
+
timeoutMs,
|
|
97
|
+
});
|
|
98
|
+
if (lsRemote.status !== 0 || lsRemote.stdout.length === 0) {
|
|
99
|
+
return {
|
|
100
|
+
remoteVerified: false,
|
|
101
|
+
remoteUrl,
|
|
102
|
+
detail: `'origin' (${remoteUrl}) is unreachable — \`${lsRemote.args}\` exited ${lsRemote.status}: ${lsRemote.stderr || '(empty ls-remote output)'}`,
|
|
103
|
+
probes: { getUrl, lsRemote },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
remoteVerified: true,
|
|
109
|
+
remoteUrl,
|
|
110
|
+
detail: `origin verified (${remoteUrl}); ls-remote HEAD → ${lsRemote.stdout.split(/\s+/)[0]}`,
|
|
111
|
+
probes: { getUrl, lsRemote },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Probe whether a specific branch exists on `origin` — the deterministic
|
|
117
|
+
* finalize backstop (issue #4483 fix direction 3): a delivery branch that
|
|
118
|
+
* was never pushed MUST fail finalize with an explicit blocker rather
|
|
119
|
+
* than let the run declare success.
|
|
120
|
+
*
|
|
121
|
+
* Distinct from `git-branch-lifecycle.js#branchExistsRemotely` in two
|
|
122
|
+
* load-bearing ways: the spawn is bounded (timeout + SIGKILL, so a hung
|
|
123
|
+
* remote cannot park the finalize seam) and the result carries the probe
|
|
124
|
+
* detail for the blocker envelope instead of a bare boolean.
|
|
125
|
+
*
|
|
126
|
+
* @param {{
|
|
127
|
+
* branch: string,
|
|
128
|
+
* cwd?: string,
|
|
129
|
+
* spawnFn?: typeof spawnSync,
|
|
130
|
+
* timeoutMs?: number,
|
|
131
|
+
* }} opts
|
|
132
|
+
* @returns {{ exists: boolean, detail: string }}
|
|
133
|
+
*/
|
|
134
|
+
export function probeRemoteBranch({
|
|
135
|
+
branch,
|
|
136
|
+
cwd = process.cwd(),
|
|
137
|
+
spawnFn = spawnSync,
|
|
138
|
+
timeoutMs = REMOTE_PROBE_TIMEOUT_MS,
|
|
139
|
+
}) {
|
|
140
|
+
if (typeof branch !== 'string' || branch.length === 0) {
|
|
141
|
+
throw new TypeError('probeRemoteBranch: branch must be a non-empty string');
|
|
142
|
+
}
|
|
143
|
+
const probe = runProbe({
|
|
144
|
+
args: ['ls-remote', '--heads', 'origin', branch],
|
|
145
|
+
cwd,
|
|
146
|
+
spawnFn,
|
|
147
|
+
timeoutMs,
|
|
148
|
+
});
|
|
149
|
+
if (probe.status !== 0) {
|
|
150
|
+
return {
|
|
151
|
+
exists: false,
|
|
152
|
+
detail: `\`${probe.args}\` exited ${probe.status}: ${probe.stderr || '(no stderr)'}`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (probe.stdout.length === 0) {
|
|
156
|
+
return {
|
|
157
|
+
exists: false,
|
|
158
|
+
detail: `\`${probe.args}\` found no ref — ${branch} was never pushed to origin`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
exists: true,
|
|
163
|
+
detail: `${branch} on origin at ${probe.stdout.split(/\s+/)[0]}`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -5,6 +5,16 @@
|
|
|
5
5
|
* subsequent fetches are cheap. A push failure raises so the caller
|
|
6
6
|
* fails non-zero — the operator must resolve before retrying.
|
|
7
7
|
*
|
|
8
|
+
* Issue #4483 — this phase IS the standalone path's deterministic
|
|
9
|
+
* land-or-block backstop (the counterpart to the Epic finalize seam's
|
|
10
|
+
* `delivery-branch-missing-on-origin` blocker). `git push` exits 0 only
|
|
11
|
+
* after origin has accepted the ref, so the throw below is the origin
|
|
12
|
+
* assertion: every later close phase (PR open, auto-merge, the
|
|
13
|
+
* `closeResult` success envelope) is unreachable unless the Story branch
|
|
14
|
+
* verifiably landed on origin. No post-push `ls-remote` re-probe is
|
|
15
|
+
* added because it would re-ask a question the push exit code already
|
|
16
|
+
* answered authoritatively.
|
|
17
|
+
*
|
|
8
18
|
* `gitSync` is accepted as an injected dependency rather than statically
|
|
9
19
|
* imported so the caller's (cache-busted) binding wins. The
|
|
10
20
|
* `single-story-close.js` orchestrator owns the static import; test
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
executeFastForward,
|
|
60
60
|
planFastForward,
|
|
61
61
|
} from './lib/orchestration/git-cleanup/phases/fast-forward.js';
|
|
62
|
+
import { verifyRemote } from './lib/orchestration/remote-verifier.js';
|
|
62
63
|
import { acquireStoryLease } from './lib/orchestration/single-story-lease-guard.js';
|
|
63
64
|
import {
|
|
64
65
|
STATE_LABELS,
|
|
@@ -446,6 +447,7 @@ export async function runSingleStoryInit({
|
|
|
446
447
|
injectedAcquireLease,
|
|
447
448
|
steal = false,
|
|
448
449
|
leaseNow,
|
|
450
|
+
injectedVerifyRemote,
|
|
449
451
|
} = {}) {
|
|
450
452
|
const parsed =
|
|
451
453
|
storyIdParam !== undefined
|
|
@@ -487,6 +489,20 @@ export async function runSingleStoryInit({
|
|
|
487
489
|
);
|
|
488
490
|
progress('INIT', `Initializing standalone Story #${storyId}...`);
|
|
489
491
|
|
|
492
|
+
// Issue #4483 — deterministic remote evidence at the standalone entry
|
|
493
|
+
// seam (the counterpart to `epic-deliver-preflight.js`'s probe). The
|
|
494
|
+
// probe is read-only, so it runs under --dry-run too. The CLI records
|
|
495
|
+
// the fact; the workflow owns the `agent::blocked` transition on
|
|
496
|
+
// `remoteVerified: false` — inline delivery to local `main` is never a
|
|
497
|
+
// sanctioned fallback.
|
|
498
|
+
const remote = (injectedVerifyRemote ?? verifyRemote)({ cwd });
|
|
499
|
+
progress(
|
|
500
|
+
'REMOTE',
|
|
501
|
+
remote.remoteVerified
|
|
502
|
+
? `✅ remoteVerified=true — ${remote.detail}`
|
|
503
|
+
: `⛔ remoteVerified=false — ${remote.detail}`,
|
|
504
|
+
);
|
|
505
|
+
|
|
490
506
|
const story = await provider.getTicket(storyId);
|
|
491
507
|
assertDeliverableStory(story, storyId);
|
|
492
508
|
|
|
@@ -563,6 +579,9 @@ export async function runSingleStoryInit({
|
|
|
563
579
|
dependenciesInstalled,
|
|
564
580
|
installFailed: installStatus.status === 'failed',
|
|
565
581
|
dryRun,
|
|
582
|
+
// Issue #4483 — verified remote evidence for the orchestrating agent.
|
|
583
|
+
remoteVerified: remote.remoteVerified,
|
|
584
|
+
remoteProbe: { remoteUrl: remote.remoteUrl, detail: remote.detail },
|
|
566
585
|
};
|
|
567
586
|
|
|
568
587
|
// Upsert the `story-init` structured comment + flip Story to executing.
|
|
@@ -632,6 +651,8 @@ export function renderSingleStoryInitComment(result) {
|
|
|
632
651
|
worktreeCreated: result.worktreeCreated,
|
|
633
652
|
dependenciesInstalled: result.dependenciesInstalled,
|
|
634
653
|
installStatus: result.installStatus,
|
|
654
|
+
remoteVerified: result.remoteVerified,
|
|
655
|
+
remoteProbe: result.remoteProbe,
|
|
635
656
|
};
|
|
636
657
|
return [
|
|
637
658
|
'## Story init (standalone)',
|
|
@@ -641,6 +662,7 @@ export function renderSingleStoryInitComment(result) {
|
|
|
641
662
|
`- **baseBranch:** \`${result.baseBranch}\``,
|
|
642
663
|
`- **workCwd:** \`${result.workCwd}\``,
|
|
643
664
|
`- **worktreeEnabled:** \`${result.worktreeEnabled}\``,
|
|
665
|
+
`- **remoteVerified:** \`${result.remoteVerified}\``,
|
|
644
666
|
`- **dependenciesInstalled:** \`${result.dependenciesInstalled}\``,
|
|
645
667
|
'',
|
|
646
668
|
'```json',
|
|
@@ -131,6 +131,14 @@ the standalone segment; segments themselves remain strictly sequential.
|
|
|
131
131
|
|
|
132
132
|
## Constraints
|
|
133
133
|
|
|
134
|
+
- **Land or block — never a silent local build (issue #4483).** The
|
|
135
|
+
helpers' orchestration path (worktrees, `story-<id>`/`epic/<id>` branches,
|
|
136
|
+
close-validation, PR) is the ONLY sanctioned delivery mechanism.
|
|
137
|
+
Executing story slices inline in this session and/or committing the
|
|
138
|
+
delivery to local `main` is expressly forbidden, regardless of how the
|
|
139
|
+
environment looks. Each path surfaces verified remote evidence
|
|
140
|
+
(`remoteVerified`) at entry; on `false`, transition the ticket to
|
|
141
|
+
`agent::blocked` quoting `remoteProbe.detail` and halt.
|
|
134
142
|
- `/deliver` requires planned tickets: Epics at `agent::ready` (the
|
|
135
143
|
Epic helper's preflight enforces this, per segment) or well-formed
|
|
136
144
|
standalone Stories. Planning happens in [`/plan`](plan.md); the
|
|
@@ -181,6 +181,17 @@ Threshold defaults live in `delivery.preflight.*` in `.agentrc.json`
|
|
|
181
181
|
(all keys default to "no cap" — the gate is opt-in until an operator
|
|
182
182
|
configures `maxStories` etc.).
|
|
183
183
|
|
|
184
|
+
**Remote evidence — land or block (issue #4483).** The envelope also
|
|
185
|
+
carries `remoteVerified` + `remoteProbe` (deterministic probes:
|
|
186
|
+
`git remote get-url origin`, bounded `git ls-remote origin HEAD`). When
|
|
187
|
+
`remoteVerified` is `false`, flip the Epic to `agent::blocked`, post a
|
|
188
|
+
friction comment quoting `remoteProbe.detail`, and halt — the same
|
|
189
|
+
explicit-block shape as #4425/#4480. NEVER fall back to executing Stories
|
|
190
|
+
inline in this session or committing the delivery to local `main`; the
|
|
191
|
+
worktree/branch/PR path below is the only sanctioned mechanism. Phase 7's
|
|
192
|
+
finalize additionally refuses with a `delivery-branch-missing-on-origin`
|
|
193
|
+
blocker when `epic/<epicId>` never reached origin.
|
|
194
|
+
|
|
184
195
|
### Phase 1 main — Seed the wave plan
|
|
185
196
|
|
|
186
197
|
```bash
|
|
@@ -97,6 +97,14 @@ Capture `workCwd` from the result envelope. Add `--dry-run` to inspect
|
|
|
97
97
|
the planned actions without git or ticket mutations (dry-run also skips
|
|
98
98
|
the lease and the sweep).
|
|
99
99
|
|
|
100
|
+
**Remote evidence — land or block (issue #4483).** The envelope also
|
|
101
|
+
carries `remoteVerified` + `remoteProbe` (`git remote get-url origin` +
|
|
102
|
+
bounded `git ls-remote origin HEAD`). When `remoteVerified` is `false`,
|
|
103
|
+
transition the Story to `agent::blocked` quoting `remoteProbe.detail` and
|
|
104
|
+
stop. Implementing the Story inline outside the worktree/branch/PR path
|
|
105
|
+
and/or committing it to local `main` is expressly forbidden — the close
|
|
106
|
+
pipeline's push is the only sanctioned landing.
|
|
107
|
+
|
|
100
108
|
### Step 0.5 — `cd` into the workCwd
|
|
101
109
|
|
|
102
110
|
```bash
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.91.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.90.0...mandrel-v1.91.0) (2026-07-12)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
* **deliver:** verify the remote at entry and assert the delivery branch on origin (refs [#4483](https://github.com/dsj1984/mandrel/issues/4483)) ([#4484](https://github.com/dsj1984/mandrel/issues/4484)) ([a99ca2f](https://github.com/dsj1984/mandrel/commit/a99ca2fc4c74f7c397041dc4c703d845fae640c2))
|
|
11
|
+
|
|
5
12
|
## [1.90.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.89.0...mandrel-v1.90.0) (2026-07-12)
|
|
6
13
|
|
|
7
14
|
|
package/package.json
CHANGED