mandrel 1.58.0 → 1.59.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 +89 -87
- package/.agents/docs/SDLC.md +11 -7
- package/.agents/docs/workflows.md +2 -1
- package/.agents/schemas/audit-rules.json +20 -0
- package/.agents/scripts/acceptance-eval.js +20 -3
- package/.agents/scripts/assert-branch.js +1 -3
- package/.agents/scripts/bootstrap.js +1 -1
- package/.agents/scripts/check-arch-cycles.js +360 -0
- package/.agents/scripts/coverage-capture.js +24 -3
- package/.agents/scripts/epic-deliver-preflight.js +5 -3
- package/.agents/scripts/epic-deliver-prepare.js +12 -4
- package/.agents/scripts/epic-execute-record-wave.js +1 -1
- package/.agents/scripts/evidence-gate.js +1 -1
- package/.agents/scripts/git-rebase-and-resolve.js +1 -1
- package/.agents/scripts/hierarchy-gate.js +34 -14
- package/.agents/scripts/lib/baselines/kinds/coverage.js +33 -149
- package/.agents/scripts/lib/baselines/kinds/duplication.js +27 -116
- package/.agents/scripts/lib/baselines/kinds/kind-factory.js +192 -0
- package/.agents/scripts/lib/baselines/kinds/lighthouse.js +34 -133
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +31 -124
- package/.agents/scripts/lib/baselines/kinds/mutation.js +25 -111
- package/.agents/scripts/lib/baselines/maintainability-baseline-io.js +59 -0
- package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +37 -0
- package/.agents/scripts/lib/baselines/writer.js +1 -1
- package/.agents/scripts/lib/close-validation/commands.js +188 -0
- package/.agents/scripts/lib/close-validation/gates.js +235 -0
- package/.agents/scripts/lib/close-validation/process.js +101 -0
- package/.agents/scripts/lib/close-validation/projections/maintainability.js +1 -1
- package/.agents/scripts/lib/close-validation/runner.js +325 -0
- package/.agents/scripts/lib/close-validation/telemetry.js +70 -0
- package/.agents/scripts/lib/config/quality.js +6 -6
- package/.agents/scripts/lib/config-resolver.js +2 -5
- package/.agents/scripts/lib/coverage-capture.js +147 -4
- package/.agents/scripts/lib/cpu-pool.js +14 -0
- package/.agents/scripts/lib/crap-utils.js +6 -11
- package/.agents/scripts/lib/dynamic-workflow/documentation-report-contract.js +87 -0
- package/.agents/scripts/lib/git-utils.js +24 -22
- package/.agents/scripts/lib/maintainability-engine.js +1 -1
- package/.agents/scripts/lib/maintainability-utils.js +4 -187
- package/.agents/scripts/lib/observability/perf-report-readers.js +32 -23
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +80 -6
- package/.agents/scripts/lib/orchestration/code-review.js +90 -77
- package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +5 -12
- package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +14 -14
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +184 -49
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +26 -2
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +26 -6
- package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +7 -20
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +1 -2
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +0 -6
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +103 -0
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +22 -64
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +38 -76
- package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +4 -16
- package/.agents/scripts/lib/orchestration/file-assumptions.js +4 -3
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +144 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +2 -2
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +7 -7
- package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +3 -3
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +7 -7
- package/.agents/scripts/lib/orchestration/preflight-cache.js +35 -12
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +5 -60
- package/.agents/scripts/lib/orchestration/review-providers/native.js +7 -6
- package/.agents/scripts/lib/orchestration/review-providers/parse-findings.js +105 -0
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +7 -59
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +2 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -4
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +32 -35
- package/.agents/scripts/lib/orchestration/skill-capsule-loader.js +1 -2
- package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +451 -503
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +8 -2
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +47 -2
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +2 -2
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +358 -54
- package/.agents/scripts/lib/orchestration/story-close/phases/close.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +3 -2
- package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +30 -3
- package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +5 -18
- package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +3 -3
- package/.agents/scripts/lib/orchestration/story-close-recovery.js +33 -16
- package/.agents/scripts/lib/orchestration/story-reachability.js +47 -0
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +2 -33
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +42 -64
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +9 -0
- package/.agents/scripts/lib/orchestration/ticketing/state.js +50 -436
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +471 -0
- package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
- package/.agents/scripts/lib/orchestration/wave-record-notifications.js +1 -1
- package/.agents/scripts/lib/orchestration/wave-record-projection.js +1 -7
- package/.agents/scripts/lib/project-root.js +17 -0
- package/.agents/scripts/lib/story-adjacency.js +76 -0
- package/.agents/scripts/lib/story-lifecycle.js +1 -1
- package/.agents/scripts/lib/transpile.js +93 -0
- package/.agents/scripts/lib/wave-runner/tick.js +4 -153
- package/.agents/scripts/lib/workers/crap-worker.js +1 -1
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +1 -1
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +20 -2
- package/.agents/scripts/lib/worktree/lifecycle/force-drain.js +90 -0
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +26 -8
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +74 -0
- package/.agents/scripts/providers/github/tickets.js +110 -6
- package/.agents/scripts/run-lint.js +9 -0
- package/.agents/scripts/run-tests.js +24 -4
- package/.agents/scripts/stories-wave-tick.js +8 -5
- package/.agents/scripts/story-init.js +149 -10
- package/.agents/scripts/sync-branch-from-base.js +1 -1
- package/.agents/skills/stack/qa/lighthouse-baseline/SKILL.md +1 -1
- package/.agents/workflows/audit-documentation.md +226 -0
- package/.agents/workflows/epic-deliver.md +16 -23
- package/.agents/workflows/epic-plan.md +1 -1
- package/.agents/workflows/helpers/epic-deliver-story.md +17 -28
- package/.agents/workflows/helpers/single-story-deliver.md +2 -1
- package/.agents/workflows/onboard.md +4 -3
- package/.agents/workflows/story-deliver.md +1 -1
- package/README.md +13 -8
- package/lib/cli/init.js +336 -0
- package/package.json +2 -1
- package/.agents/scripts/lib/auto-refresh-baselines.js +0 -308
- package/.agents/scripts/lib/close-validation.js +0 -897
- package/.agents/scripts/lib/orchestration/cascade-grouping.js +0 -275
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter.js +0 -69
- package/.agents/scripts/lib/orchestration/story-close/format-autofix-scoped.js +0 -221
- package/.agents/scripts/lib/orchestration/story-close/format-autofix-shared.js +0 -123
- package/.agents/scripts/lib/task-utils.js +0 -26
- package/.agents/scripts/story-deliver-prepare.js +0 -267
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { parseBlockedBy, parseBlocks } from '../../lib/dependency-parser.js';
|
|
25
|
+
import { Logger } from '../../lib/Logger.js';
|
|
25
26
|
import { addIssueToBoard } from './board-add.js';
|
|
26
27
|
import { createInlineTicketCache } from './cache.js';
|
|
27
28
|
import { withTransientRetry } from './errors.js';
|
|
@@ -32,6 +33,14 @@ import {
|
|
|
32
33
|
parseApiJson,
|
|
33
34
|
} from './request-helpers.js';
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* GitHub Search API hard ceiling is 1000 results per query; at
|
|
38
|
+
* `per_page=100` that is 10 pages. An Epic never has anywhere near 1000
|
|
39
|
+
* children, so hitting this cap means the query is degenerate — we stop
|
|
40
|
+
* rather than throw (the regex post-filter keeps results correct).
|
|
41
|
+
*/
|
|
42
|
+
const SEARCH_PAGE_CAP = 10;
|
|
43
|
+
|
|
35
44
|
/**
|
|
36
45
|
* Compose the final markdown body for a created ticket. Under the 3-tier
|
|
37
46
|
* hierarchy (Epic → Feature → Story), `body` is always a string supplied
|
|
@@ -100,6 +109,14 @@ export class TicketGateway {
|
|
|
100
109
|
this.repo = repo;
|
|
101
110
|
this._hooks = hooks;
|
|
102
111
|
this._cache = cache ?? createInlineTicketCache();
|
|
112
|
+
/**
|
|
113
|
+
* Per-instance memo of `getTickets(epicId, filters)` results (Story
|
|
114
|
+
* #3988). The planning-state-manager fetches the same child list twice
|
|
115
|
+
* per planning pass; without this memo each fetch re-pays the full
|
|
116
|
+
* search/list round-trip. Invalidated on every write surface.
|
|
117
|
+
* @type {Map<string, object[]>}
|
|
118
|
+
*/
|
|
119
|
+
this._listCache = new Map();
|
|
103
120
|
}
|
|
104
121
|
|
|
105
122
|
/**
|
|
@@ -142,29 +159,113 @@ export class TicketGateway {
|
|
|
142
159
|
}
|
|
143
160
|
|
|
144
161
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
162
|
+
* Run one Search API query (`/search/issues`) to completion, returning
|
|
163
|
+
* the raw issue items. Search responses are `{ total_count, items }`
|
|
164
|
+
* envelopes rather than bare arrays, so this paginates manually instead
|
|
165
|
+
* of going through `paginateRest`.
|
|
166
|
+
*/
|
|
167
|
+
async _searchIssues(query) {
|
|
168
|
+
const items = [];
|
|
169
|
+
for (let page = 1; page <= SEARCH_PAGE_CAP; page++) {
|
|
170
|
+
const params = new URLSearchParams({
|
|
171
|
+
q: query,
|
|
172
|
+
per_page: '100',
|
|
173
|
+
page: String(page),
|
|
174
|
+
});
|
|
175
|
+
const result = await withTransientRetry(
|
|
176
|
+
() =>
|
|
177
|
+
this._gh.api({
|
|
178
|
+
method: 'GET',
|
|
179
|
+
endpoint: `/search/issues?${params}`,
|
|
180
|
+
}),
|
|
181
|
+
{ label: `searchIssues page ${page}`, onRetry: defaultRetryWarn },
|
|
182
|
+
);
|
|
183
|
+
const parsed = parseApiJson(result);
|
|
184
|
+
const batch = Array.isArray(parsed?.items) ? parsed.items : [];
|
|
185
|
+
items.push(...batch);
|
|
186
|
+
if (batch.length < 100) break;
|
|
187
|
+
}
|
|
188
|
+
return items;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Server-side narrowed child lookup (Story #3988): two Search API
|
|
193
|
+
* queries — `"Epic: #N" in:body` and `"parent: #N" in:body` — deduped
|
|
194
|
+
* by issue number. Replaces the repo-wide `state=all` pagination that
|
|
195
|
+
* cost ~1 spawn per 100 repo issues and hard-failed past the
|
|
196
|
+
* `paginateRest` page cap. Search tokenization can over-match (e.g.
|
|
197
|
+
* `#10` vs `#100`), so callers MUST keep the word-boundary regex
|
|
198
|
+
* post-filter.
|
|
199
|
+
*/
|
|
200
|
+
async _searchEpicChildren(epicId, filters) {
|
|
201
|
+
const qualifiers = [`repo:${this.owner}/${this.repo}`, 'is:issue'];
|
|
202
|
+
const state = filters.state ?? 'all';
|
|
203
|
+
if (state === 'open' || state === 'closed') {
|
|
204
|
+
qualifiers.push(`state:${state}`);
|
|
205
|
+
}
|
|
206
|
+
if (filters.label) qualifiers.push(`label:"${filters.label}"`);
|
|
207
|
+
const base = qualifiers.join(' ');
|
|
208
|
+
|
|
209
|
+
const [epicRefs, parentRefs] = await Promise.all([
|
|
210
|
+
this._searchIssues(`${base} "Epic: #${epicId}" in:body`),
|
|
211
|
+
this._searchIssues(`${base} "parent: #${epicId}" in:body`),
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
const byNumber = new Map();
|
|
215
|
+
for (const issue of [...epicRefs, ...parentRefs]) {
|
|
216
|
+
if (!byNumber.has(issue.number)) byNumber.set(issue.number, issue);
|
|
217
|
+
}
|
|
218
|
+
return Array.from(byNumber.values());
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Repo-wide listing fallback — the pre-#3988 shape. Only used when the
|
|
223
|
+
* Search API path fails (search outage, search-specific rate limit).
|
|
147
224
|
*/
|
|
148
225
|
/* node:coverage ignore next */
|
|
149
|
-
async
|
|
226
|
+
async _listAllIssues(filters) {
|
|
150
227
|
const params = new URLSearchParams({ state: filters.state ?? 'all' });
|
|
151
228
|
if (filters.label) params.set('labels', filters.label);
|
|
152
|
-
|
|
153
229
|
const endpoint = `/repos/${this.owner}/${this.repo}/issues?${params}`;
|
|
154
|
-
|
|
230
|
+
return paginateRest(this._gh, endpoint);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @field-manifest /search/issues?q=...: number, id, node_id, title,
|
|
235
|
+
* body, labels, state, pull_request
|
|
236
|
+
* @field-manifest /repos/{owner}/{repo}/issues?state=...&labels=...:
|
|
237
|
+
* number, body, labels, state, pull_request
|
|
238
|
+
*/
|
|
239
|
+
async getTickets(epicId, filters = {}) {
|
|
240
|
+
const memoKey = `${epicId}|${filters.state ?? 'all'}|${filters.label ?? ''}`;
|
|
241
|
+
if (this._listCache.has(memoKey)) return this._listCache.get(memoKey);
|
|
242
|
+
|
|
243
|
+
let issues;
|
|
244
|
+
try {
|
|
245
|
+
issues = await this._searchEpicChildren(epicId, filters);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
const msg = typeof err?.message === 'string' ? err.message : String(err);
|
|
248
|
+
Logger.warn(
|
|
249
|
+
`[TicketGateway] search-based getTickets(#${epicId}) failed (${msg}); ` +
|
|
250
|
+
'falling back to repo-wide issue listing',
|
|
251
|
+
);
|
|
252
|
+
issues = await this._listAllIssues(filters);
|
|
253
|
+
}
|
|
155
254
|
|
|
156
255
|
// Word-boundary regex prevents #1 matching #10, #100, etc.
|
|
157
256
|
const epicRefRe = new RegExp(
|
|
158
257
|
`(?:Epic:\\s*#${epicId}|parent:\\s*#${epicId})(?:\\s|$|[,.)\\]])`,
|
|
159
258
|
);
|
|
160
259
|
|
|
161
|
-
|
|
260
|
+
const tickets = issues
|
|
162
261
|
.filter((issue) => {
|
|
163
262
|
if (issue.pull_request) return false;
|
|
164
263
|
const body = issue.body ?? '';
|
|
165
264
|
return epicRefRe.test(body);
|
|
166
265
|
})
|
|
167
266
|
.map(issueToListItem);
|
|
267
|
+
this._listCache.set(memoKey, tickets);
|
|
268
|
+
return tickets;
|
|
168
269
|
}
|
|
169
270
|
|
|
170
271
|
/* node:coverage ignore next */
|
|
@@ -187,6 +288,7 @@ export class TicketGateway {
|
|
|
187
288
|
|
|
188
289
|
invalidateTicket(ticketId) {
|
|
189
290
|
this._cache.invalidate(ticketId);
|
|
291
|
+
this._listCache.clear();
|
|
190
292
|
}
|
|
191
293
|
|
|
192
294
|
// ---------------------------------------------------------------------------
|
|
@@ -226,6 +328,7 @@ export class TicketGateway {
|
|
|
226
328
|
},
|
|
227
329
|
});
|
|
228
330
|
const issue = parseApiJson(result);
|
|
331
|
+
this._listCache.clear();
|
|
229
332
|
|
|
230
333
|
let subIssueLinked = false;
|
|
231
334
|
let subIssueError = null;
|
|
@@ -288,6 +391,7 @@ export class TicketGateway {
|
|
|
288
391
|
body: { title, body, labels },
|
|
289
392
|
});
|
|
290
393
|
const issue = parseApiJson(result);
|
|
394
|
+
this._listCache.clear();
|
|
291
395
|
|
|
292
396
|
const boardAdd = await addIssueToBoard({
|
|
293
397
|
nodeId: issue.node_id,
|
|
@@ -69,6 +69,15 @@ const tasks = [
|
|
|
69
69
|
cmd: 'node',
|
|
70
70
|
args: ['.agents/scripts/lint-label-vocabulary.js'],
|
|
71
71
|
},
|
|
72
|
+
{
|
|
73
|
+
// Architecture cycle ratchet (Story #3991). Detects directed import
|
|
74
|
+
// cycles under `.agents/scripts/` and fails on any cycle not in the
|
|
75
|
+
// committed allowlist (`baselines/arch-cycles.json`). Mirrors the
|
|
76
|
+
// ratchet-down semantics of `check-dead-exports.js`.
|
|
77
|
+
name: 'arch-cycles',
|
|
78
|
+
cmd: 'node',
|
|
79
|
+
args: ['.agents/scripts/check-arch-cycles.js'],
|
|
80
|
+
},
|
|
72
81
|
];
|
|
73
82
|
|
|
74
83
|
function runTask({ name, cmd, args }) {
|
|
@@ -84,11 +84,31 @@ export const TEST_RUNNER_FLAGS = Object.freeze([
|
|
|
84
84
|
* quoting). Budgeting the targets to 8 000 chars keeps every spawn's full
|
|
85
85
|
* command line at roughly a quarter of the ceiling — ample headroom for the
|
|
86
86
|
* exe path and any pass-through `--test-name-pattern` args — while keeping
|
|
87
|
-
* the chunk count (and thus the extra `node` start-ups) low.
|
|
88
|
-
*
|
|
89
|
-
* Windows.
|
|
87
|
+
* the chunk count (and thus the extra `node` start-ups) low.
|
|
88
|
+
*
|
|
89
|
+
* `MAX_TARGET_CHARS` is the **Windows** budget. On POSIX hosts `ARG_MAX` is
|
|
90
|
+
* far higher (~256 KB on macOS, ~1 MB on Linux), so applying the Windows
|
|
91
|
+
* budget there needlessly serializes the quick tier into several sequential
|
|
92
|
+
* `node --test` spawns — each chunk pays a fresh runner start-up, and cores
|
|
93
|
+
* idle at every chunk's tail. `POSIX_MAX_TARGET_CHARS` (100 000) lets the
|
|
94
|
+
* whole quick-tier target list (~33 000 chars today) collapse into a single
|
|
95
|
+
* spawn on POSIX while staying far below `ARG_MAX`.
|
|
96
|
+
* `resolveMaxTargetChars` picks the budget per platform; the Windows
|
|
97
|
+
* semantics of `MAX_TARGET_CHARS` are unchanged.
|
|
90
98
|
*/
|
|
91
99
|
export const MAX_TARGET_CHARS = 8000;
|
|
100
|
+
export const POSIX_MAX_TARGET_CHARS = 100_000;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the per-spawn target-character budget for the host platform.
|
|
104
|
+
* The `platform` parameter is injected in tests.
|
|
105
|
+
*
|
|
106
|
+
* @param {NodeJS.Platform} [platform]
|
|
107
|
+
* @returns {number}
|
|
108
|
+
*/
|
|
109
|
+
export function resolveMaxTargetChars(platform = process.platform) {
|
|
110
|
+
return platform === 'win32' ? MAX_TARGET_CHARS : POSIX_MAX_TARGET_CHARS;
|
|
111
|
+
}
|
|
92
112
|
|
|
93
113
|
/**
|
|
94
114
|
* Partition an ordered list of test-file targets into chunks whose joined
|
|
@@ -144,7 +164,7 @@ export function runTestSuite({
|
|
|
144
164
|
spawn = spawnSync,
|
|
145
165
|
cleanup = cleanupRepoTestTempArtifacts,
|
|
146
166
|
listTargets = listTestFilesForTier,
|
|
147
|
-
maxTargetChars =
|
|
167
|
+
maxTargetChars = resolveMaxTargetChars(),
|
|
148
168
|
} = {}) {
|
|
149
169
|
const { tier, rest } = parseTierArgv(argv);
|
|
150
170
|
const targets = listTargets(tier, cwd);
|
|
@@ -43,6 +43,7 @@ import { runAsCli } from './lib/cli-utils.js';
|
|
|
43
43
|
import { getRunners, resolveConfig } from './lib/config-resolver.js';
|
|
44
44
|
import { assignLayers, detectCycle } from './lib/Graph.js';
|
|
45
45
|
import { Logger } from './lib/Logger.js';
|
|
46
|
+
import { buildStoryAdjacency } from './lib/story-adjacency.js';
|
|
46
47
|
|
|
47
48
|
const HELP = `Usage: node .agents/scripts/stories-wave-tick.js --dag '<json>' | --dag-file <path> [--concurrency <n>]
|
|
48
49
|
|
|
@@ -132,15 +133,17 @@ export function parseDag(raw) {
|
|
|
132
133
|
* Build an adjacency map from parsed DAG nodes.
|
|
133
134
|
* Returns Map<id, id[]> where each id maps to its dependencies.
|
|
134
135
|
*
|
|
136
|
+
* Delegates to the shared story-level builder
|
|
137
|
+
* (`lib/story-adjacency.js#buildStoryAdjacency`) with `dropForeign: false`
|
|
138
|
+
* to preserve the operator-DAG contract: a `dependsOn` id absent from the
|
|
139
|
+
* input set still deepens the dependent's layer (assignLayers treats the
|
|
140
|
+
* unknown id as a root).
|
|
141
|
+
*
|
|
135
142
|
* @param {Array<{id: number, dependsOn: number[]}>} nodes
|
|
136
143
|
* @returns {Map<number, number[]>}
|
|
137
144
|
*/
|
|
138
145
|
export function buildAdjacency(nodes) {
|
|
139
|
-
|
|
140
|
-
for (const node of nodes) {
|
|
141
|
-
adjacency.set(node.id, [...node.dependsOn]);
|
|
142
|
-
}
|
|
143
|
-
return adjacency;
|
|
146
|
+
return buildStoryAdjacency(nodes, { dropForeign: false });
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
/**
|
|
@@ -38,8 +38,13 @@ import {
|
|
|
38
38
|
} from './lib/config-resolver.js';
|
|
39
39
|
import { parseBlockedBy } from './lib/dependency-parser.js';
|
|
40
40
|
import { getEpicBranch, getStoryBranch } from './lib/git-utils.js';
|
|
41
|
+
import { runInstallCommand } from './lib/install-cmd-parser.js';
|
|
41
42
|
import { Logger } from './lib/Logger.js';
|
|
42
43
|
import { setActiveStoryEnv } from './lib/observability/active-story-env.js';
|
|
44
|
+
import {
|
|
45
|
+
defaultStoryPhases,
|
|
46
|
+
upsertStoryRunProgress,
|
|
47
|
+
} from './lib/orchestration/epic-runner/story-run-progress-writer.js';
|
|
43
48
|
import { upsertStructuredComment } from './lib/orchestration/ticketing.js';
|
|
44
49
|
import { createProvider } from './lib/provider-factory.js';
|
|
45
50
|
import { validateBlockers } from './lib/story-init/blocker-validator.js';
|
|
@@ -330,11 +335,6 @@ export async function runStoryInit({
|
|
|
330
335
|
hierarchy: hierarchyMode,
|
|
331
336
|
});
|
|
332
337
|
|
|
333
|
-
emitStoryInitResult(result, {
|
|
334
|
-
storyId,
|
|
335
|
-
dryRun,
|
|
336
|
-
});
|
|
337
|
-
|
|
338
338
|
if (!dryRun) {
|
|
339
339
|
await postStoryInitComment({
|
|
340
340
|
provider,
|
|
@@ -342,11 +342,153 @@ export async function runStoryInit({
|
|
|
342
342
|
result,
|
|
343
343
|
logger: stageLogger,
|
|
344
344
|
});
|
|
345
|
+
|
|
346
|
+
// Story #4017 — the formerly standalone prepare CLI (which
|
|
347
|
+
// re-read the story-init structured comment seconds after this process
|
|
348
|
+
// wrote it) is inlined here: apply the install tri-state and render the
|
|
349
|
+
// initial Story-phase snapshot in-process, off the result we already
|
|
350
|
+
// hold.
|
|
351
|
+
result.prepare = await runStoryInitPrepare({
|
|
352
|
+
provider,
|
|
353
|
+
storyId,
|
|
354
|
+
result,
|
|
355
|
+
notify: _notifyFn,
|
|
356
|
+
logger: stageLogger,
|
|
357
|
+
});
|
|
345
358
|
}
|
|
346
359
|
|
|
360
|
+
emitStoryInitResult(result, {
|
|
361
|
+
storyId,
|
|
362
|
+
dryRun,
|
|
363
|
+
});
|
|
364
|
+
|
|
347
365
|
return { success: true, result };
|
|
348
366
|
}
|
|
349
367
|
|
|
368
|
+
const VALID_INSTALLED_STATES = new Set(['true', 'false', 'skipped']);
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Apply the dependenciesInstalled tri-state to derive the next install
|
|
372
|
+
* action. Pure helper — exposes the Step 0.5 truth table as data so tests
|
|
373
|
+
* can pin each branch without spinning up a child process.
|
|
374
|
+
*
|
|
375
|
+
* @param {'true' | 'false' | 'skipped'} dependenciesInstalled
|
|
376
|
+
* @param {{ skipInstall?: boolean }} [options]
|
|
377
|
+
* @returns {'skip' | 'install'}
|
|
378
|
+
*/
|
|
379
|
+
export function deriveInstallAction(dependenciesInstalled, options = {}) {
|
|
380
|
+
if (!VALID_INSTALLED_STATES.has(dependenciesInstalled)) {
|
|
381
|
+
throw new RangeError(
|
|
382
|
+
`deriveInstallAction: dependenciesInstalled "${dependenciesInstalled}" must be one of: ${[...VALID_INSTALLED_STATES].join(', ')}`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
if (options.skipInstall) return 'skip';
|
|
386
|
+
return dependenciesInstalled === 'false' ? 'install' : 'skip';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Resolve the install command to run when `dependenciesInstalled === 'false'`.
|
|
391
|
+
* `project.commands` does not currently carry a dedicated install key,
|
|
392
|
+
* so this defaults to `npm ci`. Operators can override per-invocation via
|
|
393
|
+
* the `installCmd` option.
|
|
394
|
+
*
|
|
395
|
+
* @param {{ override?: string }} [options]
|
|
396
|
+
* @returns {string}
|
|
397
|
+
*/
|
|
398
|
+
export function resolveInstallCommand(options = {}) {
|
|
399
|
+
const trimmed = options.override?.trim();
|
|
400
|
+
if (trimmed) {
|
|
401
|
+
return trimmed;
|
|
402
|
+
}
|
|
403
|
+
return 'npm ci';
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Post-init prepare step (Story #4017 — formerly a standalone prepare
|
|
408
|
+
* CLI, now consuming the in-process init result
|
|
409
|
+
* instead of re-reading the `story-init` structured comment):
|
|
410
|
+
*
|
|
411
|
+
* 1. Apply the `dependenciesInstalled` tri-state truth table — `'false'`
|
|
412
|
+
* (install attempted and failed) retries the install command in the
|
|
413
|
+
* worktree; `'true'` / `'skipped'` proceed.
|
|
414
|
+
* 2. Render the initial Story-phase snapshot with every phase pinned to
|
|
415
|
+
* `pending` and `phase: 'init'` via `upsertStoryRunProgress`
|
|
416
|
+
* (render-only since Story #3909 — no comment is posted). The
|
|
417
|
+
* `renderedBody` markdown is relayed to chat by the delivery
|
|
418
|
+
* workflows so operators see the initial progress block before the
|
|
419
|
+
* first commit lands.
|
|
420
|
+
*
|
|
421
|
+
* Install failure throws (init exits non-zero); a snapshot-render failure
|
|
422
|
+
* is non-fatal observability loss and only warns.
|
|
423
|
+
*
|
|
424
|
+
* @param {{
|
|
425
|
+
* provider: object,
|
|
426
|
+
* storyId: number,
|
|
427
|
+
* result: { workCwd?: string, dependenciesInstalled?: string, storyBranch?: string },
|
|
428
|
+
* notify?: Function | null,
|
|
429
|
+
* runInstall?: (cmd: string, cwd: string) => { status: number, stderr?: string },
|
|
430
|
+
* skipInstall?: boolean,
|
|
431
|
+
* installCmd?: string,
|
|
432
|
+
* logger?: object,
|
|
433
|
+
* }} args
|
|
434
|
+
* @returns {Promise<{
|
|
435
|
+
* installAction: 'skip' | 'install',
|
|
436
|
+
* installCmd: string | null,
|
|
437
|
+
* installResult: { status: number, stderr?: string } | null,
|
|
438
|
+
* snapshot: object | null,
|
|
439
|
+
* renderedBody: string | null,
|
|
440
|
+
* }>}
|
|
441
|
+
*/
|
|
442
|
+
export async function runStoryInitPrepare({
|
|
443
|
+
provider,
|
|
444
|
+
storyId,
|
|
445
|
+
result,
|
|
446
|
+
notify: notifyFn = null,
|
|
447
|
+
runInstall = runInstallCommand,
|
|
448
|
+
skipInstall = false,
|
|
449
|
+
installCmd: installCmdOverride,
|
|
450
|
+
logger = stageLogger,
|
|
451
|
+
}) {
|
|
452
|
+
const dependenciesInstalled = String(
|
|
453
|
+
result?.dependenciesInstalled ?? 'skipped',
|
|
454
|
+
);
|
|
455
|
+
const installAction = deriveInstallAction(dependenciesInstalled, {
|
|
456
|
+
skipInstall,
|
|
457
|
+
});
|
|
458
|
+
let installCmd = null;
|
|
459
|
+
let installResult = null;
|
|
460
|
+
if (installAction === 'install') {
|
|
461
|
+
installCmd = resolveInstallCommand({ override: installCmdOverride });
|
|
462
|
+
installResult = runInstall(installCmd, result.workCwd);
|
|
463
|
+
if (installResult.status !== 0) {
|
|
464
|
+
throw new Error(
|
|
465
|
+
`runStoryInitPrepare: install command \`${installCmd}\` failed with status ${installResult.status}: ${installResult.stderr ?? ''}`,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
let snapshot = null;
|
|
471
|
+
let renderedBody = null;
|
|
472
|
+
try {
|
|
473
|
+
const { body, payload } = await upsertStoryRunProgress({
|
|
474
|
+
provider,
|
|
475
|
+
storyId,
|
|
476
|
+
branch: result?.storyBranch ?? `story-${storyId}`,
|
|
477
|
+
phase: 'init',
|
|
478
|
+
phases: defaultStoryPhases(),
|
|
479
|
+
notify: notifyFn,
|
|
480
|
+
});
|
|
481
|
+
snapshot = payload;
|
|
482
|
+
renderedBody = body;
|
|
483
|
+
} catch (err) {
|
|
484
|
+
logger?.warn?.(
|
|
485
|
+
`[story-init] ⚠️ Failed to render initial story-run-progress snapshot: ${err?.message ?? err}`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return { installAction, installCmd, installResult, snapshot, renderedBody };
|
|
490
|
+
}
|
|
491
|
+
|
|
350
492
|
function buildStoryInitResult({
|
|
351
493
|
storyId,
|
|
352
494
|
epicId,
|
|
@@ -458,11 +600,8 @@ export function renderStoryInitCommentBody(result) {
|
|
|
458
600
|
worktreeCreated: result.worktreeCreated,
|
|
459
601
|
dependenciesInstalled: result.dependenciesInstalled,
|
|
460
602
|
installStatus: result.installStatus,
|
|
461
|
-
// Embed the canonical task list so
|
|
462
|
-
//
|
|
463
|
-
// Without this field, the prepare CLI silently seeded an empty snapshot,
|
|
464
|
-
// breaking every subsequent phase-writer call (it asserts the
|
|
465
|
-
// task id is present in the snapshot).
|
|
603
|
+
// Embed the canonical task list so downstream snapshot consumers can
|
|
604
|
+
// seed phase-writer calls without re-fetching the task graph.
|
|
466
605
|
tasks: Array.isArray(result.tasks)
|
|
467
606
|
? result.tasks.map((t) => ({ id: t.id, title: t.title }))
|
|
468
607
|
: [],
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
import path from 'node:path';
|
|
32
32
|
import { parseArgs } from 'node:util';
|
|
33
33
|
import { runAsCli } from './lib/cli-utils.js';
|
|
34
|
-
import { PROJECT_ROOT } from './lib/config-resolver.js';
|
|
35
34
|
import { syncBranchFromBase } from './lib/git/sync-from-base.js';
|
|
36
35
|
import { gitSpawn, gitSync } from './lib/git-utils.js';
|
|
37
36
|
import { Logger } from './lib/Logger.js';
|
|
37
|
+
import { PROJECT_ROOT } from './lib/project-root.js';
|
|
38
38
|
|
|
39
39
|
const progress = Logger.createProgress('sync-branch-from-base', {
|
|
40
40
|
stderr: true,
|
|
@@ -58,7 +58,7 @@ The five pieces every baseline of this shape carries:
|
|
|
58
58
|
3. **`<name>:check` npm script** — runs the measurement, compares against
|
|
59
59
|
`baselines/<name>.json` with a tolerance, exits non-zero on regression.
|
|
60
60
|
Wired into the close-validation gate chain (see `buildDefaultGates` in
|
|
61
|
-
`lib/close-validation.js`) and into the PR gate.
|
|
61
|
+
`lib/close-validation/gates.js`) and into the PR gate.
|
|
62
62
|
4. **`--self-test` flag** — every check script accepts `--self-test`,
|
|
63
63
|
which runs the comparator against synthetic inputs (a known-good and a
|
|
64
64
|
known-regression fixture) and asserts the gate's verdict matches. CI
|