mandrel 1.87.0 → 1.89.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/README.md +18 -13
- package/.agents/audit-checklists/architecture.md +24 -0
- package/.agents/audit-checklists/clean-code.md +24 -0
- package/.agents/audit-checklists/dependencies.md +14 -0
- package/.agents/audit-checklists/devops.md +17 -0
- package/.agents/audit-checklists/documentation.md +22 -0
- package/.agents/audit-checklists/lighthouse.md +15 -0
- package/.agents/audit-checklists/navigability.md +14 -0
- package/.agents/audit-checklists/performance.md +22 -0
- package/.agents/audit-checklists/privacy.md +21 -0
- package/.agents/audit-checklists/quality.md +18 -0
- package/.agents/audit-checklists/security.md +22 -0
- package/.agents/audit-checklists/seo.md +16 -0
- package/.agents/audit-checklists/sre.md +24 -0
- package/.agents/audit-checklists/ux-ui.md +21 -0
- package/.agents/docs/SDLC.md +63 -16
- package/.agents/docs/configuration.md +5 -3
- package/.agents/instructions.md +51 -21
- package/.agents/personas/architect.md +10 -7
- package/.agents/personas/engineer.md +4 -3
- package/.agents/personas/project-manager.md +5 -2
- package/.agents/personas/refactorer.md +5 -3
- package/.agents/rules/git-conventions.md +77 -0
- package/.agents/schemas/agentrc.schema.json +16 -4
- package/.agents/schemas/audit-rules.json +16 -2
- package/.agents/schemas/audit-rules.schema.json +7 -6
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +38 -0
- package/.agents/schemas/signal-event.schema.json +28 -13
- package/.agents/scripts/acceptance-spec-reconciler.js +6 -4
- package/.agents/scripts/check-context-budget.js +320 -0
- package/.agents/scripts/diagnose-friction.js +4 -4
- package/.agents/scripts/epic-audit-prepare.js +30 -2
- package/.agents/scripts/epic-audit-recheck.js +46 -13
- package/.agents/scripts/epic-deliver-prepare.js +80 -8
- package/.agents/scripts/epic-plan-spec.js +4 -8
- package/.agents/scripts/generate-lens-checklists.js +180 -0
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +300 -0
- package/.agents/scripts/lib/audit-suite/findings.js +27 -0
- package/.agents/scripts/lib/audit-suite/index.js +9 -0
- package/.agents/scripts/lib/audit-suite/lens-checklist.js +212 -0
- package/.agents/scripts/lib/audit-suite/selector.js +136 -5
- package/.agents/scripts/lib/checks/loop-health.js +340 -0
- package/.agents/scripts/lib/cli-args.js +8 -0
- package/.agents/scripts/lib/config/explain.js +4 -0
- package/.agents/scripts/lib/config/runners.js +21 -2
- package/.agents/scripts/lib/config/temp-paths.js +24 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +23 -3
- package/.agents/scripts/lib/config-settings-schema-quality.js +7 -0
- package/.agents/scripts/lib/doc-tiers.js +291 -0
- package/.agents/scripts/lib/epic-body-sections.js +5 -2
- package/.agents/scripts/lib/epic-merge-lock.js +83 -0
- package/.agents/scripts/lib/epic-plan-clarity.js +3 -1
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +66 -20
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +395 -86
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +299 -72
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +438 -0
- package/.agents/scripts/lib/gates/friction.js +15 -5
- package/.agents/scripts/lib/observability/perf-aggregator.js +30 -104
- package/.agents/scripts/lib/observability/perf-report-readers.js +1 -1
- package/.agents/scripts/lib/observability/signal-validator.js +204 -0
- package/.agents/scripts/lib/observability/signals-writer.js +157 -54
- package/.agents/scripts/lib/observability/tool-trace-hook.js +42 -4
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
- package/.agents/scripts/lib/orchestration/code-review.js +74 -4
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +213 -0
- package/.agents/scripts/lib/orchestration/doc-reader.js +4 -96
- package/.agents/scripts/lib/orchestration/docs-digest.js +34 -0
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/authoring-context.js +56 -19
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +22 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +188 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +6 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +69 -8
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +11 -2
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +47 -61
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +39 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/label-transitioner.js +144 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +258 -14
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +6 -0
- package/.agents/scripts/lib/orchestration/merge-block-class.js +218 -0
- package/.agents/scripts/lib/orchestration/plan-review-routing.js +1 -1
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +3 -3
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +63 -34
- package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +167 -52
- package/.agents/scripts/lib/orchestration/retro/phases/post-and-mirror.js +49 -2
- package/.agents/scripts/lib/orchestration/retro-proposals.js +12 -55
- package/.agents/scripts/lib/orchestration/retro-runner.js +9 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +8 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +419 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +35 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +353 -69
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +66 -4
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +60 -9
- package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +7 -5
- package/.agents/scripts/lib/orchestration/story-close/merge-runner.js +24 -2
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +167 -8
- package/.agents/scripts/lib/orchestration/story-close/shared-checkout-guard.js +163 -0
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +20 -9
- package/.agents/scripts/lib/planning-corpus.js +306 -0
- package/.agents/scripts/lib/signals/detectors/common.js +10 -10
- package/.agents/scripts/lib/signals/detectors/index.js +4 -4
- package/.agents/scripts/lib/signals/detectors/retry.js +19 -18
- package/.agents/scripts/lib/signals/detectors/rework.js +1 -1
- package/.agents/scripts/lib/signals/schema.js +56 -81
- package/.agents/scripts/lib/signals/span-tree.js +6 -5
- package/.agents/scripts/lib/story-plan.js +3 -0
- package/.agents/scripts/lib/wave-runner/tick.js +10 -2
- package/.agents/scripts/lifecycle-emit.js +39 -8
- package/.agents/scripts/providers/github/issues.js +12 -1
- package/.agents/scripts/resolve-doc-tiers.js +83 -0
- package/.agents/scripts/retro-run.js +51 -0
- package/.agents/scripts/signals-view.js +1 -1
- package/.agents/scripts/single-story-close.js +20 -1
- package/.agents/scripts/standalone-feedback-rollup.js +188 -0
- package/.agents/scripts/story-close.js +48 -0
- package/.agents/scripts/story-plan.js +51 -12
- package/.agents/scripts/validate-docs-freshness.js +69 -15
- package/.agents/skills/core/documentation-and-adrs/SKILL.md +58 -0
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +5 -3
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +20 -7
- package/.agents/skills/core/scope-triage/SKILL.md +61 -0
- package/.agents/skills/skills.index.json +3 -3
- package/.agents/workflows/audit-documentation.md +82 -2
- package/.agents/workflows/helpers/code-review.md +193 -44
- package/.agents/workflows/helpers/deliver-epic.md +128 -39
- package/.agents/workflows/helpers/deliver-stories.md +26 -0
- package/.agents/workflows/helpers/epic-audit.md +116 -283
- package/.agents/workflows/helpers/epic-deliver-story.md +14 -0
- package/.agents/workflows/helpers/epic-plan-decompose.md +18 -200
- package/.agents/workflows/helpers/epic-plan-spec.md +18 -180
- package/.agents/workflows/helpers/plan-epic.md +141 -105
- package/.agents/workflows/helpers/plan-story.md +32 -0
- package/.agents/workflows/helpers/single-story-deliver.md +43 -0
- package/.agents/workflows/loops/nightly-audit.md +9 -7
- package/docs/CHANGELOG.md +29 -0
- package/lib/cli/doctor.js +44 -0
- package/package.json +4 -3
- package/.agents/scripts/epic-plan-spec-validate.js +0 -111
- package/.agents/scripts/lib/feedback-loop/code-review-graduator.js +0 -207
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +0 -58
- package/.agents/scripts/lib/signals/detectors/hotspot.js +0 -292
|
@@ -51,20 +51,21 @@
|
|
|
51
51
|
*/
|
|
52
52
|
|
|
53
53
|
function tsOf(evt) {
|
|
54
|
-
//
|
|
55
|
-
|
|
54
|
+
// Canonical envelope only (Epic #4406) — the legacy `timestamp` alias
|
|
55
|
+
// was deleted from every writer in the same PR.
|
|
56
|
+
return evt?.ts ?? null;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
function epicOf(evt) {
|
|
59
|
-
return evt?.
|
|
60
|
+
return evt?.epicId ?? null;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
function storyOf(evt) {
|
|
63
|
-
return evt?.
|
|
64
|
+
return evt?.storyId ?? null;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
function taskOf(evt) {
|
|
67
|
-
return evt?.
|
|
68
|
+
return evt?.taskId ?? null;
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
/**
|
|
@@ -193,6 +193,7 @@ export async function loadBodyTemplate(projectRoot) {
|
|
|
193
193
|
* bodyTemplate: string,
|
|
194
194
|
* duplicateCandidates: Array<object>,
|
|
195
195
|
* techStack?: string|null,
|
|
196
|
+
* corpusContext?: { docsDigest: string|null, relevantSections: Array<object> }|null,
|
|
196
197
|
* maxResults?: number,
|
|
197
198
|
* }} opts
|
|
198
199
|
*/
|
|
@@ -203,6 +204,7 @@ export function buildContextEnvelope({
|
|
|
203
204
|
bodyTemplate,
|
|
204
205
|
duplicateCandidates,
|
|
205
206
|
techStack = null,
|
|
207
|
+
corpusContext = null,
|
|
206
208
|
maxResults = DEFAULT_DUPLICATE_MAX_RESULTS,
|
|
207
209
|
}) {
|
|
208
210
|
return {
|
|
@@ -218,6 +220,7 @@ export function buildContextEnvelope({
|
|
|
218
220
|
candidates: duplicateCandidates,
|
|
219
221
|
},
|
|
220
222
|
techStack,
|
|
223
|
+
corpusContext,
|
|
221
224
|
deliverContract: {
|
|
222
225
|
workflow: '.agents/workflows/helpers/single-story-deliver.md',
|
|
223
226
|
requiredLabels: ['type::story', `persona::${persona}`],
|
|
@@ -425,7 +425,13 @@ export function planTick(state, records, inFlight) {
|
|
|
425
425
|
inFlight.length === 0
|
|
426
426
|
) {
|
|
427
427
|
signals.push({
|
|
428
|
+
// `index` is what the perf-aggregator's `bucketWaves` keys on to
|
|
429
|
+
// bracket a wave's wall-clock (Epic #4406 / Story #4413). Without
|
|
430
|
+
// it every wave-start collapsed to NaN and the waveParallelism
|
|
431
|
+
// table rendered structurally empty. The continuous ready-set
|
|
432
|
+
// scheduler runs a single logical wave, so `index: 0`.
|
|
428
433
|
kind: 'wave-start',
|
|
434
|
+
index: 0,
|
|
429
435
|
stories: records.map((s) => ({ id: s.id, title: s.title })),
|
|
430
436
|
});
|
|
431
437
|
}
|
|
@@ -443,7 +449,7 @@ export function planTick(state, records, inFlight) {
|
|
|
443
449
|
nextAction = { kind: 'observe', waitingOn };
|
|
444
450
|
} else if (allDone) {
|
|
445
451
|
// Every Story is done and nothing is in flight: the run is complete.
|
|
446
|
-
signals.push({ kind: 'wave-complete' });
|
|
452
|
+
signals.push({ kind: 'wave-complete', index: 0 });
|
|
447
453
|
nextAction = { kind: 'epic-complete' };
|
|
448
454
|
} else {
|
|
449
455
|
// Ready set empty, nothing in flight, but not all Stories are done — a
|
|
@@ -739,7 +745,9 @@ function defaultSignalEmit(epicId, ctx) {
|
|
|
739
745
|
return async (signal) => {
|
|
740
746
|
await appendEpicSignal({
|
|
741
747
|
epicId,
|
|
742
|
-
|
|
748
|
+
// Canonical envelope (Epic #4406 / Story #4413): the single
|
|
749
|
+
// `epicId` key — the legacy `epic` alias is gone.
|
|
750
|
+
signal: { ts: new Date().toISOString(), epicId, ...signal },
|
|
743
751
|
config: ctx?.config,
|
|
744
752
|
});
|
|
745
753
|
};
|
|
@@ -130,14 +130,27 @@ export function parseArgv(argv) {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
133
|
+
* Argv keys that are runtime OPTIONS consumed by `main()` /
|
|
134
|
+
* `runLifecycleEmit()` directly rather than forwarded into the emitted
|
|
135
|
+
* bus payload. `--headless` (Story #4427) is the first member: every
|
|
136
|
+
* lifecycle event schema declares `additionalProperties: false`, so a
|
|
137
|
+
* flag that selects delivery-run behavior (rather than describing the
|
|
138
|
+
* event itself) would fail schema validation if it leaked into the
|
|
139
|
+
* payload.
|
|
140
|
+
*/
|
|
141
|
+
const RUNTIME_OPTION_KEYS = Object.freeze(['event', 'headless']);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Build the bus emit payload from parsed argv. `event` and `headless`
|
|
145
|
+
* (see `RUNTIME_OPTION_KEYS`) are consumed as runtime options, not
|
|
146
|
+
* payload fields. `--epic` is mapped to `epicId` (integer). All other
|
|
147
|
+
* flags are mapped from kebab-case to camelCase with light value
|
|
148
|
+
* coercion.
|
|
136
149
|
*/
|
|
137
150
|
export function buildPayload(parsed) {
|
|
138
151
|
const payload = {};
|
|
139
152
|
for (const [key, raw] of Object.entries(parsed)) {
|
|
140
|
-
if (key
|
|
153
|
+
if (RUNTIME_OPTION_KEYS.includes(key)) continue;
|
|
141
154
|
if (key === 'epic') {
|
|
142
155
|
const n = Number.parseInt(raw, 10);
|
|
143
156
|
if (!Number.isInteger(n) || n < 1) {
|
|
@@ -261,11 +274,16 @@ export async function emitBlockedSignal({
|
|
|
261
274
|
config,
|
|
262
275
|
signal: {
|
|
263
276
|
kind: 'friction',
|
|
277
|
+
ts: new Date().toISOString(),
|
|
278
|
+
epicId,
|
|
279
|
+
category: 'lifecycle-listener-failure',
|
|
280
|
+
emitter: { tool: 'lifecycle-emit.js' },
|
|
264
281
|
severity: 'high',
|
|
265
282
|
event,
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
283
|
+
details: {
|
|
284
|
+
message: `lifecycle-emit: ${event} produced failed listener classification(s): ${reasons}`,
|
|
285
|
+
outcomes: failedOutcomes,
|
|
286
|
+
},
|
|
269
287
|
},
|
|
270
288
|
});
|
|
271
289
|
} catch (err) {
|
|
@@ -321,6 +339,11 @@ export async function emitBlockedSignal({
|
|
|
321
339
|
* alongside an injected `bus` (when the caller owns listener wiring) —
|
|
322
340
|
* lets a test inject a bus + a chain carrying a `failed`-classifying
|
|
323
341
|
* listener to exercise the non-zero-exit path without the real roster.
|
|
342
|
+
* @param {boolean} [opts.headless] Explicit must-land signal (Story
|
|
343
|
+
* #4427), forwarded to `buildDefaultListenerChain({ headless })` (and
|
|
344
|
+
* from there to `MergeWatcher`) when this call constructs the default
|
|
345
|
+
* chain. Defaults to `false` — attended-mode behavior is unchanged.
|
|
346
|
+
* Ignored when the caller injects its own `bus`/`chain`.
|
|
324
347
|
*
|
|
325
348
|
* @returns {Promise<{ event: string, payload: object, seqId: number,
|
|
326
349
|
* outcomes: Array<object>, failed: boolean }>} The `outcomes[]` array
|
|
@@ -340,6 +363,7 @@ export async function runLifecycleEmit({
|
|
|
340
363
|
config,
|
|
341
364
|
emitBlockedSignalFn = emitBlockedSignal,
|
|
342
365
|
chain: injectedChain,
|
|
366
|
+
headless = false,
|
|
343
367
|
} = {}) {
|
|
344
368
|
if (typeof event !== 'string' || event.length === 0) {
|
|
345
369
|
throw new Error('lifecycle-emit: --event is required');
|
|
@@ -404,6 +428,7 @@ export async function runLifecycleEmit({
|
|
|
404
428
|
checkpointer: resolvedCheckpointer,
|
|
405
429
|
config: resolvedConfig,
|
|
406
430
|
logger,
|
|
431
|
+
headless,
|
|
407
432
|
});
|
|
408
433
|
}
|
|
409
434
|
const { seqId } = await targetBus.emit(event, payload ?? {});
|
|
@@ -464,7 +489,13 @@ async function main() {
|
|
|
464
489
|
}
|
|
465
490
|
const event = parsed.event;
|
|
466
491
|
const payload = buildPayload(parsed);
|
|
467
|
-
|
|
492
|
+
// `--headless true` is a runtime option (Story #4427), not a payload
|
|
493
|
+
// field — see RUNTIME_OPTION_KEYS. Threaded explicitly through
|
|
494
|
+
// runLifecycleEmit → buildDefaultListenerChain → MergeWatcher so the
|
|
495
|
+
// epic-path must-land terminal step reads a caller-supplied input
|
|
496
|
+
// rather than an ambient global.
|
|
497
|
+
const headless = parsed.headless === 'true';
|
|
498
|
+
const out = await runLifecycleEmit({ event, payload, headless });
|
|
468
499
|
process.stdout.write(`${JSON.stringify(out)}\n`);
|
|
469
500
|
// Exit non-zero when any listener classification came back `failed` so
|
|
470
501
|
// the workflow's "re-run on non-zero" loop closes the partial-finalize
|
|
@@ -150,6 +150,14 @@ export class IssuesGateway {
|
|
|
150
150
|
/**
|
|
151
151
|
* List Epic-typed issues. Filter shape preserved from the old code.
|
|
152
152
|
*
|
|
153
|
+
* `filters.pageCap` / `filters.perPage` pass through to `paginateRest`
|
|
154
|
+
* so a caller that only needs a bounded shortlist (e.g. the
|
|
155
|
+
* standalone-Story corpus lookup in `planning-corpus.js`, which ranks
|
|
156
|
+
* candidates down to a top-5 shortlist) can cap the scan instead of
|
|
157
|
+
* inheriting the default 50-page / 5000-item ceiling. Omitted, both
|
|
158
|
+
* fall back to `paginateRest`'s own defaults — unchanged behavior for
|
|
159
|
+
* existing callers (e.g. `duplicate-search.js`).
|
|
160
|
+
*
|
|
153
161
|
* @field-manifest /repos/{owner}/{repo}/issues?labels=type::epic: number,
|
|
154
162
|
* title, labels, state, state_reason, pull_request
|
|
155
163
|
*/
|
|
@@ -160,7 +168,10 @@ export class IssuesGateway {
|
|
|
160
168
|
labels: TYPE_LABELS.EPIC,
|
|
161
169
|
});
|
|
162
170
|
const endpoint = `/repos/${this.owner}/${this.repo}/issues?${params}`;
|
|
163
|
-
const issues = await paginateRest(this._gh, endpoint
|
|
171
|
+
const issues = await paginateRest(this._gh, endpoint, {
|
|
172
|
+
pageCap: filters.pageCap,
|
|
173
|
+
perPage: filters.perPage,
|
|
174
|
+
});
|
|
164
175
|
return issues
|
|
165
176
|
.filter((issue) => !issue.pull_request)
|
|
166
177
|
.map(issueToEpicListItem);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI: resolve the repository's documentation read-tiers (Story #4438).
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper over `lib/doc-tiers.js#resolveDocTiers` that prints the tier
|
|
5
|
+
* map — `{ tiers: { alwaysLoaded, mandatoryRead, digestVisible, onDemand } }`,
|
|
6
|
+
* every entry `{ path, bytes }` — as JSON. Consumed by the `audit-documentation`
|
|
7
|
+
* lens (read-tier severity weighting), the `check-context-budget.js` ratchet,
|
|
8
|
+
* and operators inspecting the always-loaded closure.
|
|
9
|
+
*
|
|
10
|
+
* Flags:
|
|
11
|
+
* --json emit the tier map as JSON to stdout (default rendering is also
|
|
12
|
+
* JSON; the flag is accepted for parity with the sibling ratchets
|
|
13
|
+
* and future non-JSON renderings).
|
|
14
|
+
* --root <path> resolve tiers against an explicit repo root (default: the
|
|
15
|
+
* resolved PROJECT_ROOT).
|
|
16
|
+
*
|
|
17
|
+
* Exit code is always 0 on success — this is a reporter, not a gate.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import process from 'node:process';
|
|
21
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
22
|
+
import { PROJECT_ROOT, resolveConfig } from './lib/config-resolver.js';
|
|
23
|
+
import { resolveDocTiers } from './lib/doc-tiers.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse argv for `--root <path>` and `--json`.
|
|
27
|
+
*
|
|
28
|
+
* @param {string[]} argv
|
|
29
|
+
* @returns {{ rootPath: string | null, json: boolean }}
|
|
30
|
+
*/
|
|
31
|
+
export function parseArgv(argv = []) {
|
|
32
|
+
let rootPath = null;
|
|
33
|
+
let json = false;
|
|
34
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
35
|
+
const a = argv[i];
|
|
36
|
+
if (a === '--root') {
|
|
37
|
+
const next = argv[i + 1];
|
|
38
|
+
if (next && !next.startsWith('--')) {
|
|
39
|
+
rootPath = next;
|
|
40
|
+
i += 1;
|
|
41
|
+
}
|
|
42
|
+
} else if (a === '--json') {
|
|
43
|
+
json = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { rootPath, json };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Top-level CLI entry. Exported so tests can drive it against a fixture root
|
|
51
|
+
* with an injected sink and config.
|
|
52
|
+
*
|
|
53
|
+
* @param {{
|
|
54
|
+
* argv?: string[],
|
|
55
|
+
* config?: object,
|
|
56
|
+
* root?: string,
|
|
57
|
+
* stdout?: { write: (s: string) => void },
|
|
58
|
+
* }} [opts]
|
|
59
|
+
* @returns {Promise<number>} always 0
|
|
60
|
+
*/
|
|
61
|
+
export async function runCli({
|
|
62
|
+
argv = process.argv.slice(2),
|
|
63
|
+
config,
|
|
64
|
+
root,
|
|
65
|
+
stdout = process.stdout,
|
|
66
|
+
} = {}) {
|
|
67
|
+
const { rootPath } = parseArgv(argv);
|
|
68
|
+
const resolvedConfig = config ?? resolveConfig();
|
|
69
|
+
const resolvedRoot = root ?? rootPath ?? PROJECT_ROOT;
|
|
70
|
+
const result = resolveDocTiers(resolvedConfig, { root: resolvedRoot });
|
|
71
|
+
stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function main() {
|
|
76
|
+
return runCli();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
runAsCli(import.meta.url, main, {
|
|
80
|
+
source: 'resolve-doc-tiers',
|
|
81
|
+
propagateExitCode: true,
|
|
82
|
+
errorPrefix: '[resolve-doc-tiers] ❌ Fatal error',
|
|
83
|
+
});
|
|
@@ -37,6 +37,46 @@ import { createLedgerWriter } from './lib/orchestration/lifecycle/ledger-writer.
|
|
|
37
37
|
import { runRetro } from './lib/orchestration/retro-runner.js';
|
|
38
38
|
import { createProvider } from './lib/provider-factory.js';
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Pure: resolve the framework/consumer repo slugs (`"<owner>/<repo>"`)
|
|
42
|
+
* from a resolved config's `github` block, for threading into the retro's
|
|
43
|
+
* routed-proposal composer.
|
|
44
|
+
*
|
|
45
|
+
* - `consumerRepo` is the project's OWN repo (`github.owner/github.repo`).
|
|
46
|
+
* It has **no** silent fallback: when `github.owner` / `github.repo` are
|
|
47
|
+
* absent the slug is `''`, which `gatherRetroSignals` treats as "disable
|
|
48
|
+
* the consumer pane loudly" rather than routing consumer-tagged friction
|
|
49
|
+
* at the framework mirror (Story #4417).
|
|
50
|
+
* - `frameworkRepo` is the distinct framework mirror
|
|
51
|
+
* (`github.frameworkRepo.{owner,repo}`) when configured, otherwise `''`
|
|
52
|
+
* — `gatherRetroSignals` then falls back to its `DEFAULT_FRAMEWORK_REPO`
|
|
53
|
+
* constant (a stable, known default is legitimate for the framework).
|
|
54
|
+
*
|
|
55
|
+
* Exported for tests.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} [config]
|
|
58
|
+
* @returns {{ frameworkRepo: string, consumerRepo: string }}
|
|
59
|
+
*/
|
|
60
|
+
export function resolveRetroRepos(config) {
|
|
61
|
+
const gh = config?.github ?? {};
|
|
62
|
+
const consumerRepo =
|
|
63
|
+
typeof gh.owner === 'string' &&
|
|
64
|
+
gh.owner.length > 0 &&
|
|
65
|
+
typeof gh.repo === 'string' &&
|
|
66
|
+
gh.repo.length > 0
|
|
67
|
+
? `${gh.owner}/${gh.repo}`
|
|
68
|
+
: '';
|
|
69
|
+
const fw = gh.frameworkRepo ?? {};
|
|
70
|
+
const frameworkRepo =
|
|
71
|
+
typeof fw.owner === 'string' &&
|
|
72
|
+
fw.owner.length > 0 &&
|
|
73
|
+
typeof fw.repo === 'string' &&
|
|
74
|
+
fw.repo.length > 0
|
|
75
|
+
? `${fw.owner}/${fw.repo}`
|
|
76
|
+
: '';
|
|
77
|
+
return { frameworkRepo, consumerRepo };
|
|
78
|
+
}
|
|
79
|
+
|
|
40
80
|
const HELP = `Usage: node .agents/scripts/retro-run.js --epic <epicId> [--full-retro]
|
|
41
81
|
|
|
42
82
|
Composes and posts the Epic retro structured comment for Epic #<epicId>,
|
|
@@ -125,11 +165,22 @@ export async function runRetroCli({
|
|
|
125
165
|
: createLedgerWriter({ epicId, tempRoot });
|
|
126
166
|
ledger.register(bus);
|
|
127
167
|
|
|
168
|
+
// Story #4417 — thread the framework/consumer repo slugs resolved from
|
|
169
|
+
// `config.github` into the retro so the routed-proposal composer files
|
|
170
|
+
// consumer-tagged friction at the project's own repo (not the framework
|
|
171
|
+
// mirror), and disables the consumer pane loudly when unresolved.
|
|
172
|
+
const { frameworkRepo, consumerRepo } = resolveRetroRepos(config);
|
|
173
|
+
|
|
128
174
|
const result = await runRetroFn({
|
|
129
175
|
epicId,
|
|
130
176
|
provider,
|
|
131
177
|
bus,
|
|
132
178
|
forceFull: fullRetro,
|
|
179
|
+
frameworkRepo,
|
|
180
|
+
consumerRepo,
|
|
181
|
+
// Story #4418 — thread the resolved config so the retro auto-filer can
|
|
182
|
+
// read the `delivery.feedbackLoop.retroProposals` toggle.
|
|
183
|
+
config,
|
|
133
184
|
logger,
|
|
134
185
|
});
|
|
135
186
|
|
|
@@ -161,7 +161,7 @@ function formatDuration(ms) {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
function describeEvent(evt) {
|
|
164
|
-
const ts = evt?.ts ??
|
|
164
|
+
const ts = evt?.ts ?? '(no ts)';
|
|
165
165
|
const kind = evt?.kind ?? '(no kind)';
|
|
166
166
|
const phase = evt?.phase ? ` phase=${evt.phase}` : '';
|
|
167
167
|
const category = evt?.category ? ` category=${evt.category}` : '';
|
|
@@ -23,6 +23,14 @@
|
|
|
23
23
|
* the post-merge confirmation step,
|
|
24
24
|
* `single-story-confirm-merge.js`)
|
|
25
25
|
* 8. worktree-reap — drop the per-Story worktree
|
|
26
|
+
* 9. confirm-merge — Story #4428, headless-only (`--wait-merge`):
|
|
27
|
+
* poll the just-armed PR to merge confirmation
|
|
28
|
+
* (reusing `confirmStoryMerged`) or terminate
|
|
29
|
+
* `agent::blocked` with a classified
|
|
30
|
+
* `merge.unlanded` lifecycle event. Attended runs
|
|
31
|
+
* (the default, no `--wait-merge`) skip this
|
|
32
|
+
* phase entirely and keep resting at
|
|
33
|
+
* `agent::closing`, exactly as before.
|
|
26
34
|
*
|
|
27
35
|
* Existing tests import the re-exported helpers
|
|
28
36
|
* (`runSingleStoryClose`, `ensurePullRequest`, `parsePrNumber`,
|
|
@@ -33,8 +41,19 @@
|
|
|
33
41
|
* node single-story-close.js --story <STORY_ID> [--cwd <main-repo>]
|
|
34
42
|
* [--skip-validation] [--skip-sync]
|
|
35
43
|
* [--no-auto-merge] [--no-full-scope-crap]
|
|
44
|
+
* [--wait-merge | --no-wait-merge]
|
|
36
45
|
*
|
|
37
|
-
*
|
|
46
|
+
* `--wait-merge` is the headless must-land signal (Story #4428, Epic
|
|
47
|
+
* #4425): the invoking surface (a headless `/single-story-deliver` run, a
|
|
48
|
+
* CI-driven wrapper, or `/deliver`'s standalone multi-Story fan-out) opts
|
|
49
|
+
* in explicitly — attended runs never pass it, so the default exit shape
|
|
50
|
+
* (rest at `agent::closing`, issue OPEN) is unchanged. `--no-wait-merge` is
|
|
51
|
+
* the explicit opt-out that always wins over `--wait-merge`, for a caller
|
|
52
|
+
* that wants to manage merge confirmation externally even in an otherwise
|
|
53
|
+
* headless context.
|
|
54
|
+
*
|
|
55
|
+
* Exit codes: 0 ok, 1 error (including a headless `--wait-merge` run that
|
|
56
|
+
* gave up without a confirmed merge — see phase 9 above).
|
|
38
57
|
*
|
|
39
58
|
* @see .agents/workflows/helpers/single-story-deliver.md
|
|
40
59
|
*/
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* standalone-feedback-rollup.js — end-of-run feedback rollup for the
|
|
5
|
+
* `/deliver` standalone multi-Story path (Epic #4406 / Story #4416).
|
|
6
|
+
*
|
|
7
|
+
* The standalone delivery path (`helpers/deliver-stories`) drives one or
|
|
8
|
+
* more Epic-free Stories to green, and each per-Story sub-agent appends
|
|
9
|
+
* `friction` records to its **standalone** signals stream at
|
|
10
|
+
* `temp/standalone/stories/story-<sid>/signals.ndjson` (written via
|
|
11
|
+
* `appendSignal({ epicId: null, storyId })`). Before this rollup that
|
|
12
|
+
* tree was write-only — nothing ever read it back. This CLI closes the
|
|
13
|
+
* gap: it scans the standalone streams for the delivered Story set and
|
|
14
|
+
* emits a per-category friction summary that `deliver-stories.md`
|
|
15
|
+
* surfaces in its Phase 3 run summary.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* node standalone-feedback-rollup.js --stories <id,...>
|
|
19
|
+
*
|
|
20
|
+
* Contract:
|
|
21
|
+
* - Stream paths resolve **exclusively** through the `temp-paths`
|
|
22
|
+
* helpers via `forEachLine(null, storyId, cb, config)` — the `null`
|
|
23
|
+
* Epic sentinel routes to the standalone tree. No hand-built path
|
|
24
|
+
* strings.
|
|
25
|
+
* - Aggregates `friction` records by their **top-level** `category`
|
|
26
|
+
* (Epic #4406 canonical envelope shape); a record with no category
|
|
27
|
+
* buckets under `Unknown`.
|
|
28
|
+
* - **Never fails a run.** A missing or empty stream degrades to an
|
|
29
|
+
* empty contribution; the CLI always prints a JSON summary and
|
|
30
|
+
* exits 0. Observability MUST NOT take down the delivering flow.
|
|
31
|
+
*
|
|
32
|
+
* Output (stdout, one JSON object):
|
|
33
|
+
* {
|
|
34
|
+
* "kind": "standalone-feedback-rollup",
|
|
35
|
+
* "stories": [<id>, ...],
|
|
36
|
+
* "totalFriction": <int>,
|
|
37
|
+
* "byCategory": { "<category>": <count>, ... },
|
|
38
|
+
* "perStory": { "<id>": { "friction": <int>, "missing": <bool> }, ... }
|
|
39
|
+
* }
|
|
40
|
+
*
|
|
41
|
+
* @see .agents/scripts/lib/observability/signals-writer.js (forEachLine, appendSignal)
|
|
42
|
+
* @see .agents/scripts/lib/config/temp-paths.js (signalsFile — null Epic sentinel)
|
|
43
|
+
* @see .agents/workflows/helpers/deliver-stories.md (Phase 3 summary wiring)
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
47
|
+
import { resolveConfig } from './lib/config-resolver.js';
|
|
48
|
+
import { forEachLine } from './lib/observability/signals-writer.js';
|
|
49
|
+
|
|
50
|
+
const FRICTION_KIND = 'friction';
|
|
51
|
+
const UNKNOWN_CATEGORY = 'Unknown';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse `--stories <id,...>` into an ordered, de-duplicated array of
|
|
55
|
+
* positive-integer Story IDs. Accepts a single comma-separated value or
|
|
56
|
+
* repeated `--stories` flags. Non-integer / non-positive tokens are
|
|
57
|
+
* rejected so a typo surfaces as an input error rather than a silently
|
|
58
|
+
* empty rollup.
|
|
59
|
+
*
|
|
60
|
+
* @param {string[]} args
|
|
61
|
+
* @returns {{ stories: number[] }}
|
|
62
|
+
*/
|
|
63
|
+
export function parseArguments(args) {
|
|
64
|
+
const raw = [];
|
|
65
|
+
for (let i = 0; i < args.length; i++) {
|
|
66
|
+
if (args[i] === '--stories') {
|
|
67
|
+
const value = args[++i];
|
|
68
|
+
if (typeof value === 'string') {
|
|
69
|
+
raw.push(...value.split(','));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
const stories = [];
|
|
76
|
+
for (const token of raw) {
|
|
77
|
+
const trimmed = token.trim();
|
|
78
|
+
if (trimmed.length === 0) continue;
|
|
79
|
+
const id = Number(trimmed);
|
|
80
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`--stories expects comma-separated positive integers; got ${JSON.stringify(trimmed)}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (seen.has(id)) continue;
|
|
86
|
+
seen.add(id);
|
|
87
|
+
stories.push(id);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (stories.length === 0) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
'Usage: node standalone-feedback-rollup.js --stories <id,...>',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { stories };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Read one standalone Story's signals stream and aggregate its `friction`
|
|
101
|
+
* records by top-level `category`, mutating the shared `byCategory` tally.
|
|
102
|
+
*
|
|
103
|
+
* Degrades to a zero contribution on a missing / empty / unreadable
|
|
104
|
+
* stream — `forEachLine` already swallows fs + JSON faults and reports a
|
|
105
|
+
* missing file via `{ missing: true }` rather than throwing.
|
|
106
|
+
*
|
|
107
|
+
* @param {number} storyId
|
|
108
|
+
* @param {Record<string, number>} byCategory Mutated in place.
|
|
109
|
+
* @param {object} config
|
|
110
|
+
* @returns {Promise<{ friction: number, missing: boolean }>}
|
|
111
|
+
*/
|
|
112
|
+
async function rollupStory(storyId, byCategory, config) {
|
|
113
|
+
let friction = 0;
|
|
114
|
+
// `epicId: null` routes forEachLine → signalsFile(null, storyId, config)
|
|
115
|
+
// → the standalone tree. Never a hand-built path string.
|
|
116
|
+
const result = await forEachLine(
|
|
117
|
+
null,
|
|
118
|
+
storyId,
|
|
119
|
+
(record) => {
|
|
120
|
+
if (
|
|
121
|
+
record === null ||
|
|
122
|
+
typeof record !== 'object' ||
|
|
123
|
+
record.kind !== FRICTION_KIND
|
|
124
|
+
) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const category =
|
|
128
|
+
typeof record.category === 'string' && record.category.length > 0
|
|
129
|
+
? record.category
|
|
130
|
+
: UNKNOWN_CATEGORY;
|
|
131
|
+
byCategory[category] = (byCategory[category] ?? 0) + 1;
|
|
132
|
+
friction += 1;
|
|
133
|
+
},
|
|
134
|
+
config,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return { friction, missing: Boolean(result?.missing) };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Aggregate friction across every named standalone Story stream.
|
|
142
|
+
*
|
|
143
|
+
* @param {number[]} stories
|
|
144
|
+
* @param {object} [config]
|
|
145
|
+
* @returns {Promise<{
|
|
146
|
+
* kind: string,
|
|
147
|
+
* stories: number[],
|
|
148
|
+
* totalFriction: number,
|
|
149
|
+
* byCategory: Record<string, number>,
|
|
150
|
+
* perStory: Record<string, { friction: number, missing: boolean }>,
|
|
151
|
+
* }>}
|
|
152
|
+
*/
|
|
153
|
+
export async function buildRollup(stories, config) {
|
|
154
|
+
const byCategory = {};
|
|
155
|
+
const perStory = {};
|
|
156
|
+
let totalFriction = 0;
|
|
157
|
+
|
|
158
|
+
for (const storyId of stories) {
|
|
159
|
+
const { friction, missing } = await rollupStory(
|
|
160
|
+
storyId,
|
|
161
|
+
byCategory,
|
|
162
|
+
config,
|
|
163
|
+
);
|
|
164
|
+
perStory[storyId] = { friction, missing };
|
|
165
|
+
totalFriction += friction;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
kind: 'standalone-feedback-rollup',
|
|
170
|
+
stories,
|
|
171
|
+
totalFriction,
|
|
172
|
+
byCategory,
|
|
173
|
+
perStory,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function main(args = process.argv.slice(2)) {
|
|
178
|
+
const { stories } = parseArguments(args);
|
|
179
|
+
const config = resolveConfig();
|
|
180
|
+
const summary = await buildRollup(stories, config);
|
|
181
|
+
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
runAsCli(import.meta.url, main, {
|
|
186
|
+
source: 'StandaloneFeedbackRollup',
|
|
187
|
+
propagateExitCode: true,
|
|
188
|
+
});
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* envelope. Pipeline shape:
|
|
11
11
|
*
|
|
12
12
|
* 1. parse + resolveCloseInputs (lib/orchestration/story-close/close-inputs.js)
|
|
13
|
+
* 1.5. already-done no-op guard (inline below) — a Story already at
|
|
14
|
+
* `agent::done` AND closed short-circuits here so no phase below it
|
|
15
|
+
* ever flips the label away from `agent::done` or reopens the issue.
|
|
13
16
|
* 2. preflight (phases/preflight.js)
|
|
14
17
|
* 3. state-flip → closing (inline helper below)
|
|
15
18
|
* 4. capture starting branch (phases/branch-restore.js)
|
|
@@ -152,6 +155,51 @@ export async function runStoryClose({
|
|
|
152
155
|
storyBranch,
|
|
153
156
|
} = resolved;
|
|
154
157
|
|
|
158
|
+
// Phase 0 — idempotency guard. A re-run against a Story that is already
|
|
159
|
+
// fully closed must be a safe no-op. Without this guard, Phase 3 below
|
|
160
|
+
// (`transitionToClosing`) unconditionally flips the label to
|
|
161
|
+
// `agent::closing` — and `transitionTicketState`
|
|
162
|
+
// (lib/orchestration/ticketing/transition.js) unconditionally sends
|
|
163
|
+
// `state: 'open'` for any non-`agent::done` target — so re-running close
|
|
164
|
+
// against an already-closed Story reopens the GitHub issue and strips
|
|
165
|
+
// `agent::done` *before* the post-merge pipeline even starts. If the
|
|
166
|
+
// pipeline then throws or a phase silently no-ops (the merge is already
|
|
167
|
+
// fully reaped, so there is nothing left to redo), the Story is left
|
|
168
|
+
// regressed at `agent::closing`/OPEN instead of a safe no-op.
|
|
169
|
+
//
|
|
170
|
+
// The "already fully closed" test mirrors the deepest-level guard in the
|
|
171
|
+
// post-merge `ticketClosurePhase` (post-merge/phases/ticket-closure.js):
|
|
172
|
+
// label `agent::done` AND issue `state === 'closed'`, an AND — never a
|
|
173
|
+
// state-alone check. The `state === 'closed'` disjunct is deliberately
|
|
174
|
+
// excluded per the hard-won reasoning in
|
|
175
|
+
// `lib/single-story/confirm-merge.js`: a Story can arrive here closed at
|
|
176
|
+
// the GitHub layer (a `Closes #<id>` PR footer, or a prior close that set
|
|
177
|
+
// `state: closed` but was killed before the label flip landed) while its
|
|
178
|
+
// label is legitimately still `agent::closing`. Treating that as
|
|
179
|
+
// already-done would skip the very `closing → done` re-assertion the
|
|
180
|
+
// re-run exists to perform, stranding the label. Requiring BOTH signals
|
|
181
|
+
// no-ops only a genuinely finished Story.
|
|
182
|
+
const alreadyDone =
|
|
183
|
+
Array.isArray(story?.labels) &&
|
|
184
|
+
story.labels.includes(STATE_LABELS.DONE) &&
|
|
185
|
+
story?.state === 'closed';
|
|
186
|
+
if (alreadyDone) {
|
|
187
|
+
progress(
|
|
188
|
+
'SKIP',
|
|
189
|
+
`Story #${storyId} is already ${STATE_LABELS.DONE} and closed — no-op re-run.`,
|
|
190
|
+
);
|
|
191
|
+
return {
|
|
192
|
+
success: true,
|
|
193
|
+
result: {
|
|
194
|
+
storyId,
|
|
195
|
+
epicId,
|
|
196
|
+
action: 'noop',
|
|
197
|
+
reason: 'already-done',
|
|
198
|
+
merged: true,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
155
203
|
const notifyFn = (ticketId, payload, opts = {}) =>
|
|
156
204
|
notify(ticketId, payload, { config, provider, ...opts });
|
|
157
205
|
|