mandrel 2.56.0 → 2.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/plan-critic.md +13 -18
- package/.agents/agents/story-worker.md +25 -34
- package/.agents/docs/agentrc-reference.json +0 -30
- package/.agents/docs/configuration.md +8 -28
- package/.agents/docs/execution-reference.md +5 -5
- package/.agents/docs/quality-gates.md +8 -7
- package/.agents/instructions.md +9 -10
- package/.agents/schemas/agentrc.schema.json +9 -185
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +107 -17
- package/.agents/scripts/ceremony-derive.js +191 -0
- package/.agents/scripts/check-context-budget.js +28 -33
- package/.agents/scripts/check-cyclomatic.js +4 -3
- package/.agents/scripts/deliver-light.js +31 -94
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
- package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
- package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
- package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
- package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
- package/.agents/scripts/lib/close-validation/gates.js +52 -1
- package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
- package/.agents/scripts/lib/config/delivery-routing.js +7 -33
- package/.agents/scripts/lib/config/explain.js +0 -19
- package/.agents/scripts/lib/config/limits.js +18 -78
- package/.agents/scripts/lib/config/quality.js +6 -3
- package/.agents/scripts/lib/config/runners.js +3 -2
- package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
- package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
- package/.agents/scripts/lib/config-settings-schema.js +16 -143
- package/.agents/scripts/lib/crap-engine.js +35 -4
- package/.agents/scripts/lib/crap-utils.js +17 -1
- package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
- package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
- package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
- package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
- package/.agents/scripts/lib/orchestration/plan-context.js +181 -387
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +300 -0
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +131 -168
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +118 -297
- package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +30 -139
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
- package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
- package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
- package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
- package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
- package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
- package/.agents/scripts/lib/story-body/story-body.js +17 -237
- package/.agents/scripts/lib/templates/decomposer-prompts.js +84 -121
- package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
- package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
- package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
- package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
- package/.agents/scripts/lib/test-run-credit.js +266 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
- package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
- package/.agents/scripts/lib/workers/crap-worker.js +32 -41
- package/.agents/scripts/plan-context.js +7 -9
- package/.agents/scripts/plan-critics.js +28 -54
- package/.agents/scripts/plan-persist.js +25 -68
- package/.agents/scripts/quality-preview.js +51 -0
- package/.agents/scripts/run-tests.js +12 -0
- package/.agents/scripts/stories-wave-tick.js +23 -45
- package/.agents/scripts/test-isolate.js +13 -180
- package/.agents/scripts/update-coverage-baseline.js +25 -70
- package/.agents/scripts/update-crap-baseline.js +19 -123
- package/.agents/skills/core/scope-triage/SKILL.md +3 -3
- package/.agents/workflows/audit-clean-code.md +4 -3
- package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
- package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
- package/.agents/workflows/helpers/code-review.md +2 -3
- package/.agents/workflows/helpers/deliver-digest.md +41 -57
- package/.agents/workflows/helpers/deliver-light.md +40 -105
- package/.agents/workflows/helpers/deliver-reference.md +1 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +37 -58
- package/.agents/workflows/helpers/deliver-story.md +9 -13
- package/.agents/workflows/helpers/plan-reference.md +132 -219
- package/.agents/workflows/mandrel-plan.md +27 -40
- package/.agents/workflows/memory-consolidate.md +9 -13
- package/docs/CHANGELOG.md +23 -0
- package/lib/migrations/index.js +4 -0
- package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
- package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
- package/package.json +1 -1
- package/.agents/scripts/lib/framework-version.js +0 -39
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
- package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
- package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
- package/.agents/scripts/lib/orchestration/verify-tier-repair.js +0 -107
|
@@ -1,124 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* plan-critic-conditions.js —
|
|
3
|
-
* the
|
|
2
|
+
* plan-critic-conditions.js — the pre-mortem critic's dispatch decision for
|
|
3
|
+
* the `/mandrel-plan` critic CLI (Epic #4474 PR6, design §4; narrowed to one
|
|
4
|
+
* arm by Story #5312).
|
|
4
5
|
*
|
|
5
|
-
* The
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* computes those decisions deterministically so the workflow never judges
|
|
11
|
-
* its own dispatch conditions:
|
|
6
|
+
* The critic is a fresh-context sub-agent dispatch, and the dispatch is
|
|
7
|
+
* **conditional**: the dominant plan cost is turns × standing context, and an
|
|
8
|
+
* unconditional critic pays a full sub-agent spawn even when it provably has
|
|
9
|
+
* nothing to find. This module computes the decision deterministically so
|
|
10
|
+
* the workflow never judges its own dispatch condition.
|
|
12
11
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* table). A fail-open precondition (missing/unparseable table) on a
|
|
18
|
-
* small draft is NOT a confirmed divergence — it skips, because a
|
|
19
|
-
* ≤-threshold draft is small enough for gate #2's single-view review
|
|
20
|
-
* to catch a distorted shape without a dedicated sub-agent.
|
|
21
|
-
* - **Pre-mortem**: dispatch when the ticket count is at least half
|
|
22
|
-
* of `maxTickets`, OR any configured `planning.riskHeuristics` phrase
|
|
23
|
-
* matches the plan text (case-insensitive substring), OR the
|
|
24
|
-
* **external-dependency probe** (Story #4700) finds an out-of-repo marker
|
|
25
|
-
* in the plan text. Story #4542 removed the authored-risk-verdict condition
|
|
26
|
-
* along with the verdict itself; every surviving condition reads the plan's
|
|
27
|
-
* own observable text and shape rather than a self-assessment.
|
|
28
|
-
*
|
|
29
|
-
* The external-dependency probe (Story #4700) is what gives the default N=1
|
|
30
|
-
* path a cheap viability check: on that path the size condition is unreachable
|
|
31
|
-
* (`count*2 >= maxTickets` never holds at one ticket) and a repo whose resolved
|
|
32
|
-
* `planning.riskHeuristics` is empty has no phrase to match, so a plan-time
|
|
33
|
-
* discoverable blocker — a scoped package the plan names that no manifest
|
|
34
|
-
* declares, a cross-repo reference, an external service prerequisite — reached
|
|
12
|
+
* One trigger survives: the **external-dependency probe** (Story #4700),
|
|
13
|
+
* which finds an out-of-repo marker in the plan text — a scoped package the
|
|
14
|
+
* plan names that no manifest declares, a cross-repo reference, an external
|
|
15
|
+
* service prerequisite — so a plan-time discoverable blocker does not reach
|
|
35
16
|
* delivery unquestioned (the swarm-os #757 shape). The probe is deliberately
|
|
36
|
-
* **conservative**: it matches only explicit markers (npm scoped-package
|
|
37
|
-
* `github.com/<owner>/<repo>` URLs, prerequisite-keyword-anchored
|
|
38
|
-
* never NLP guesswork
|
|
39
|
-
*
|
|
17
|
+
* **conservative**: it matches only explicit markers (npm scoped-package
|
|
18
|
+
* specs, `github.com/<owner>/<repo>` URLs, prerequisite-keyword-anchored
|
|
19
|
+
* endpoints), never NLP guesswork.
|
|
20
|
+
*
|
|
21
|
+
* Story #5312 deleted the two triggers that sat beside it — the ticket count
|
|
22
|
+
* reaching half a `maxTickets` budget, and a `planning.riskHeuristics` phrase
|
|
23
|
+
* matching the plan text — with the constants they read. The count trigger
|
|
24
|
+
* was unreachable at the default N=1; the phrase list was empty in every
|
|
25
|
+
* consumer that resolved it. The consolidation critic went with them: its
|
|
26
|
+
* one deterministic input was a `## Delivery Slicing` table no Story carries.
|
|
40
27
|
*
|
|
41
|
-
* Under-firing risk (design PR6 note): the persist validators are
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
28
|
+
* Under-firing risk (design PR6 note): the persist validators are unchanged
|
|
29
|
+
* hard gates; every skip decision this module produces is logged to the
|
|
30
|
+
* plan-metrics ledger (`appendCriticSkip`) by the caller so under-firing is
|
|
31
|
+
* auditable.
|
|
45
32
|
*
|
|
46
33
|
* Pure, synchronous, no I/O. The single caller is `plan-critics-evaluate.js`,
|
|
47
|
-
* driven by the `plan-critics.js` CLI
|
|
48
|
-
* Persist
|
|
49
|
-
* resolved config.
|
|
50
|
-
*/
|
|
51
|
-
|
|
52
|
-
import { evaluateConsolidationPrecondition } from './consolidation-precondition.js';
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Draft-story count above which the consolidation critic fires even
|
|
56
|
-
* without a confirmed slicing divergence (#4474 PR6: "> 5 stories").
|
|
34
|
+
* driven by the `plan-critics.js` CLI the operator runs between Author and
|
|
35
|
+
* Persist when they want the critic; the CLI owns reading the authored
|
|
36
|
+
* artifacts and the resolved config.
|
|
57
37
|
*/
|
|
58
|
-
export const CONSOLIDATION_STORY_THRESHOLD = 5;
|
|
59
38
|
|
|
60
39
|
/**
|
|
61
40
|
* @typedef {Object} CriticDispatchDecision
|
|
62
|
-
* @property {'
|
|
41
|
+
* @property {'pre-mortem'} critic
|
|
63
42
|
* @property {boolean} dispatch
|
|
64
43
|
* @property {string[]} reasons Why the critic fires — or why it is safe to
|
|
65
44
|
* skip. Never empty: a skip's reasons are the audit trail the
|
|
66
45
|
* plan-metrics ledger records.
|
|
67
46
|
*/
|
|
68
47
|
|
|
69
|
-
/**
|
|
70
|
-
* Decide the consolidation dispatch: precondition AND size/divergence.
|
|
71
|
-
*
|
|
72
|
-
* @param {object} input
|
|
73
|
-
* @param {object[]} input.draftStories - The draft `tickets.json` array
|
|
74
|
-
* (raw Story objects with top-level `slug` / `depends_on` / `body`).
|
|
75
|
-
* @param {string} input.specText - The text carrying the `## Delivery
|
|
76
|
-
* Slicing` table. At author time this is the authored `techspec.md`
|
|
77
|
-
* content (the Epic body carries the same folded section post-persist).
|
|
78
|
-
* @returns {CriticDispatchDecision}
|
|
79
|
-
*/
|
|
80
|
-
export function evaluateConsolidationDispatch({ draftStories, specText }) {
|
|
81
|
-
const precondition = evaluateConsolidationPrecondition({
|
|
82
|
-
draftStories,
|
|
83
|
-
epicBody: specText,
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
if (!precondition.dispatch) {
|
|
87
|
-
return {
|
|
88
|
-
critic: 'consolidation',
|
|
89
|
-
dispatch: false,
|
|
90
|
-
reasons: precondition.reasons,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const storyCount = draftStories.length;
|
|
95
|
-
const oversized = storyCount > CONSOLIDATION_STORY_THRESHOLD;
|
|
96
|
-
const diverges = precondition.cause === 'divergence';
|
|
97
|
-
|
|
98
|
-
if (!oversized && !diverges) {
|
|
99
|
-
return {
|
|
100
|
-
critic: 'consolidation',
|
|
101
|
-
dispatch: false,
|
|
102
|
-
reasons: [
|
|
103
|
-
`Draft has ${storyCount} story(ies) (≤ ${CONSOLIDATION_STORY_THRESHOLD}) and no confirmed Delivery Slicing divergence — gate #2's single-view review covers a draft this small.`,
|
|
104
|
-
...precondition.reasons,
|
|
105
|
-
],
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const reasons = [];
|
|
110
|
-
if (diverges) reasons.push(...precondition.reasons);
|
|
111
|
-
if (oversized) {
|
|
112
|
-
reasons.push(
|
|
113
|
-
`Draft has ${storyCount} stories (> ${CONSOLIDATION_STORY_THRESHOLD}) — large enough that a distorted shape can hide from the gate #2 single view.`,
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
if (!diverges && precondition.cause === 'fail-open') {
|
|
117
|
-
reasons.push(...precondition.reasons);
|
|
118
|
-
}
|
|
119
|
-
return { critic: 'consolidation', dispatch: true, reasons };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
48
|
/**
|
|
123
49
|
* Explicit npm scoped-package marker: `@scope/name`. Requires the leading `@`
|
|
124
50
|
* and an interior `/`, so bare GitHub handles (`@dsj1984`) and the
|
|
@@ -215,8 +141,8 @@ function matchExternalServicePrereqs(planText) {
|
|
|
215
141
|
/**
|
|
216
142
|
* The external-dependency probe (Story #4700): a conservative, marker-only
|
|
217
143
|
* scan of the draft plan text for artifacts outside the current repo that the
|
|
218
|
-
* plan depends on. A match is the pre-mortem's
|
|
219
|
-
*
|
|
144
|
+
* plan depends on. A match is the pre-mortem's dispatch condition; a no-match
|
|
145
|
+
* plan skips the critic.
|
|
220
146
|
*
|
|
221
147
|
* @param {object} input
|
|
222
148
|
* @param {string} [input.planText] - Concatenated plan text (tech spec +
|
|
@@ -262,19 +188,12 @@ export function evaluateExternalDependencyProbe({
|
|
|
262
188
|
}
|
|
263
189
|
|
|
264
190
|
/**
|
|
265
|
-
* Decide the pre-mortem dispatch:
|
|
266
|
-
*
|
|
191
|
+
* Decide the pre-mortem dispatch: an external-dependency probe match
|
|
192
|
+
* (Story #4700) — the one deterministic trigger left after Story #5312.
|
|
267
193
|
*
|
|
268
194
|
* @param {object} input
|
|
269
|
-
* @param {
|
|
270
|
-
*
|
|
271
|
-
* @param {number} input.maxTickets - The reviewability budget
|
|
272
|
-
* (`getLimits(config).maxTickets`).
|
|
273
|
-
* @param {string[]} [input.riskHeuristics] - `planning.riskHeuristics`
|
|
274
|
-
* phrases from the resolved config.
|
|
275
|
-
* @param {string} [input.planText] - Concatenated plan text the heuristics and
|
|
276
|
-
* the external-dependency probe match against (tech spec + serialized
|
|
277
|
-
* tickets).
|
|
195
|
+
* @param {string} [input.planText] - Concatenated plan text the probe
|
|
196
|
+
* matches against (tech spec + serialized tickets).
|
|
278
197
|
* @param {string[]} [input.knownPackages] - Package specifiers the repo's own
|
|
279
198
|
* manifests declare (own name + dependency maps + workspace package names),
|
|
280
199
|
* passed to the external-dependency probe.
|
|
@@ -284,58 +203,28 @@ export function evaluateExternalDependencyProbe({
|
|
|
284
203
|
* @returns {CriticDispatchDecision}
|
|
285
204
|
*/
|
|
286
205
|
export function evaluatePremortemDispatch({
|
|
287
|
-
ticketCount,
|
|
288
|
-
maxTickets,
|
|
289
|
-
riskHeuristics = [],
|
|
290
206
|
planText = '',
|
|
291
207
|
knownPackages = [],
|
|
292
208
|
ownerRepo = null,
|
|
293
209
|
}) {
|
|
294
|
-
if (!Number.isInteger(maxTickets) || maxTickets <= 0) {
|
|
295
|
-
throw new TypeError(
|
|
296
|
-
'evaluatePremortemDispatch: maxTickets must be a positive integer',
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
const reasons = [];
|
|
300
|
-
|
|
301
|
-
const count = Number.isInteger(ticketCount) ? ticketCount : 0;
|
|
302
|
-
if (count * 2 >= maxTickets) {
|
|
303
|
-
reasons.push(
|
|
304
|
-
`Ticket count ${count} is at least half the reviewability budget (maxTickets ${maxTickets}).`,
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const haystack = String(planText).toLowerCase();
|
|
309
|
-
const matched = riskHeuristics.filter(
|
|
310
|
-
(phrase) =>
|
|
311
|
-
typeof phrase === 'string' &&
|
|
312
|
-
phrase.trim().length > 0 &&
|
|
313
|
-
haystack.includes(phrase.trim().toLowerCase()),
|
|
314
|
-
);
|
|
315
|
-
if (matched.length > 0) {
|
|
316
|
-
reasons.push(
|
|
317
|
-
`planning.riskHeuristics match(es) in the plan text: ${matched.map((p) => `"${p.trim()}"`).join(', ')}.`,
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
210
|
const externalDeps = evaluateExternalDependencyProbe({
|
|
322
211
|
planText,
|
|
323
212
|
knownPackages,
|
|
324
213
|
ownerRepo,
|
|
325
214
|
});
|
|
326
215
|
if (externalDeps.matched) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
216
|
+
return {
|
|
217
|
+
critic: 'pre-mortem',
|
|
218
|
+
dispatch: true,
|
|
219
|
+
reasons: externalDeps.reasons,
|
|
220
|
+
};
|
|
332
221
|
}
|
|
333
222
|
|
|
334
223
|
return {
|
|
335
224
|
critic: 'pre-mortem',
|
|
336
225
|
dispatch: false,
|
|
337
226
|
reasons: [
|
|
338
|
-
|
|
227
|
+
'The external-dependency probe found no out-of-repo markers in the plan text.',
|
|
339
228
|
],
|
|
340
229
|
};
|
|
341
230
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* plan-critics-evaluate.js — shared critic-dispatch evaluation for the
|
|
3
|
-
* collapsed /mandrel-plan flow (#4496 fix 6).
|
|
3
|
+
* collapsed /mandrel-plan flow (#4496 fix 6; pre-mortem only since Story #5312).
|
|
4
4
|
*
|
|
5
|
-
* One consumer: the `plan-critics.js` CLI, which
|
|
6
|
-
* Author and Persist
|
|
7
|
-
* module, prints the verdict as JSON, and records
|
|
8
|
-
* plan-metrics ledger; the workflow dispatches a fresh-context
|
|
9
|
-
* sub-agent on a `dispatch: true` verdict and folds the findings into
|
|
10
|
-
* re-author round before persist.
|
|
5
|
+
* One consumer: the `plan-critics.js` CLI, which the operator runs between
|
|
6
|
+
* Author and Persist when they want the pre-mortem. The CLI loads the draft
|
|
7
|
+
* artifacts, calls this module, prints the verdict as JSON, and records a
|
|
8
|
+
* skip on the plan-metrics ledger; the workflow dispatches a fresh-context
|
|
9
|
+
* critic sub-agent on a `dispatch: true` verdict and folds the findings into
|
|
10
|
+
* a re-author round before persist.
|
|
11
11
|
*
|
|
12
12
|
* Story #4592 moved that evaluation here from `run-plan-persist.js`, which
|
|
13
13
|
* ran it after authoring was finished and immediately before
|
|
@@ -21,26 +21,7 @@
|
|
|
21
21
|
* @module lib/orchestration/plan-critics-evaluate
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
evaluateConsolidationDispatch,
|
|
27
|
-
evaluatePremortemDispatch,
|
|
28
|
-
} from './plan-critic-conditions.js';
|
|
29
|
-
import { evaluateTextHygiene } from './plan-text-hygiene.js';
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Resolve the planning risk heuristics list from the canonical config
|
|
33
|
-
* block (same resolution `plan-context.js` and the decompose context use).
|
|
34
|
-
*
|
|
35
|
-
* @param {object} config
|
|
36
|
-
* @returns {string[]}
|
|
37
|
-
*/
|
|
38
|
-
function resolveRiskHeuristics(config = {}) {
|
|
39
|
-
if (Array.isArray(config.planning?.riskHeuristics)) {
|
|
40
|
-
return config.planning.riskHeuristics;
|
|
41
|
-
}
|
|
42
|
-
return [];
|
|
43
|
-
}
|
|
24
|
+
import { evaluatePremortemDispatch } from './plan-critic-conditions.js';
|
|
44
25
|
|
|
45
26
|
/**
|
|
46
27
|
* Resolve the `{ owner, repo }` the external-dependency probe's cross-repo arm
|
|
@@ -57,24 +38,11 @@ function resolveOwnerRepo(config = {}) {
|
|
|
57
38
|
}
|
|
58
39
|
|
|
59
40
|
/**
|
|
60
|
-
* Evaluate the
|
|
61
|
-
* the
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* deterministic precondition + size/divergence conditions.
|
|
66
|
-
* - Pre-mortem: ticket count at least half `maxTickets`, OR any
|
|
67
|
-
* `planning.riskHeuristics` phrase matching the plan text, OR the
|
|
68
|
-
* external-dependency probe (Story #4700) matching an out-of-repo marker
|
|
69
|
-
* — a scoped package absent from `knownPackages`, a cross-repo
|
|
70
|
-
* `github.com/<owner>/<repo>` reference, or a named external service
|
|
71
|
-
* prerequisite. Story #4542 retired its authored-risk-level condition with
|
|
72
|
-
* the verdict itself.
|
|
73
|
-
* - Text hygiene (Story #4599, advisory-only): deterministic body lints
|
|
74
|
-
* (dangling-citation / open-question / slicing-mass) over the draft
|
|
75
|
-
* stories. It has no `dispatch` semantics and spawns nothing — its
|
|
76
|
-
* `findings[]` are re-author-round input, and the consolidation /
|
|
77
|
-
* premortem dispatch verdicts are untouched by it.
|
|
41
|
+
* Evaluate the pre-mortem critic's dispatch condition over the authored
|
|
42
|
+
* planning artifacts: the external-dependency probe (Story #4700) matching an
|
|
43
|
+
* out-of-repo marker — a scoped package absent from `knownPackages`, a
|
|
44
|
+
* cross-repo `github.com/<owner>/<repo>` reference, or a named external
|
|
45
|
+
* service prerequisite.
|
|
78
46
|
*
|
|
79
47
|
* @param {{
|
|
80
48
|
* techSpecContent: string,
|
|
@@ -88,9 +56,7 @@ function resolveOwnerRepo(config = {}) {
|
|
|
88
56
|
* that gathers them; this module stays pure. Empty when unresolved, which
|
|
89
57
|
* only widens what the probe treats as external.
|
|
90
58
|
* @returns {{
|
|
91
|
-
* consolidation: { critic: string, dispatch: boolean, reasons: string[] },
|
|
92
59
|
* premortem: { critic: string, dispatch: boolean, reasons: string[] },
|
|
93
|
-
* textHygiene: { critic: string, findings: Array<object> },
|
|
94
60
|
* }}
|
|
95
61
|
*/
|
|
96
62
|
export function evaluatePlanCritics({
|
|
@@ -100,24 +66,7 @@ export function evaluatePlanCritics({
|
|
|
100
66
|
knownPackages = [],
|
|
101
67
|
}) {
|
|
102
68
|
const ticketList = Array.isArray(tickets) ? tickets : null;
|
|
103
|
-
const consolidation =
|
|
104
|
-
ticketList === null
|
|
105
|
-
? {
|
|
106
|
-
critic: 'consolidation',
|
|
107
|
-
dispatch: false,
|
|
108
|
-
reasons: [
|
|
109
|
-
'single-delivery shape — no draft tickets exist to consolidate.',
|
|
110
|
-
],
|
|
111
|
-
}
|
|
112
|
-
: evaluateConsolidationDispatch({
|
|
113
|
-
draftStories: ticketList,
|
|
114
|
-
specText: techSpecContent,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
69
|
const premortem = evaluatePremortemDispatch({
|
|
118
|
-
ticketCount: ticketList?.length ?? 0,
|
|
119
|
-
maxTickets: getLimits(config).maxTickets,
|
|
120
|
-
riskHeuristics: resolveRiskHeuristics(config),
|
|
121
70
|
planText: [
|
|
122
71
|
techSpecContent ?? '',
|
|
123
72
|
ticketList ? JSON.stringify(ticketList) : '',
|
|
@@ -126,10 +75,5 @@ export function evaluatePlanCritics({
|
|
|
126
75
|
ownerRepo: resolveOwnerRepo(config),
|
|
127
76
|
});
|
|
128
77
|
|
|
129
|
-
|
|
130
|
-
critic: 'text-hygiene',
|
|
131
|
-
findings: evaluateTextHygiene({ draftStories: ticketList }).findings,
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
return { consolidation, premortem, textHygiene };
|
|
78
|
+
return { premortem };
|
|
135
79
|
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* changes-repair.js — repair-before-judging for `changes[]` entries
|
|
3
|
+
* (Story #5312).
|
|
4
|
+
*
|
|
5
|
+
* The `{ path, assumption }` object shape is a deterministic, mechanically
|
|
6
|
+
* derivable formality. The validator already knew how to salvage a path from
|
|
7
|
+
* a plain-string bullet (`suggestPathEntryFix`) — then rejected the plan
|
|
8
|
+
* anyway and charged the author a full re-drafting round to paste that exact
|
|
9
|
+
* object back. Story #5005 made the same call for the verify-tier suffix and
|
|
10
|
+
* repaired it instead; the suffix is gone now, and the repair moves to the
|
|
11
|
+
* one formality left: this module applies the inference the validator
|
|
12
|
+
* trusts, so the dry-run rewrites and **reports** each repair rather than
|
|
13
|
+
* refusing on it.
|
|
14
|
+
*
|
|
15
|
+
* Two shapes are repaired, on both authoring surfaces (a structured object
|
|
16
|
+
* body's `changes[]` array and a serialized string body's `## Changes`
|
|
17
|
+
* section):
|
|
18
|
+
*
|
|
19
|
+
* - a **plain-string bullet** (`src/app.js`, `` `src/app.js` ``,
|
|
20
|
+
* `src/app.js — adds the route`) becomes `{ path, assumption }`, the
|
|
21
|
+
* assumption resolved by probing the base branch — a path present at
|
|
22
|
+
* base is a `refactors-existing`, an absent one a `creates`;
|
|
23
|
+
* - a **trailing parenthetical** on a path (`src/app.js (new)`,
|
|
24
|
+
* `` `src/app.js (creates)` — refactors-existing ``) is stripped; an
|
|
25
|
+
* authored assumption is kept, an absent one probed as above.
|
|
26
|
+
*
|
|
27
|
+
* A string nothing path-shaped can be salvaged from is left untouched and
|
|
28
|
+
* still fails the body-shape validator — that is the one `changes[]` failure
|
|
29
|
+
* only the author can resolve.
|
|
30
|
+
*
|
|
31
|
+
* It lives beside the validator rather than inside it because the validator's
|
|
32
|
+
* job is to *judge*: mixing a mutating repair pass into a module of pure
|
|
33
|
+
* collectors muddies both. `persist-helpers.js#validateTickets` calls this
|
|
34
|
+
* first, then the validators.
|
|
35
|
+
*
|
|
36
|
+
* @module lib/orchestration/plan-persist/changes-repair
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { FILE_ASSUMPTION_VALUES } from '../file-assumption-enum.js';
|
|
40
|
+
|
|
41
|
+
/** The `## Changes` heading (either level the parser accepts). */
|
|
42
|
+
const CHANGES_HEADING_RE = /^#{2,3}\s+Changes\s*$/i;
|
|
43
|
+
|
|
44
|
+
/** Any heading — the end of the `## Changes` section. */
|
|
45
|
+
const ANY_HEADING_RE = /^#{1,6}\s+\S/;
|
|
46
|
+
|
|
47
|
+
/** A trailing `(…)` on a path token. */
|
|
48
|
+
const TRAILING_PARENTHETICAL_RE = /\s*\([^)]*\)\s*$/;
|
|
49
|
+
|
|
50
|
+
/** The humanized canonical bullet: `` `path` — assumption ``. */
|
|
51
|
+
const HUMANIZED_RE = /^`([^`]+)`\s+—\s+(\S+)$/;
|
|
52
|
+
|
|
53
|
+
// A token that looks like a file path / glob / module id: it carries a `/` or a
|
|
54
|
+
// `.`-separated segment. Deliberately loose — a false positive only produces a
|
|
55
|
+
// `{ path, assumption }` entry the base-branch probes then judge.
|
|
56
|
+
const PATH_LIKE_RE = /^[\w@*-]*[/.][\w@./*-]+$/;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Strip a trailing parenthetical from a path token, reporting whether one
|
|
60
|
+
* was present.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} raw
|
|
63
|
+
* @returns {{ path: string, stripped: boolean }}
|
|
64
|
+
*/
|
|
65
|
+
function stripParenthetical(raw) {
|
|
66
|
+
const path = raw.replace(TRAILING_PARENTHETICAL_RE, '').trim();
|
|
67
|
+
return { path, stripped: path !== raw.trim() };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Salvage the path token from a plain-string bullet: drop a leading list
|
|
72
|
+
* marker, take the segment before any humanized ` — ` tail, peel quotes and
|
|
73
|
+
* backticks, strip a trailing parenthetical. Returns `null` when nothing
|
|
74
|
+
* path-shaped survives.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} raw
|
|
77
|
+
* @returns {string|null}
|
|
78
|
+
*/
|
|
79
|
+
function salvagePath(raw) {
|
|
80
|
+
let s = raw
|
|
81
|
+
.trim()
|
|
82
|
+
.replace(/^[-*]\s+/, '')
|
|
83
|
+
.trim();
|
|
84
|
+
s = s.split('—')[0].trim();
|
|
85
|
+
s = s
|
|
86
|
+
.replace(/^[`'"]+/, '')
|
|
87
|
+
.replace(/[`'"]+$/, '')
|
|
88
|
+
.trim();
|
|
89
|
+
s = stripParenthetical(s).path;
|
|
90
|
+
return s !== '' && PATH_LIKE_RE.test(s) ? s : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolve the assumption for a path with none authored: present at base →
|
|
95
|
+
* `refactors-existing`, absent → `creates`.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} path
|
|
98
|
+
* @param {(path: string) => boolean} existsAtBase
|
|
99
|
+
* @returns {'refactors-existing'|'creates'}
|
|
100
|
+
*/
|
|
101
|
+
function probeAssumption(path, existsAtBase) {
|
|
102
|
+
return existsAtBase(path) ? 'refactors-existing' : 'creates';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Repair one structured `changes[]` item. Returns the corrected entry and a
|
|
107
|
+
* repair record, or `null` when the item needs no repair (or cannot be
|
|
108
|
+
* repaired).
|
|
109
|
+
*
|
|
110
|
+
* @param {unknown} item
|
|
111
|
+
* @param {(path: string) => boolean} existsAtBase
|
|
112
|
+
* @returns {{ entry: { path: string, assumption: string }, repair: object }|null}
|
|
113
|
+
*/
|
|
114
|
+
function repairStructuredItem(item, existsAtBase) {
|
|
115
|
+
if (typeof item === 'string') {
|
|
116
|
+
const path = salvagePath(item);
|
|
117
|
+
if (path === null) return null;
|
|
118
|
+
const assumption = probeAssumption(path, existsAtBase);
|
|
119
|
+
return {
|
|
120
|
+
entry: { path, assumption },
|
|
121
|
+
repair: { from: item, path, assumption, reason: 'plain-string' },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (item === null || typeof item !== 'object') return null;
|
|
125
|
+
if (typeof item.path !== 'string') return null;
|
|
126
|
+
const { path, stripped } = stripParenthetical(item.path);
|
|
127
|
+
const authored = FILE_ASSUMPTION_VALUES.includes(item.assumption);
|
|
128
|
+
if (!stripped && authored) return null;
|
|
129
|
+
if (path === '') return null;
|
|
130
|
+
const assumption = authored
|
|
131
|
+
? item.assumption
|
|
132
|
+
: probeAssumption(path, existsAtBase);
|
|
133
|
+
return {
|
|
134
|
+
entry: { path, assumption },
|
|
135
|
+
repair: {
|
|
136
|
+
from: item.path,
|
|
137
|
+
path,
|
|
138
|
+
assumption,
|
|
139
|
+
reason: stripped ? 'trailing-parenthetical' : 'missing-assumption',
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Repair one line of a serialized `## Changes` section. Returns the rewritten
|
|
146
|
+
* line and a repair record, or `null` when the line is already canonical or
|
|
147
|
+
* cannot be repaired.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} line
|
|
150
|
+
* @param {(path: string) => boolean} existsAtBase
|
|
151
|
+
* @returns {{ line: string, repair: object }|null}
|
|
152
|
+
*/
|
|
153
|
+
function repairSectionLine(line, existsAtBase) {
|
|
154
|
+
const marker = line.match(/^(\s*[-*]\s+)/);
|
|
155
|
+
if (!marker) return null;
|
|
156
|
+
const content = line.slice(marker[1].length).trim();
|
|
157
|
+
if (content === '') return null;
|
|
158
|
+
const humanized = content.match(HUMANIZED_RE);
|
|
159
|
+
if (humanized) {
|
|
160
|
+
const { path, stripped } = stripParenthetical(humanized[1]);
|
|
161
|
+
const authored = FILE_ASSUMPTION_VALUES.includes(humanized[2]);
|
|
162
|
+
if (!stripped && authored) return null;
|
|
163
|
+
if (path === '') return null;
|
|
164
|
+
const assumption = authored
|
|
165
|
+
? humanized[2]
|
|
166
|
+
: probeAssumption(path, existsAtBase);
|
|
167
|
+
return {
|
|
168
|
+
line: `${marker[1]}\`${path}\` — ${assumption}`,
|
|
169
|
+
repair: {
|
|
170
|
+
from: content,
|
|
171
|
+
path,
|
|
172
|
+
assumption,
|
|
173
|
+
reason: stripped ? 'trailing-parenthetical' : 'missing-assumption',
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (content.startsWith('{')) {
|
|
178
|
+
let parsed;
|
|
179
|
+
try {
|
|
180
|
+
parsed = JSON.parse(content);
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const repaired = repairStructuredItem(parsed, existsAtBase);
|
|
185
|
+
if (repaired === null) return null;
|
|
186
|
+
return {
|
|
187
|
+
line: `${marker[1]}\`${repaired.entry.path}\` — ${repaired.entry.assumption}`,
|
|
188
|
+
repair: repaired.repair,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const path = salvagePath(content);
|
|
192
|
+
if (path === null) return null;
|
|
193
|
+
const assumption = probeAssumption(path, existsAtBase);
|
|
194
|
+
return {
|
|
195
|
+
line: `${marker[1]}\`${path}\` — ${assumption}`,
|
|
196
|
+
repair: { from: content, path, assumption, reason: 'plain-string' },
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Repair the `## Changes` section of a serialized body in place. Only lines
|
|
202
|
+
* between the heading and the next heading are touched; the rest of the
|
|
203
|
+
* body is byte-identical.
|
|
204
|
+
*
|
|
205
|
+
* @param {string} body
|
|
206
|
+
* @param {(path: string) => boolean} existsAtBase
|
|
207
|
+
* @returns {{ body: string, repairs: object[] }}
|
|
208
|
+
*/
|
|
209
|
+
function repairSerializedBody(body, existsAtBase) {
|
|
210
|
+
const lines = body.split('\n');
|
|
211
|
+
const repairs = [];
|
|
212
|
+
let inChanges = false;
|
|
213
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
214
|
+
const line = lines[i];
|
|
215
|
+
if (CHANGES_HEADING_RE.test(line.trim())) {
|
|
216
|
+
inChanges = true;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (!inChanges) continue;
|
|
220
|
+
if (ANY_HEADING_RE.test(line) || line.trim().startsWith('---')) {
|
|
221
|
+
inChanges = false;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const repaired = repairSectionLine(line, existsAtBase);
|
|
225
|
+
if (repaired === null) continue;
|
|
226
|
+
lines[i] = repaired.line;
|
|
227
|
+
repairs.push(repaired.repair);
|
|
228
|
+
}
|
|
229
|
+
return { body: lines.join('\n'), repairs };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Repair the `changes[]` of one ticket, on whichever surface carries it.
|
|
234
|
+
*
|
|
235
|
+
* @param {object} ticket Mutated in place.
|
|
236
|
+
* @param {(path: string) => boolean} existsAtBase
|
|
237
|
+
* @returns {object[]} The repairs applied to this ticket.
|
|
238
|
+
*/
|
|
239
|
+
function repairTicket(ticket, existsAtBase) {
|
|
240
|
+
const body = ticket.body;
|
|
241
|
+
if (typeof body === 'string') {
|
|
242
|
+
const { body: next, repairs } = repairSerializedBody(body, existsAtBase);
|
|
243
|
+
if (repairs.length > 0) ticket.body = next;
|
|
244
|
+
return repairs;
|
|
245
|
+
}
|
|
246
|
+
const changes =
|
|
247
|
+
body && typeof body === 'object' && Array.isArray(body.changes)
|
|
248
|
+
? body.changes
|
|
249
|
+
: Array.isArray(ticket.changes)
|
|
250
|
+
? ticket.changes
|
|
251
|
+
: null;
|
|
252
|
+
if (changes === null) return [];
|
|
253
|
+
const repairs = [];
|
|
254
|
+
for (let i = 0; i < changes.length; i += 1) {
|
|
255
|
+
const repaired = repairStructuredItem(changes[i], existsAtBase);
|
|
256
|
+
if (repaired === null) continue;
|
|
257
|
+
changes[i] = repaired.entry;
|
|
258
|
+
repairs.push(repaired.repair);
|
|
259
|
+
}
|
|
260
|
+
return repairs;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Render one repair as the dry-run line the operator reads.
|
|
265
|
+
*
|
|
266
|
+
* @param {{ slug: string, from: string, path: string, assumption: string, reason: string }} repair
|
|
267
|
+
* @returns {string}
|
|
268
|
+
*/
|
|
269
|
+
export function renderChangeRepair({ slug, from, path, assumption, reason }) {
|
|
270
|
+
const why =
|
|
271
|
+
reason === 'plain-string'
|
|
272
|
+
? 'plain-string bullet'
|
|
273
|
+
: reason === 'trailing-parenthetical'
|
|
274
|
+
? 'trailing parenthetical'
|
|
275
|
+
: 'missing assumption';
|
|
276
|
+
return `Story "${slug}": changes[] entry "${from}" (${why}) repaired to {"path":"${path}","assumption":"${assumption}"} by probing base.`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Rewrite every repairable `changes[]` entry across the draft, probing the
|
|
281
|
+
* base branch for the assumption where none was authored. Mutates `tickets`
|
|
282
|
+
* in place (the persist pipeline threads this same array on to assembly)
|
|
283
|
+
* and returns the repairs, each tagged with the Story's slug. Total — a
|
|
284
|
+
* non-array argument and non-Story tickets are no-ops.
|
|
285
|
+
*
|
|
286
|
+
* @param {object[]} tickets
|
|
287
|
+
* @param {{ existsAtBase: (path: string) => boolean }} args
|
|
288
|
+
* @returns {Array<{ slug: string, from: string, path: string, assumption: string, reason: string }>}
|
|
289
|
+
*/
|
|
290
|
+
export function repairChangeEntries(tickets, { existsAtBase }) {
|
|
291
|
+
const repairs = [];
|
|
292
|
+
for (const ticket of Array.isArray(tickets) ? tickets : []) {
|
|
293
|
+
if (!ticket || ticket.type !== 'story') continue;
|
|
294
|
+
const slug = ticket.slug ?? ticket.title ?? '<unknown>';
|
|
295
|
+
for (const repair of repairTicket(ticket, existsAtBase)) {
|
|
296
|
+
repairs.push({ slug, ...repair });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return repairs;
|
|
300
|
+
}
|