mandrel 2.55.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 +4 -30
- package/.agents/docs/configuration.md +11 -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/rules/ci-remediation.md +39 -21
- package/.agents/schemas/agentrc.schema.json +28 -185
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +107 -17
- package/.agents/scripts/audit-to-stories.js +222 -75
- 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/file-ci-gap.js +306 -0
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +25 -1
- package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +40 -52
- package/.agents/scripts/lib/audit-to-stories/finding-adapter.js +5 -1
- package/.agents/scripts/lib/audit-to-stories/issue-corpus.js +162 -0
- package/.agents/scripts/lib/audit-to-stories/issues-file.js +121 -0
- package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/ledger-record.js +126 -0
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +11 -0
- 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 +49 -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/feedback-loop/graduator-core.js +53 -13
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +71 -25
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +18 -25
- package/.agents/scripts/lib/{audit-to-stories/ledger.js → findings/audit-ledger.js} +131 -24
- package/.agents/scripts/lib/findings/route-finding.js +38 -0
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/github/framework-repo.js +148 -2
- package/.agents/scripts/lib/label-constants.js +6 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
- package/.agents/scripts/lib/observability/source-classifier.js +2 -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/ci-gap-intake.js +605 -0
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +13 -8
- 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/audit-provenance.js +197 -0
- 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 +133 -299
- 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/run-epilogue.js +4 -4
- 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/story-follow-ups.js +32 -20
- 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/pr-watch-with-update.js +3 -2
- 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/audit-to-stories.md +63 -27
- 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 +56 -62
- package/.agents/workflows/helpers/deliver-story.md +9 -13
- package/.agents/workflows/helpers/plan-reference.md +132 -196
- package/.agents/workflows/mandrel-plan.md +28 -41
- package/.agents/workflows/memory-consolidate.md +9 -13
- package/docs/CHANGELOG.md +33 -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,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/orchestration/plan-persist/audit-provenance.js — what an audit-seeded
|
|
3
|
+
* plan leaves behind for the next sweep.
|
|
4
|
+
*
|
|
5
|
+
* The chained `/mandrel-plan` path files Stories from an audit seed, and until
|
|
6
|
+
* Story #5307 those Stories were invisible to the sweep that proposed them:
|
|
7
|
+
* they carried no `audit::*` label, so they were absent from the label-listed
|
|
8
|
+
* corpus dedup indexes, and nothing recorded them in the cross-run ledger. The
|
|
9
|
+
* provenance footers `carryProvenanceFooters` stamps could not cover for
|
|
10
|
+
* either — with an index in play the exact lookup is answered from that pool
|
|
11
|
+
* and never reaches the provider.
|
|
12
|
+
*
|
|
13
|
+
* Both halves ride the create loop in `story-ops.js` because that is the only
|
|
14
|
+
* seam on this path that cannot be forgotten: there is no second required pass
|
|
15
|
+
* here the way `--wire-edges` is one for the standalone filer.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
DEFAULT_LEDGER_PATH,
|
|
20
|
+
readLedger,
|
|
21
|
+
recordFiledIdentities,
|
|
22
|
+
writeLedger,
|
|
23
|
+
} from '../../findings/audit-ledger.js';
|
|
24
|
+
import {
|
|
25
|
+
parseAuditLabelFooter,
|
|
26
|
+
parseFingerprintFooter,
|
|
27
|
+
parseSemanticKeyFooter,
|
|
28
|
+
} from '../../findings/route-finding.js';
|
|
29
|
+
import { Logger } from '../../Logger.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A fresh accumulator for one persist run's audit filings.
|
|
33
|
+
*
|
|
34
|
+
* The run collects into this and flushes once, so a plan that files N Stories
|
|
35
|
+
* leaves one reviewable ledger diff rather than N.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} [ledgerPath]
|
|
38
|
+
* @returns {{ path: string, ledger: object|null, recorded: number, ambiguous: number }}
|
|
39
|
+
*/
|
|
40
|
+
function newAuditLedgerRecord(ledgerPath) {
|
|
41
|
+
return {
|
|
42
|
+
path: ledgerPath ?? DEFAULT_LEDGER_PATH,
|
|
43
|
+
ledger: null,
|
|
44
|
+
recorded: 0,
|
|
45
|
+
ambiguous: 0,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Merge the seed's `audit::*` labels into an audit-seeded Story's labels.
|
|
51
|
+
*
|
|
52
|
+
* Taken from the **seed union**, not from the attributed provenance source: a
|
|
53
|
+
* label only scopes which issues the next sweep's corpus fetch returns, and
|
|
54
|
+
* matching inside that corpus is by fingerprint and semantic key. A superset
|
|
55
|
+
* corpus can therefore only ever find more, never less — so erring wide is the
|
|
56
|
+
* safe direction, and it keeps lens-to-label knowledge in the audit filer that
|
|
57
|
+
* owns it rather than re-deriving a dimension here (the junk-derivation trap
|
|
58
|
+
* Story #4195 closed).
|
|
59
|
+
*
|
|
60
|
+
* A non-audit seed carries no such footer, so this is a no-op there.
|
|
61
|
+
*
|
|
62
|
+
* @param {Array<object>} stories — the assembled Stories.
|
|
63
|
+
* @param {string} [provenanceSource] — the seed this plan was authored from.
|
|
64
|
+
* @returns {Array<object>}
|
|
65
|
+
*/
|
|
66
|
+
export function withAuditLabels(stories, provenanceSource) {
|
|
67
|
+
const fromSeed = parseAuditLabelFooter(provenanceSource ?? '');
|
|
68
|
+
if (fromSeed.length === 0) return stories;
|
|
69
|
+
return stories.map((story) => ({
|
|
70
|
+
...story,
|
|
71
|
+
labels: [...new Set([...story.labels, ...fromSeed])],
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Record one just-created Story in the cross-run audit ledger.
|
|
77
|
+
*
|
|
78
|
+
* The identities are read back off the body this run assembled, so what is
|
|
79
|
+
* recorded is exactly what was stamped — attribution already applied. A Story
|
|
80
|
+
* from a non-audit plan carries no footers and records nothing, which is why
|
|
81
|
+
* a `--seed` or `--tickets` run never touches the ledger file at all.
|
|
82
|
+
*
|
|
83
|
+
* **The union fallback is deliberately NOT recorded.** When the seed carried
|
|
84
|
+
* footers but the plan attributed none per-Story, every sibling carries every
|
|
85
|
+
* fingerprint; binding a finding to one of them would be a coin flip, and a
|
|
86
|
+
* wrong binding is worse than none — the finding would be suppressed against an
|
|
87
|
+
* Issue that never tracked it, or resurrected when an unrelated Story closed.
|
|
88
|
+
* The audit path stamps `provenance` mechanically, so this is the exception.
|
|
89
|
+
*
|
|
90
|
+
* @param {{ story: object, id: number, ledgerRecord: object, attributed: boolean }} args
|
|
91
|
+
*/
|
|
92
|
+
function recordAuditFiling({ story, id, ledgerRecord, attributed }) {
|
|
93
|
+
const fingerprints = parseFingerprintFooter(story.body);
|
|
94
|
+
if (fingerprints.length === 0) return;
|
|
95
|
+
if (!attributed) {
|
|
96
|
+
ledgerRecord.ambiguous += fingerprints.length;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const semanticKeys = parseSemanticKeyFooter(story.body);
|
|
100
|
+
const identities = fingerprints.map((fingerprint, i) => ({
|
|
101
|
+
fingerprint,
|
|
102
|
+
semanticKey: semanticKeys[i] ?? '',
|
|
103
|
+
title: story.title,
|
|
104
|
+
}));
|
|
105
|
+
const { ledger, recorded } = recordFiledIdentities({
|
|
106
|
+
ledger: ledgerRecord.ledger ?? readLedger(ledgerRecord.path),
|
|
107
|
+
identities,
|
|
108
|
+
issue: { number: id },
|
|
109
|
+
});
|
|
110
|
+
ledgerRecord.ledger = ledger;
|
|
111
|
+
ledgerRecord.recorded += recorded;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Persist the run's audit filings, once, after every Story exists.
|
|
116
|
+
*
|
|
117
|
+
* Writing per-Story would rewrite a committed baseline N times for one plan;
|
|
118
|
+
* writing once keeps the diff to a single reviewable change. A run that
|
|
119
|
+
* recorded nothing writes nothing.
|
|
120
|
+
*
|
|
121
|
+
* @param {object} ledgerRecord
|
|
122
|
+
* @param {{ warn: Function }} logger
|
|
123
|
+
*/
|
|
124
|
+
function flushAuditLedger(ledgerRecord, logger) {
|
|
125
|
+
if (ledgerRecord.ambiguous > 0) {
|
|
126
|
+
logger.warn(
|
|
127
|
+
`[plan-persist] audit ledger: ${ledgerRecord.ambiguous} identity(ies) not recorded — ` +
|
|
128
|
+
'the seed carried provenance footers but no Story attributed them, so ownership is ' +
|
|
129
|
+
'ambiguous. Author per-Story `provenance` to record them.',
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (!ledgerRecord.ledger || ledgerRecord.recorded === 0) return;
|
|
133
|
+
writeLedger(ledgerRecord.path, ledgerRecord.ledger);
|
|
134
|
+
logger.warn(
|
|
135
|
+
`[plan-persist] audit ledger: recorded ${ledgerRecord.recorded} filed finding(s) ` +
|
|
136
|
+
`to ${ledgerRecord.path}.`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Record everything one persist run filed, once, after every Story exists.
|
|
142
|
+
*
|
|
143
|
+
* Driven from the persist orchestrator rather than from inside the create loop:
|
|
144
|
+
* the loop's job is to create issues, and this is the only other place that
|
|
145
|
+
* always runs after it. Either seam is unforgettable — there is no second
|
|
146
|
+
* required pass on this path the way `--wire-edges` is one for the standalone
|
|
147
|
+
* filer — and keeping the side effect at the orchestration layer leaves the
|
|
148
|
+
* create loop doing one thing.
|
|
149
|
+
*
|
|
150
|
+
* One write per plan, not per Story: the ledger is committed state, and N
|
|
151
|
+
* rewrites of it for one plan is a diff nobody can review. A run that recorded
|
|
152
|
+
* nothing — a non-audit plan, or one whose identities were all union-carried —
|
|
153
|
+
* writes nothing at all.
|
|
154
|
+
*
|
|
155
|
+
* @param {object} params
|
|
156
|
+
* @param {Array<object>} params.stories — the assembled Stories.
|
|
157
|
+
* @param {Array<{ slug: string, id: number }>} params.created — persist receipts.
|
|
158
|
+
* @param {Array<object>} params.tickets — the raw authored tickets.
|
|
159
|
+
* @param {string} [params.ledgerPath]
|
|
160
|
+
* @param {{ warn: Function }} [params.logger]
|
|
161
|
+
* @param {boolean} [params.dryRun] — a dry run records nothing at all.
|
|
162
|
+
* @returns {{ recorded: number, ambiguous: number }}
|
|
163
|
+
*/
|
|
164
|
+
export function recordAuditFilings({
|
|
165
|
+
stories,
|
|
166
|
+
created,
|
|
167
|
+
tickets,
|
|
168
|
+
ledgerPath,
|
|
169
|
+
logger,
|
|
170
|
+
dryRun = false,
|
|
171
|
+
}) {
|
|
172
|
+
// A dry run created no issue to record against, and must not touch committed
|
|
173
|
+
// state. Owned here rather than at the call site so the contract is testable.
|
|
174
|
+
if (dryRun) return { recorded: 0, ambiguous: 0 };
|
|
175
|
+
const record = newAuditLedgerRecord(ledgerPath);
|
|
176
|
+
const idBySlug = new Map((created ?? []).map((c) => [c.slug, c.id]));
|
|
177
|
+
// Attribution is a property of what the plan AUTHORED, so it is read off the
|
|
178
|
+
// raw tickets rather than threaded through assembly: a Story that declared no
|
|
179
|
+
// `provenance` inherited the seed union, where ownership is a coin flip.
|
|
180
|
+
const attributedSlugs = new Set(
|
|
181
|
+
(tickets ?? [])
|
|
182
|
+
.filter((t) => t?.provenance !== undefined && t?.provenance !== null)
|
|
183
|
+
.map((t) => t?.slug),
|
|
184
|
+
);
|
|
185
|
+
for (const story of stories ?? []) {
|
|
186
|
+
const id = idBySlug.get(story.slug);
|
|
187
|
+
if (typeof id !== 'number') continue;
|
|
188
|
+
recordAuditFiling({
|
|
189
|
+
story,
|
|
190
|
+
id,
|
|
191
|
+
ledgerRecord: record,
|
|
192
|
+
attributed: attributedSlugs.has(story.slug),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
flushAuditLedger(record, logger ?? Logger);
|
|
196
|
+
return { recorded: record.recorded, ambiguous: record.ambiguous };
|
|
197
|
+
}
|