mandrel 2.16.0 → 2.18.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/docs/agentrc-reference.json +10 -0
- package/.agents/docs/configuration.md +9 -0
- package/.agents/docs/quality-gates.md +137 -0
- package/.agents/schemas/agentrc.schema.json +48 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +4 -0
- package/.agents/schemas/baselines/crap.schema.json +4 -0
- package/.agents/schemas/story-deliver-terminal.schema.json +6 -1
- package/.agents/scripts/acceptance-eval.js +52 -12
- package/.agents/scripts/audit-to-stories.js +92 -25
- package/.agents/scripts/boot-sweep.js +67 -8
- package/.agents/scripts/check-baseline-drift.js +138 -0
- package/.agents/scripts/coverage-capture.js +74 -25
- package/.agents/scripts/deliver-recover.js +45 -18
- package/.agents/scripts/drain-pending-cleanup.js +67 -23
- package/.agents/scripts/generate-lens-checklists.js +81 -30
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +88 -17
- package/.agents/scripts/lib/baselines/drift-detector.js +351 -0
- package/.agents/scripts/lib/baselines/envelope.js +7 -0
- package/.agents/scripts/lib/baselines/kernel.js +31 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +76 -0
- package/.agents/scripts/lib/baselines/reader.js +12 -1
- package/.agents/scripts/lib/baselines/refresh-service.js +7 -1
- package/.agents/scripts/lib/baselines/writer.js +10 -0
- package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +23 -8
- package/.agents/scripts/lib/cli-utils.js +48 -13
- package/.agents/scripts/lib/close-validation/projections/advisories.js +184 -0
- package/.agents/scripts/lib/close-validation/projections/crap.js +303 -0
- package/.agents/scripts/lib/close-validation/runner.js +68 -0
- package/.agents/scripts/lib/config/gates/crap.schema.js +7 -0
- package/.agents/scripts/lib/config/quality.js +40 -0
- package/.agents/scripts/lib/config/temp-paths.js +27 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +69 -0
- package/.agents/scripts/lib/coverage-utils.js +92 -9
- package/.agents/scripts/lib/crap-engine.js +113 -23
- package/.agents/scripts/lib/crap-utils.js +159 -93
- package/.agents/scripts/lib/dynamic-workflow/audit-orchestrator.js +97 -10
- package/.agents/scripts/lib/dynamic-workflow/degraded-coverage.js +81 -0
- package/.agents/scripts/lib/git-branch-lifecycle.js +15 -8
- package/.agents/scripts/lib/observability/terse-result.js +7 -3
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +35 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +13 -0
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes-ff.js +16 -1
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +19 -41
- package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +122 -0
- package/.agents/scripts/lib/orchestration/single-story-close/gate-log.js +9 -5
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +15 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +31 -1
- package/.agents/scripts/lib/orchestration/story-deliver-terminal-schema.js +166 -0
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +21 -50
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +26 -12
- package/.agents/scripts/lib/single-story-sweep.js +11 -0
- package/.agents/scripts/lib/stdio-flush.js +71 -0
- package/.agents/scripts/lib/temp-retention.js +559 -0
- package/.agents/scripts/lib/transpile.js +133 -6
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +47 -101
- package/.agents/scripts/lib/workers/crap-worker.js +49 -76
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +81 -8
- package/.agents/scripts/nav-registry-diff.js +30 -8
- package/.agents/scripts/plan-run-epilogue.js +27 -11
- package/.agents/scripts/resolve-doc-tiers.js +18 -8
- package/.agents/scripts/single-story-close.js +9 -92
- package/.agents/scripts/single-story-init.js +1 -1
- package/.agents/scripts/sync-branch-from-base.js +6 -1
- package/.agents/scripts/update-crap-baseline.js +13 -0
- package/README.md +14 -6
- package/docs/CHANGELOG.md +36 -0
- package/lib/cli/version-helpers.js +7 -0
- package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +15 -8
- package/package.json +5 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* story-deliver-terminal-schema.js — load `story-deliver-terminal.schema.json`
|
|
3
|
+
* and validate envelopes against it.
|
|
4
|
+
*
|
|
5
|
+
* Split out of `story-deliver-terminal.js` so the envelope WRITER holds only
|
|
6
|
+
* the contract's shape and vocabulary, and this module holds the one thing
|
|
7
|
+
* the writer must never depend on at call time: the filesystem.
|
|
8
|
+
*
|
|
9
|
+
* That separation is the fix, not just tidiness. `single-story-close.js`
|
|
10
|
+
* invoked by a *worktree-relative* path runs the Story worktree's own copy of
|
|
11
|
+
* the script and then **reaps that worktree** as one of its phases. The schema
|
|
12
|
+
* used to be read lazily, on the first envelope build — which happens after
|
|
13
|
+
* the reap — so the read hit a path that no longer existed. The throw landed
|
|
14
|
+
* inside the close CLI's error path, so a Story whose PR had merged, whose
|
|
15
|
+
* label was `agent::done`, and whose post-land tail was green exited non-zero
|
|
16
|
+
* emitting NO envelope at all: the delivery engine's documented return
|
|
17
|
+
* contract lost to a success, recoverable only by a second close run from the
|
|
18
|
+
* main checkout.
|
|
19
|
+
*
|
|
20
|
+
* Two guarantees close that, and both live here:
|
|
21
|
+
*
|
|
22
|
+
* 1. The schema is read and parsed ONCE, at module load. The parsed schema
|
|
23
|
+
* outlives the file, so what happens to the directory afterwards is
|
|
24
|
+
* irrelevant. Compilation stays lazy — it needs no filesystem — so an
|
|
25
|
+
* import costs one small read and nothing else.
|
|
26
|
+
* 2. An unreadable schema DEGRADES validation rather than throwing. A
|
|
27
|
+
* schema violation still fails loudly; see {@link validateTerminalEnvelope}.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
|
|
34
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
35
|
+
import addFormats from 'ajv-formats';
|
|
36
|
+
|
|
37
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Absolute path to the shipped schema — the SSOT this module reads.
|
|
41
|
+
*
|
|
42
|
+
* Module-private, like every other `SCHEMA_PATH` in the tree
|
|
43
|
+
* (`validation-evidence.js`, `signal-validator.js`): the path is an
|
|
44
|
+
* implementation detail of loading, and callers want the verdict, not the
|
|
45
|
+
* location.
|
|
46
|
+
*/
|
|
47
|
+
const SCHEMA_PATH = path.resolve(
|
|
48
|
+
__dirname,
|
|
49
|
+
'..',
|
|
50
|
+
'..',
|
|
51
|
+
'..',
|
|
52
|
+
'schemas',
|
|
53
|
+
'story-deliver-terminal.schema.json',
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Read and parse the shipped schema. Never throws: a read failure is recorded
|
|
58
|
+
* on the returned source and degrades validation downstream, because importing
|
|
59
|
+
* this module must never be what breaks a delivery.
|
|
60
|
+
*
|
|
61
|
+
* @returns {{ schema: object|null, error: string|null }}
|
|
62
|
+
*/
|
|
63
|
+
function loadSchemaSource() {
|
|
64
|
+
try {
|
|
65
|
+
return {
|
|
66
|
+
schema: JSON.parse(fs.readFileSync(SCHEMA_PATH, 'utf8')),
|
|
67
|
+
error: null,
|
|
68
|
+
};
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return { schema: null, error: err?.message ?? String(err) };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The schema, read and parsed at module load. Deliberately eager — see the
|
|
76
|
+
* module header for the failure that made it so.
|
|
77
|
+
*
|
|
78
|
+
* @type {{ schema: object|null, error: string|null }}
|
|
79
|
+
*/
|
|
80
|
+
const SCHEMA_SOURCE = loadSchemaSource();
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Compiled validators keyed by the source object they came from.
|
|
84
|
+
*
|
|
85
|
+
* A `WeakMap` rather than one module-level slot so an injected `schemaSource`
|
|
86
|
+
* (the test seam) can never poison the validator the production path memoizes.
|
|
87
|
+
*
|
|
88
|
+
* @type {WeakMap<object, Function>}
|
|
89
|
+
*/
|
|
90
|
+
const VALIDATORS = new WeakMap();
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Compile (once per source) and return the terminal-envelope validator, or
|
|
94
|
+
* `null` when the source carries no usable schema.
|
|
95
|
+
*
|
|
96
|
+
* @param {{ schema: object|null }} source
|
|
97
|
+
* @returns {Function|null}
|
|
98
|
+
*/
|
|
99
|
+
function getValidator(source) {
|
|
100
|
+
if (!source?.schema) return null;
|
|
101
|
+
const cached = VALIDATORS.get(source);
|
|
102
|
+
if (cached) return cached;
|
|
103
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
104
|
+
addFormats(ajv);
|
|
105
|
+
const validate = ajv.compile(source.schema);
|
|
106
|
+
VALIDATORS.set(source, validate);
|
|
107
|
+
return validate;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let _unvalidatedWarned = false;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Announce — once per process — that an envelope is going out unvalidated.
|
|
114
|
+
*
|
|
115
|
+
* Written straight to stderr rather than through `Logger.warn` for the same
|
|
116
|
+
* reason `emitTerminalEnvelope` bypasses `Logger.info`: the envelope itself is
|
|
117
|
+
* unsuppressible, so the notice that one was not checked has to be too. Under
|
|
118
|
+
* `AGENT_LOG_LEVEL=silent` a level-gated warning would vanish and the degrade
|
|
119
|
+
* would be invisible.
|
|
120
|
+
*
|
|
121
|
+
* @param {string|null|undefined} error
|
|
122
|
+
* @returns {void}
|
|
123
|
+
*/
|
|
124
|
+
function warnUnvalidated(error) {
|
|
125
|
+
if (_unvalidatedWarned) return;
|
|
126
|
+
_unvalidatedWarned = true;
|
|
127
|
+
process.stderr.write(
|
|
128
|
+
`[story-deliver-terminal] ⚠️ terminal-envelope schema unavailable (${error ?? 'unknown'}) — ` +
|
|
129
|
+
`emitting the envelope UNVALIDATED. The return contract is preserved; its shape is not checked. ` +
|
|
130
|
+
`Expected at: ${SCHEMA_PATH}\n`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Validate a candidate envelope against the shipped schema.
|
|
136
|
+
*
|
|
137
|
+
* When the schema is unavailable the result reports `validated: false` and
|
|
138
|
+
* `valid: true` — a **deliberate degrade**, not an oversight. Losing the shape
|
|
139
|
+
* check costs a guard against a malformed envelope; throwing here costs the
|
|
140
|
+
* envelope entirely, and the envelope is the documented return contract of the
|
|
141
|
+
* delivery engine. An unvalidated terminal a caller can act on beats no
|
|
142
|
+
* terminal at all, so the unreadable-schema case degrades and says so on
|
|
143
|
+
* stderr while a schema *violation* still fails loudly at the writer.
|
|
144
|
+
*
|
|
145
|
+
* @param {object} envelope
|
|
146
|
+
* @param {{ schemaSource?: { schema: object|null, error: string|null } }} [opts]
|
|
147
|
+
* `schemaSource` is a test seam — production always uses the eagerly loaded
|
|
148
|
+
* module-level source.
|
|
149
|
+
* @returns {{ valid: boolean, errors: string[], validated: boolean }}
|
|
150
|
+
*/
|
|
151
|
+
export function validateTerminalEnvelope(
|
|
152
|
+
envelope,
|
|
153
|
+
{ schemaSource = SCHEMA_SOURCE } = {},
|
|
154
|
+
) {
|
|
155
|
+
const validate = getValidator(schemaSource);
|
|
156
|
+
if (!validate) {
|
|
157
|
+
warnUnvalidated(schemaSource?.error);
|
|
158
|
+
return { valid: true, errors: [], validated: false };
|
|
159
|
+
}
|
|
160
|
+
const valid = validate(envelope);
|
|
161
|
+
if (valid) return { valid: true, errors: [], validated: true };
|
|
162
|
+
const errors = (validate.errors ?? []).map(
|
|
163
|
+
(e) => `${e.instancePath || '/'} ${e.message}`,
|
|
164
|
+
);
|
|
165
|
+
return { valid: false, errors, validated: true };
|
|
166
|
+
}
|
|
@@ -28,22 +28,12 @@
|
|
|
28
28
|
* pre-#4543 pipeline collapsed by treating budget exhaustion as a block.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import {
|
|
32
|
-
import path from 'node:path';
|
|
33
|
-
import { fileURLToPath } from 'node:url';
|
|
31
|
+
import { validateTerminalEnvelope } from './story-deliver-terminal-schema.js';
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const SCHEMA_PATH = path.resolve(
|
|
40
|
-
__dirname,
|
|
41
|
-
'..',
|
|
42
|
-
'..',
|
|
43
|
-
'..',
|
|
44
|
-
'schemas',
|
|
45
|
-
'story-deliver-terminal.schema.json',
|
|
46
|
-
);
|
|
33
|
+
// Re-exported so the schema split stays an implementation detail: every
|
|
34
|
+
// consumer still reaches the validator through the envelope module that owns
|
|
35
|
+
// the contract.
|
|
36
|
+
export { validateTerminalEnvelope };
|
|
47
37
|
|
|
48
38
|
export const TERMINAL_ENVELOPE_KIND = 'story-deliver-terminal';
|
|
49
39
|
|
|
@@ -152,39 +142,6 @@ export const NEXT_COMMANDS = Object.freeze({
|
|
|
152
142
|
escalateToPlan: (prompt) => `/plan "${quoteForPlan(prompt)}"`,
|
|
153
143
|
});
|
|
154
144
|
|
|
155
|
-
/** @type {Function|null} */
|
|
156
|
-
let _validator = null;
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Compile (once) and return the terminal-envelope validator.
|
|
160
|
-
*
|
|
161
|
-
* @returns {Function}
|
|
162
|
-
*/
|
|
163
|
-
function getValidator() {
|
|
164
|
-
if (_validator) return _validator;
|
|
165
|
-
const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
|
|
166
|
-
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
167
|
-
addFormats(ajv);
|
|
168
|
-
_validator = ajv.compile(schema);
|
|
169
|
-
return _validator;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Validate a candidate envelope against the shipped schema.
|
|
174
|
-
*
|
|
175
|
-
* @param {object} envelope
|
|
176
|
-
* @returns {{ valid: boolean, errors: string[] }}
|
|
177
|
-
*/
|
|
178
|
-
export function validateTerminalEnvelope(envelope) {
|
|
179
|
-
const validate = getValidator();
|
|
180
|
-
const valid = validate(envelope);
|
|
181
|
-
if (valid) return { valid: true, errors: [] };
|
|
182
|
-
const errors = (validate.errors ?? []).map(
|
|
183
|
-
(e) => `${e.instancePath || '/'} ${e.message}`,
|
|
184
|
-
);
|
|
185
|
-
return { valid: false, errors };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
145
|
/**
|
|
189
146
|
* Drop `undefined`-valued keys so the schema's `additionalProperties: false`
|
|
190
147
|
* and its nullable unions both stay satisfiable from one optional-argument
|
|
@@ -207,9 +164,14 @@ function compact(obj) {
|
|
|
207
164
|
* Throws a `TypeError` naming the schema violations when the assembled
|
|
208
165
|
* object does not validate. That is deliberate: the whole point of the
|
|
209
166
|
* envelope is that a caller can trust its status without re-probing
|
|
210
|
-
* GitHub, so emitting
|
|
167
|
+
* GitHub, so emitting a *malformed* one would reintroduce the ambiguity
|
|
211
168
|
* this replaces.
|
|
212
169
|
*
|
|
170
|
+
* A schema that cannot be READ is the opposite case and does not throw —
|
|
171
|
+
* see {@link validateTerminalEnvelope}. "This envelope is wrong" is worth
|
|
172
|
+
* failing on; "I could not check this envelope" is not worth destroying
|
|
173
|
+
* the return contract over.
|
|
174
|
+
*
|
|
213
175
|
* @param {object} args
|
|
214
176
|
* @param {number|null} args.storyId `null` only for an `escalated` terminal,
|
|
215
177
|
* which by construction never authored a Story.
|
|
@@ -227,6 +189,9 @@ function compact(obj) {
|
|
|
227
189
|
* @param {number} args.elapsedSeconds
|
|
228
190
|
* @param {object|null} [args.waitBudget]
|
|
229
191
|
* @param {string} [args.timestamp]
|
|
192
|
+
* @param {{ schema: object|null, error: string|null }} [args.schemaSource]
|
|
193
|
+
* Test seam; never passed in production. Not part of the envelope — the
|
|
194
|
+
* envelope is assembled from named fields only.
|
|
230
195
|
* @returns {object} The validated envelope.
|
|
231
196
|
*/
|
|
232
197
|
export function buildTerminalEnvelope({
|
|
@@ -245,6 +210,7 @@ export function buildTerminalEnvelope({
|
|
|
245
210
|
elapsedSeconds = 0,
|
|
246
211
|
waitBudget,
|
|
247
212
|
timestamp = new Date().toISOString(),
|
|
213
|
+
schemaSource,
|
|
248
214
|
}) {
|
|
249
215
|
const envelope = compact({
|
|
250
216
|
kind: TERMINAL_ENVELOPE_KIND,
|
|
@@ -268,7 +234,12 @@ export function buildTerminalEnvelope({
|
|
|
268
234
|
timestamp,
|
|
269
235
|
});
|
|
270
236
|
|
|
271
|
-
|
|
237
|
+
// Passing `{ schemaSource }` unconditionally is safe: an `undefined`
|
|
238
|
+
// property value is exactly what triggers the destructuring default on the
|
|
239
|
+
// other side, so production still gets the eagerly-loaded module source.
|
|
240
|
+
const { valid, errors } = validateTerminalEnvelope(envelope, {
|
|
241
|
+
schemaSource,
|
|
242
|
+
});
|
|
272
243
|
if (!valid) {
|
|
273
244
|
throw new TypeError(
|
|
274
245
|
`buildTerminalEnvelope: assembled envelope violates story-deliver-terminal.schema.json:\n` +
|
|
@@ -449,15 +449,29 @@ function computeMissingBddScaffoldFindings(stories, reach, severity) {
|
|
|
449
449
|
* Tasks satisfy (a) or (b). When ≥2 such Stories sit in the same wave (no
|
|
450
450
|
* transitive `depends_on` between them), emit a single finding keyed by the
|
|
451
451
|
* registry path.
|
|
452
|
+
*
|
|
453
|
+
* Reached from the module's `_internal` named export. The pass is pure — it
|
|
454
|
+
* performs no filesystem I/O and spawns no process — so its optional final
|
|
455
|
+
* `deps` parameter seams the three collaborating predicates rather than a
|
|
456
|
+
* built-in; each entry defaults to the real implementation
|
|
457
|
+
* (`.agents/rules/test-seams.md` rules 1-3: the defaults live on the function,
|
|
458
|
+
* never on a module-level mutable variable).
|
|
459
|
+
*
|
|
460
|
+
* @param {object} input
|
|
461
|
+
* @param {{
|
|
462
|
+
* isRegistryPathImpl?: typeof isRegistryPath,
|
|
463
|
+
* inSameWaveImpl?: typeof inSameWave,
|
|
464
|
+
* registryRegistryImpl?: typeof registryRegistry,
|
|
465
|
+
* }} [deps]
|
|
452
466
|
*/
|
|
453
|
-
function computeRegistryFindings(
|
|
454
|
-
stories,
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
467
|
+
function computeRegistryFindings(
|
|
468
|
+
{ stories, reach, patterns, producers, assumptionEntries, severity },
|
|
469
|
+
{
|
|
470
|
+
isRegistryPathImpl = isRegistryPath,
|
|
471
|
+
inSameWaveImpl = inSameWave,
|
|
472
|
+
registryRegistryImpl = registryRegistry,
|
|
473
|
+
} = {},
|
|
474
|
+
) {
|
|
461
475
|
const findings = [];
|
|
462
476
|
// Build the matching registry path set from producer & creator paths.
|
|
463
477
|
const registryHits = new Map(); // registryPath -> Map<storySlug, producers[]>
|
|
@@ -474,7 +488,7 @@ function computeRegistryFindings({
|
|
|
474
488
|
// (a) direct registry edits — object-form `{ path, assumption }` entries
|
|
475
489
|
// from `indexAssumptionEntries` (and the producer index built from them).
|
|
476
490
|
for (const [path, entries] of producers.entries()) {
|
|
477
|
-
if (!
|
|
491
|
+
if (!isRegistryPathImpl(path, patterns)) continue;
|
|
478
492
|
for (const e of entries) {
|
|
479
493
|
bump(path, {
|
|
480
494
|
storySlug: e.storySlug,
|
|
@@ -485,7 +499,7 @@ function computeRegistryFindings({
|
|
|
485
499
|
}
|
|
486
500
|
}
|
|
487
501
|
for (const e of assumptionEntries) {
|
|
488
|
-
if (!
|
|
502
|
+
if (!isRegistryPathImpl(e.path, patterns)) continue;
|
|
489
503
|
bump(e.path, {
|
|
490
504
|
storySlug: e.storySlug,
|
|
491
505
|
taskSlug: e.taskSlug,
|
|
@@ -510,7 +524,7 @@ function computeRegistryFindings({
|
|
|
510
524
|
continue;
|
|
511
525
|
const childParent = parentDirOf(change.path);
|
|
512
526
|
if (!childParent) continue;
|
|
513
|
-
for (const reg of
|
|
527
|
+
for (const reg of registryRegistryImpl(
|
|
514
528
|
producers,
|
|
515
529
|
assumptionEntries,
|
|
516
530
|
patterns,
|
|
@@ -532,7 +546,7 @@ function computeRegistryFindings({
|
|
|
532
546
|
const cluster = new Set();
|
|
533
547
|
for (let i = 0; i < stories.length; i += 1) {
|
|
534
548
|
for (let j = i + 1; j < stories.length; j += 1) {
|
|
535
|
-
if (
|
|
549
|
+
if (inSameWaveImpl(reach, stories[i], stories[j])) {
|
|
536
550
|
cluster.add(stories[i]);
|
|
537
551
|
cluster.add(stories[j]);
|
|
538
552
|
}
|
|
@@ -90,6 +90,7 @@ const STORY_BRANCH_INCLUDE = 'story-*';
|
|
|
90
90
|
* candidates: number,
|
|
91
91
|
* localDeleted: number,
|
|
92
92
|
* remoteDeleted: number,
|
|
93
|
+
* reaped: string[],
|
|
93
94
|
* protected: Array<{ branch: string, reason: string, worktreePath?: string|null }>,
|
|
94
95
|
* contentMerged: Array<{ branch: string, worktreePath: string|null }>,
|
|
95
96
|
* failures: Array<{ branch: string|null, scope: string, stderr?: string }>,
|
|
@@ -147,6 +148,7 @@ export async function sweepMergedBranches({
|
|
|
147
148
|
candidates: 0,
|
|
148
149
|
localDeleted: 0,
|
|
149
150
|
remoteDeleted: 0,
|
|
151
|
+
reaped: [],
|
|
150
152
|
protected: [],
|
|
151
153
|
contentMerged: [],
|
|
152
154
|
failures: [],
|
|
@@ -296,6 +298,7 @@ async function runSweepUnderLock({
|
|
|
296
298
|
candidates: 0,
|
|
297
299
|
localDeleted: 0,
|
|
298
300
|
remoteDeleted: 0,
|
|
301
|
+
reaped: [],
|
|
299
302
|
protected: [],
|
|
300
303
|
contentMerged,
|
|
301
304
|
failures: [],
|
|
@@ -320,6 +323,7 @@ async function runSweepUnderLock({
|
|
|
320
323
|
candidates: reapCandidates.length,
|
|
321
324
|
localDeleted: 0,
|
|
322
325
|
remoteDeleted: 0,
|
|
326
|
+
reaped: [],
|
|
323
327
|
protected: protectedList,
|
|
324
328
|
contentMerged,
|
|
325
329
|
failures: [],
|
|
@@ -365,6 +369,7 @@ function executeReap({
|
|
|
365
369
|
candidates: candidateCount,
|
|
366
370
|
localDeleted: 0,
|
|
367
371
|
remoteDeleted: 0,
|
|
372
|
+
reaped: [],
|
|
368
373
|
protected: protectedList,
|
|
369
374
|
contentMerged,
|
|
370
375
|
failures: [{ branch: null, scope: 'execute', stderr: msg }],
|
|
@@ -398,6 +403,11 @@ function executeReap({
|
|
|
398
403
|
candidates: candidateCount,
|
|
399
404
|
localDeleted,
|
|
400
405
|
remoteDeleted,
|
|
406
|
+
// Story #4794 — the branch names, not just the count. Each one is a merge
|
|
407
|
+
// this sweep CONFIRMED (merged PR + matching headRefOid), which is exactly
|
|
408
|
+
// the evidence the temp-retention catch-up needs to purge that Story's
|
|
409
|
+
// spent artifacts. Previously these existed only inside a log string.
|
|
410
|
+
reaped: reapable.map((c) => c.branch),
|
|
401
411
|
protected: protectedList,
|
|
402
412
|
contentMerged,
|
|
403
413
|
failures: result.failures,
|
|
@@ -503,6 +513,7 @@ function zeroResult({ error }) {
|
|
|
503
513
|
candidates: 0,
|
|
504
514
|
localDeleted: 0,
|
|
505
515
|
remoteDeleted: 0,
|
|
516
|
+
reaped: [],
|
|
506
517
|
protected: [],
|
|
507
518
|
contentMerged: [],
|
|
508
519
|
failures: [],
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// .agents/scripts/lib/stdio-flush.js
|
|
2
|
+
/**
|
|
3
|
+
* Stdio drain helper (Story #4783).
|
|
4
|
+
*
|
|
5
|
+
* `process.stdout` / `process.stderr` are only synchronous when they point at
|
|
6
|
+
* a TTY or a regular file. On a **pipe** — every `cmd | consumer`, every
|
|
7
|
+
* `child_process` capture, every `$(...)` substitution — Node writes
|
|
8
|
+
* asynchronously once the 64 KiB kernel pipe buffer fills: `write()` returns
|
|
9
|
+
* `false` and the remaining bytes sit in the stream's internal queue until the
|
|
10
|
+
* reader drains it.
|
|
11
|
+
*
|
|
12
|
+
* `process.exit()` does not wait for that queue. Anything still buffered when
|
|
13
|
+
* the process terminates is discarded, so a CLI that emits more than a pipe
|
|
14
|
+
* buffer's worth of output and then exits eagerly silently truncates — the
|
|
15
|
+
* exit code still reads green, and the consumer parses a half-written
|
|
16
|
+
* envelope. `runAsCli` is the shared boundary where that used to happen for
|
|
17
|
+
* every `.agents/scripts` entry point.
|
|
18
|
+
*
|
|
19
|
+
* The fix is to stop exiting eagerly (set `process.exitCode` and let the loop
|
|
20
|
+
* drain naturally). This helper is the belt-and-braces half: an explicit await
|
|
21
|
+
* on the queued bytes, so the flush is complete before the CLI's last frame
|
|
22
|
+
* unwinds even when something further out terminates the process.
|
|
23
|
+
*
|
|
24
|
+
* @module stdio-flush
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Await the point at which one writable stream's queued bytes have been handed
|
|
29
|
+
* to the OS. Resolves immediately when the stream has nothing queued, is not
|
|
30
|
+
* writable, or is not a stream at all — a flush must never be the reason a
|
|
31
|
+
* process hangs.
|
|
32
|
+
*
|
|
33
|
+
* Both settle paths are covered: the zero-length `write()` callback (which
|
|
34
|
+
* fires after every previously queued chunk, since writes are ordered) and the
|
|
35
|
+
* `'drain'` event (which fires when a backed-up stream empties). Whichever
|
|
36
|
+
* lands first resolves; the other is detached.
|
|
37
|
+
*
|
|
38
|
+
* @param {NodeJS.WritableStream & { writableLength?: number, writableEnded?: boolean, destroyed?: boolean }} [stream]
|
|
39
|
+
* @returns {Promise<void>}
|
|
40
|
+
*/
|
|
41
|
+
function drainStream(stream) {
|
|
42
|
+
if (!stream || typeof stream.write !== 'function') return Promise.resolve();
|
|
43
|
+
if (stream.destroyed || stream.writableEnded) return Promise.resolve();
|
|
44
|
+
if ((stream.writableLength ?? 0) === 0) return Promise.resolve();
|
|
45
|
+
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
let settled = false;
|
|
48
|
+
const done = () => {
|
|
49
|
+
if (settled) return;
|
|
50
|
+
settled = true;
|
|
51
|
+
stream.removeListener?.('drain', done);
|
|
52
|
+
resolve();
|
|
53
|
+
};
|
|
54
|
+
stream.once?.('drain', done);
|
|
55
|
+
// The write callback fires once this (empty) chunk — and therefore every
|
|
56
|
+
// chunk queued ahead of it — has been flushed to the underlying handle.
|
|
57
|
+
stream.write('', done);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Await the drain of the process's stdio streams. Never rejects: a stream that
|
|
63
|
+
* errored, closed, or was never writable resolves as already-flushed.
|
|
64
|
+
*
|
|
65
|
+
* @param {Array<NodeJS.WritableStream|undefined>} [streams] Defaults to
|
|
66
|
+
* `[process.stdout, process.stderr]`.
|
|
67
|
+
* @returns {Promise<void>}
|
|
68
|
+
*/
|
|
69
|
+
export async function flushStdio(streams = [process.stdout, process.stderr]) {
|
|
70
|
+
await Promise.all(streams.map((stream) => drainStream(stream)));
|
|
71
|
+
}
|