mandrel 2.25.0 → 2.26.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/acceptance-critic.md +10 -6
- package/.agents/audit-checklists/baselines.md +21 -0
- package/.agents/docs/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +3 -1
- package/.agents/instructions.md +1 -1
- package/.agents/schemas/audit-rules.json +15 -0
- package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +4 -0
- package/.agents/schemas/baselines/crap.schema.json +8 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +89 -6
- package/.agents/scripts/audit-baselines.js +136 -0
- package/.agents/scripts/check-arch-cycles.js +12 -93
- package/.agents/scripts/check-baseline-drift.js +16 -3
- package/.agents/scripts/check-baselines.js +19 -3
- package/.agents/scripts/check-cyclomatic.js +214 -0
- package/.agents/scripts/check-schema-references.js +392 -0
- package/.agents/scripts/check-test-temp-hygiene.js +38 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -0
- package/.agents/scripts/diagnose-friction.js +85 -19
- package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
- package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
- package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
- package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
- package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
- package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
- package/.agents/scripts/lib/audit-baselines/read.js +87 -0
- package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
- package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
- package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
- package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
- package/.agents/scripts/lib/audit-suite/index.js +0 -5
- package/.agents/scripts/lib/audit-suite/selector.js +9 -62
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
- package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
- package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
- package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
- package/.agents/scripts/lib/baselines/git-base.js +26 -4
- package/.agents/scripts/lib/baselines/kinds/crap.js +112 -15
- package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
- package/.agents/scripts/lib/baselines/scope.js +39 -90
- package/.agents/scripts/lib/baselines/writer.js +16 -11
- package/.agents/scripts/lib/changed-files.js +8 -1
- package/.agents/scripts/lib/cli-args.js +115 -1
- package/.agents/scripts/lib/close-validation/runner.js +70 -25
- package/.agents/scripts/lib/crap-engine.js +32 -13
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +13 -0
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
- package/.agents/scripts/lib/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
- package/.agents/scripts/lib/observability/source-classifier.js +175 -2
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
- package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +209 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +48 -12
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
- package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
- package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +29 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +245 -140
- package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
- package/.agents/scripts/lib/story-adjacency.js +3 -3
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- package/.agents/scripts/lib/util/concurrent-map.js +17 -0
- package/.agents/scripts/lib/util/parse-id-list.js +103 -0
- package/.agents/scripts/lib/wave-runner/live-probe.js +24 -14
- package/.agents/scripts/lib/wave-runner/ready-set.js +189 -42
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +4 -10
- package/.agents/scripts/lib/workers/crap-worker.js +2 -10
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
- package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
- package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
- package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
- package/.agents/scripts/lib/worktree-manager.js +14 -0
- package/.agents/scripts/plan-run-epilogue.js +17 -5
- package/.agents/scripts/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +112 -28
- package/.agents/scripts/resolve-stories.js +4 -1
- package/.agents/scripts/run-coverage.js +86 -35
- package/.agents/scripts/run-lint.js +20 -0
- package/.agents/scripts/run-tests.js +26 -36
- package/.agents/scripts/single-story-close.js +28 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/stories-wave-tick.js +214 -38
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- package/.agents/scripts/validate-docs-freshness.js +1 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
- package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/deliver.md +13 -4
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
- package/.agents/workflows/helpers/deliver-digest.md +41 -21
- package/.agents/workflows/helpers/deliver-reference.md +77 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +47 -6
- package/.agents/workflows/helpers/plan-reference.md +15 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +3 -0
- package/README.md +13 -6
- package/docs/CHANGELOG.md +64 -0
- package/package.json +9 -4
- package/.agents/schemas/friction-event.schema.json +0 -56
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
|
@@ -24,6 +24,10 @@ import {
|
|
|
24
24
|
parse as parseStoryBody,
|
|
25
25
|
serialize as serializeStoryBody,
|
|
26
26
|
} from '../../story-body/story-body.js';
|
|
27
|
+
import {
|
|
28
|
+
concurrentMap,
|
|
29
|
+
FANOUT_CONCURRENCY,
|
|
30
|
+
} from '../../util/concurrent-map.js';
|
|
27
31
|
import { assertSpecWithinBudget } from '../spec-spill.js';
|
|
28
32
|
import { assertAcceptancePartition } from '../split-policy-validator.js';
|
|
29
33
|
import {
|
|
@@ -795,6 +799,14 @@ async function ensurePersistLabel({
|
|
|
795
799
|
* (see `ensureCohortLabel`). `/deliver` never reads it — delivery stays
|
|
796
800
|
* ids-only over live state (Story #4540's actual point).
|
|
797
801
|
*
|
|
802
|
+
* **The create loop stays serial and dependency-ordered** — deliberately, and
|
|
803
|
+
* unlike every other per-Story loop on this path (Story #4952). It is not an
|
|
804
|
+
* independent fan-out: `renderStoryBodyForCreate(story, idBySlug)` resolves a
|
|
805
|
+
* Story's `depends_on` slugs to the real issue ids its siblings were just
|
|
806
|
+
* minted with, and `idBySlug` is filled *in loop order* by the POSTs
|
|
807
|
+
* themselves. Running it concurrently would render `#undefined` dependency
|
|
808
|
+
* refs for any Story whose dependency had not yet returned an id.
|
|
809
|
+
*
|
|
798
810
|
* **Sibling order is mirrored into native GitHub `blocked_by` edges** once
|
|
799
811
|
* every id is known (Story #4544), so plan-created order stops depending on
|
|
800
812
|
* prose. That pass is non-fatal — see `mirrorNativeDependencyEdges`.
|
|
@@ -960,6 +972,16 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
|
|
|
960
972
|
* Fails closed: an un-flipped Story is invisible to `/deliver`, which is the
|
|
961
973
|
* safe direction — the operator is told exactly which ids need the label.
|
|
962
974
|
*
|
|
975
|
+
* **Collect failures, never fast-fail** (preserved verbatim under the
|
|
976
|
+
* Story #4952 concurrency conversion). Every Story is attempted even when an
|
|
977
|
+
* earlier one's PATCH rejects, because the whole value of the closing error is
|
|
978
|
+
* naming the *complete* set of ids that still need the label by hand. The
|
|
979
|
+
* mapper below therefore absorbs its own rejection into a per-Story outcome
|
|
980
|
+
* rather than letting `concurrentMap`'s first-rejection-wins policy abandon
|
|
981
|
+
* the remaining flips. `concurrentMap` preserves input order, so `readied[]`
|
|
982
|
+
* and the reported failures come back in `created[]` order exactly as the
|
|
983
|
+
* serial loop produced them.
|
|
984
|
+
*
|
|
963
985
|
* @param {object} args
|
|
964
986
|
* @param {object} args.provider
|
|
965
987
|
* @param {Array<{ id: number, slug: string }>} args.created
|
|
@@ -972,18 +994,32 @@ export async function markStoriesReady({ provider, created }) {
|
|
|
972
994
|
'Stories to agent::ready.',
|
|
973
995
|
);
|
|
974
996
|
}
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
997
|
+
const outcomes = await concurrentMap(
|
|
998
|
+
created,
|
|
999
|
+
async (story) => {
|
|
1000
|
+
try {
|
|
1001
|
+
await provider.updateTicket(story.id, {
|
|
1002
|
+
labels: { add: [AGENT_LABELS.READY] },
|
|
1003
|
+
});
|
|
1004
|
+
return { id: story.id, failure: null };
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
return {
|
|
1007
|
+
id: story.id,
|
|
1008
|
+
failure: `#${story.id} (${story.slug}): ${err.message}`,
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
},
|
|
1012
|
+
// The terminal per-Story `agent::ready` PATCHes (Story #4952): each flip
|
|
1013
|
+
// is an independent single-issue write, so the loop was serial only by
|
|
1014
|
+
// construction. A latency fix, not a throughput one.
|
|
1015
|
+
{ concurrency: FANOUT_CONCURRENCY },
|
|
1016
|
+
);
|
|
1017
|
+
const readied = outcomes
|
|
1018
|
+
.filter((outcome) => outcome.failure === null)
|
|
1019
|
+
.map((outcome) => outcome.id);
|
|
1020
|
+
const failed = outcomes
|
|
1021
|
+
.filter((outcome) => outcome.failure !== null)
|
|
1022
|
+
.map((outcome) => outcome.failure);
|
|
987
1023
|
if (failed.length > 0) {
|
|
988
1024
|
throw new Error(
|
|
989
1025
|
`[plan-persist] ${failed.length} Story(ies) were created with their ` +
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
38
|
import { Logger } from '../../Logger.js';
|
|
39
|
+
import {
|
|
40
|
+
concurrentMap,
|
|
41
|
+
FANOUT_CONCURRENCY,
|
|
42
|
+
} from '../../util/concurrent-map.js';
|
|
39
43
|
import { upsertStructuredComment } from '../ticketing.js';
|
|
40
44
|
|
|
41
45
|
/** Structured-comment type marking a source issue as superseded. */
|
|
@@ -440,41 +444,94 @@ export async function closeSupersededTickets({
|
|
|
440
444
|
const createdBySlug = new Map(
|
|
441
445
|
(created ?? []).map((story) => [story.slug, story]),
|
|
442
446
|
);
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
447
|
+
const units = collectSupersedeUnits(stories, createdBySlug);
|
|
448
|
+
|
|
449
|
+
// Story #4952 — one bounded fan-out across distinct source tickets. The
|
|
450
|
+
// mapper inherits `closeOneSupersededTicket`'s never-throw contract (and
|
|
451
|
+
// resolves the two pre-write outcomes itself), so `concurrentMap`'s
|
|
452
|
+
// first-rejection-wins policy can never fire and no unit is abandoned
|
|
453
|
+
// because a sibling failed. Input order is preserved, so the report arrays
|
|
454
|
+
// below read exactly as the serial nested loop wrote them.
|
|
455
|
+
const outcomes = await concurrentMap(
|
|
456
|
+
units,
|
|
457
|
+
({ id, note, createdStory }) => {
|
|
449
458
|
if (!createdStory) {
|
|
450
|
-
|
|
451
|
-
continue;
|
|
452
|
-
}
|
|
453
|
-
if (dryRun) {
|
|
454
|
-
report.planned.push({ ticket: id, storySlug: createdStory.slug });
|
|
455
|
-
continue;
|
|
459
|
+
return { outcome: 'skipped', reason: 'story-not-created' };
|
|
456
460
|
}
|
|
457
|
-
|
|
461
|
+
if (dryRun) return { outcome: 'planned' };
|
|
462
|
+
return closeOneSupersededTicket({
|
|
458
463
|
provider,
|
|
459
464
|
id,
|
|
460
465
|
note,
|
|
461
466
|
story: createdStory,
|
|
462
467
|
sourceTicketIds: sources,
|
|
463
468
|
});
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
469
|
+
},
|
|
470
|
+
// The per-source-ticket close (Story #4952) fans out across **distinct**
|
|
471
|
+
// tickets — `assertSupersedePartition` has already failed the run closed
|
|
472
|
+
// if two Stories claim the same id, so no two units in flight can touch
|
|
473
|
+
// the same issue. Within one unit the probe → comment → close sequence
|
|
474
|
+
// stays strictly ordered: commenting on an issue the probe reported
|
|
475
|
+
// closed, or closing one the comment never landed on, is the whole
|
|
476
|
+
// failure mode this phase is careful about.
|
|
477
|
+
{ concurrency: FANOUT_CONCURRENCY },
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
const report = emptyReport({ enabled: true, dryRun });
|
|
481
|
+
outcomes.forEach((result, index) => {
|
|
482
|
+
recordSupersedeOutcome(report, units[index], result);
|
|
483
|
+
});
|
|
473
484
|
|
|
474
485
|
logSupersedeReport(report);
|
|
475
486
|
return report;
|
|
476
487
|
}
|
|
477
488
|
|
|
489
|
+
/**
|
|
490
|
+
* Flatten the per-Story `supersedes[]` maps into one list of close units, in
|
|
491
|
+
* the nested iteration order the report arrays are expected to follow.
|
|
492
|
+
*
|
|
493
|
+
* @param {Array<{ slug: string, supersedes?: Array<{ id: number, note: string|null }> }>|undefined} stories
|
|
494
|
+
* @param {Map<string, { slug: string, id: number, title: string }>} createdBySlug
|
|
495
|
+
* @returns {Array<{ id: number, note: string|null, createdStory: object|undefined }>}
|
|
496
|
+
*/
|
|
497
|
+
function collectSupersedeUnits(stories, createdBySlug) {
|
|
498
|
+
const units = [];
|
|
499
|
+
for (const story of stories ?? []) {
|
|
500
|
+
const createdStory = createdBySlug.get(story.slug);
|
|
501
|
+
for (const { id, note } of story.supersedes ?? []) {
|
|
502
|
+
units.push({ id, note, createdStory });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return units;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* File one unit's outcome onto the report.
|
|
510
|
+
*
|
|
511
|
+
* @param {SupersedeReport} report
|
|
512
|
+
* @param {{ id: number, createdStory: object|undefined }} unit
|
|
513
|
+
* @param {{ outcome: string, reason?: string }} result
|
|
514
|
+
* @returns {void}
|
|
515
|
+
*/
|
|
516
|
+
function recordSupersedeOutcome(report, unit, result) {
|
|
517
|
+
if (result.outcome === 'closed') {
|
|
518
|
+
report.closed.push(unit.id);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (result.outcome === 'planned') {
|
|
522
|
+
report.planned.push({
|
|
523
|
+
ticket: unit.id,
|
|
524
|
+
storySlug: unit.createdStory.slug,
|
|
525
|
+
});
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (result.outcome === 'skipped') {
|
|
529
|
+
report.skipped.push({ ticket: unit.id, reason: result.reason });
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
report.failed.push({ ticket: unit.id, reason: result.reason });
|
|
533
|
+
}
|
|
534
|
+
|
|
478
535
|
/**
|
|
479
536
|
* Surface the supersede outcome on the console so a partial failure is
|
|
480
537
|
* visible without reading the JSON envelope.
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
*
|
|
19
19
|
* - **dangling-citation** — a sentence referencing a document section
|
|
20
20
|
* (`§`, "design note", "review doc") with no repo-relative path and no
|
|
21
|
-
* `#<digits>` issue anchor in the same sentence.
|
|
21
|
+
* `#<digits>` issue anchor in the same sentence. An anchor written
|
|
22
|
+
* inside a code span counts — the conventional markdown form (Story
|
|
23
|
+
* #4906).
|
|
22
24
|
* - **open-question** — interrogative-to-operator phrasing ("Flag if",
|
|
23
25
|
* "TBD", "confirm with the operator", a trailing `?`) in Goal/Spec
|
|
24
26
|
* prose outside code spans. Bodies record decisions; unresolved
|
|
@@ -73,28 +75,55 @@ const OPEN_QUESTION_MARKERS = [
|
|
|
73
75
|
*/
|
|
74
76
|
|
|
75
77
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
+
* Private-use sentinel standing in for one extracted inline code span.
|
|
79
|
+
* It carries no citation marker, no anchor and no sentence boundary, so it
|
|
80
|
+
* is inert for every marker heuristic while recording where the span sat.
|
|
81
|
+
*/
|
|
82
|
+
const CODE_SLOT_PATTERN = /\uE000(\d+)\uE001/g;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Replace fenced code blocks with a space and each inline code span with a
|
|
86
|
+
* positional slot, so code content (shell snippets, grep patterns, JSON)
|
|
87
|
+
* never trips a prose heuristic yet stays recoverable for the anchor check.
|
|
78
88
|
*
|
|
79
89
|
* @param {string} text
|
|
80
|
-
* @returns {string}
|
|
90
|
+
* @returns {{ slotted: string, spans: string[] }}
|
|
81
91
|
*/
|
|
82
|
-
function
|
|
83
|
-
|
|
92
|
+
function slotCodeSpans(text) {
|
|
93
|
+
const spans = [];
|
|
94
|
+
const slotted = text
|
|
95
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
96
|
+
.replace(/`[^`\n]*`/g, (span) => {
|
|
97
|
+
spans.push(span.slice(1, -1));
|
|
98
|
+
return `\uE000${spans.length - 1}\uE001`;
|
|
99
|
+
});
|
|
100
|
+
return { slotted, spans };
|
|
84
101
|
}
|
|
85
102
|
|
|
86
103
|
/**
|
|
87
104
|
* Split prose into sentence-ish units. Newlines are boundaries too, so a
|
|
88
|
-
* bullet list yields one unit per bullet.
|
|
105
|
+
* bullet list yields one unit per bullet. Each unit carries two views of
|
|
106
|
+
* the same sentence: `prose`, with code content removed, which every
|
|
107
|
+
* marker heuristic reads; and `anchorText`, with inline code content
|
|
108
|
+
* restored, so a citation anchored inside a code span is still visible to
|
|
109
|
+
* the anchor check. Restoring only ever adds anchors — markers are matched
|
|
110
|
+
* against `prose` alone, exactly as before.
|
|
89
111
|
*
|
|
90
112
|
* @param {string} text
|
|
91
|
-
* @returns {string
|
|
113
|
+
* @returns {Array<{ prose: string, anchorText: string }>}
|
|
92
114
|
*/
|
|
93
115
|
function splitSentences(text) {
|
|
94
|
-
|
|
116
|
+
const { slotted, spans } = slotCodeSpans(text);
|
|
117
|
+
return slotted
|
|
95
118
|
.split(/(?<=[.!?])\s+|\n+/)
|
|
96
|
-
.map((
|
|
97
|
-
|
|
119
|
+
.map((unit) => ({
|
|
120
|
+
prose: unit.replace(CODE_SLOT_PATTERN, ' ').trim(),
|
|
121
|
+
anchorText: unit.replace(
|
|
122
|
+
CODE_SLOT_PATTERN,
|
|
123
|
+
(_slot, index) => ` ${spans[Number(index)]} `,
|
|
124
|
+
),
|
|
125
|
+
}))
|
|
126
|
+
.filter((unit) => unit.prose.length > 0);
|
|
98
127
|
}
|
|
99
128
|
|
|
100
129
|
/**
|
|
@@ -112,18 +141,21 @@ function excerpt(text) {
|
|
|
112
141
|
|
|
113
142
|
/**
|
|
114
143
|
* dangling-citation: a citation-marker sentence with no locating anchor.
|
|
144
|
+
* The marker is matched against the sentence's code-stripped prose; the
|
|
145
|
+
* anchor against its code-restored text, so a path or issue reference
|
|
146
|
+
* written in a code span — the conventional markdown form — counts.
|
|
115
147
|
*
|
|
116
|
-
* @param {string} prose -
|
|
148
|
+
* @param {string} prose - Raw body prose.
|
|
117
149
|
* @param {string} slug
|
|
118
150
|
* @returns {TextHygieneFinding[]}
|
|
119
151
|
*/
|
|
120
152
|
function findDanglingCitations(prose, slug) {
|
|
121
153
|
const findings = [];
|
|
122
|
-
for (const sentence of splitSentences(prose)) {
|
|
154
|
+
for (const { prose: sentence, anchorText } of splitSentences(prose)) {
|
|
123
155
|
const cites = CITATION_MARKERS.some((m) => m.test(sentence));
|
|
124
156
|
if (!cites) continue;
|
|
125
157
|
const anchored =
|
|
126
|
-
ISSUE_ANCHOR.test(
|
|
158
|
+
ISSUE_ANCHOR.test(anchorText) || REPO_PATH_ANCHOR.test(anchorText);
|
|
127
159
|
if (anchored) continue;
|
|
128
160
|
findings.push({
|
|
129
161
|
kind: 'dangling-citation',
|
|
@@ -142,13 +174,13 @@ function findDanglingCitations(prose, slug) {
|
|
|
142
174
|
* open-question: operator-directed phrasing (or a trailing `?`) in prose a
|
|
143
175
|
* non-interactive sub-agent executes.
|
|
144
176
|
*
|
|
145
|
-
* @param {string} prose -
|
|
177
|
+
* @param {string} prose - Raw Goal/Spec prose.
|
|
146
178
|
* @param {string} slug
|
|
147
179
|
* @returns {TextHygieneFinding[]}
|
|
148
180
|
*/
|
|
149
181
|
function findOpenQuestions(prose, slug) {
|
|
150
182
|
const findings = [];
|
|
151
|
-
for (const sentence of splitSentences(prose)) {
|
|
183
|
+
for (const { prose: sentence } of splitSentences(prose)) {
|
|
152
184
|
const marked =
|
|
153
185
|
OPEN_QUESTION_MARKERS.some((m) => m.test(sentence)) ||
|
|
154
186
|
sentence.endsWith('?');
|
|
@@ -217,12 +249,11 @@ export function evaluateTextHygiene({ draftStories = null } = {}) {
|
|
|
217
249
|
}
|
|
218
250
|
const goal = typeof body.goal === 'string' ? body.goal : '';
|
|
219
251
|
const spec = typeof body.spec === 'string' ? body.spec : '';
|
|
220
|
-
const bodyProse =
|
|
221
|
-
typeof story.body === 'string' ? story.body : [goal, spec].join('\n')
|
|
222
|
-
);
|
|
252
|
+
const bodyProse =
|
|
253
|
+
typeof story.body === 'string' ? story.body : [goal, spec].join('\n');
|
|
223
254
|
findings.push(
|
|
224
255
|
...findDanglingCitations(bodyProse, slug),
|
|
225
|
-
...findOpenQuestions(
|
|
256
|
+
...findOpenQuestions([goal, spec].join('\n'), slug),
|
|
226
257
|
...findSlicingMass(body, slug),
|
|
227
258
|
);
|
|
228
259
|
}
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* the provider call needed to load the Epic.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import * as os from 'node:os';
|
|
11
10
|
import path from 'node:path';
|
|
12
11
|
import {
|
|
13
12
|
resolveFeatureRoots,
|
|
@@ -15,39 +14,15 @@ import {
|
|
|
15
14
|
} from '../../bdd-runner-detect.js';
|
|
16
15
|
import { scanBddScenarios } from '../../bdd-scenario-scanner.js';
|
|
17
16
|
import { getPaths, PROJECT_ROOT } from '../../config-resolver.js';
|
|
18
|
-
import { scanMemoryFreshness } from '../../feedback-loop/memory-freshness.js';
|
|
19
17
|
import { fetchPriorFeedback } from '../../feedback-loop/prior-feedback-fetcher.js';
|
|
20
18
|
import { Logger } from '../../Logger.js';
|
|
21
19
|
import { hasTicketSection } from '../../ticket-body-sections.js';
|
|
20
|
+
import {
|
|
21
|
+
concurrentMap,
|
|
22
|
+
FANOUT_CONCURRENCY,
|
|
23
|
+
} from '../../util/concurrent-map.js';
|
|
22
24
|
import { ensureDocsDigest } from '../docs-digest.js';
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Resolve the per-project memory directory used by the memory-freshness
|
|
26
|
-
* pre-flight (Story #2557 / Epic #2547).
|
|
27
|
-
*
|
|
28
|
-
* Resolution order:
|
|
29
|
-
* 1. `MANDREL_MEMORY_DIR` environment variable (test seam and operator
|
|
30
|
-
* override).
|
|
31
|
-
* 2. `~/.claude/projects/<repo>/memory/` — the standard Claude Code
|
|
32
|
-
* memory substrate path, scoped by the configured GitHub repo so each
|
|
33
|
-
* consumer project gets its own memory pool.
|
|
34
|
-
* 3. `null` when neither is resolvable. The scanner tolerates a missing
|
|
35
|
-
* `memoryDir` and surfaces a single `errors[]` entry.
|
|
36
|
-
*
|
|
37
|
-
* @param {{ github?: { owner?: string, repo?: string }|null }} opts
|
|
38
|
-
* @returns {string|null}
|
|
39
|
-
*/
|
|
40
|
-
function resolveMemoryDir({ github } = {}) {
|
|
41
|
-
if (
|
|
42
|
-
typeof process.env.MANDREL_MEMORY_DIR === 'string' &&
|
|
43
|
-
process.env.MANDREL_MEMORY_DIR.length > 0
|
|
44
|
-
) {
|
|
45
|
-
return process.env.MANDREL_MEMORY_DIR;
|
|
46
|
-
}
|
|
47
|
-
const repo = github?.repo;
|
|
48
|
-
if (typeof repo !== 'string' || repo.length === 0) return null;
|
|
49
|
-
return path.join(os.homedir(), '.claude', 'projects', repo, 'memory');
|
|
50
|
-
}
|
|
25
|
+
import { buildMemoryPoolAdvisory } from './memory-pool-advisory.js';
|
|
51
26
|
|
|
52
27
|
/**
|
|
53
28
|
* Build the digest-first `docsContext` envelope field (Story #4433 — hard
|
|
@@ -95,6 +70,24 @@ async function buildPlanningDocsContext({ seedIssueId, settings, cwd }) {
|
|
|
95
70
|
return { mode: 'digest', digestPath: relPath };
|
|
96
71
|
}
|
|
97
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Story #2637 — index existing BDD scenarios so the Acceptance Engineer step
|
|
75
|
+
* can annotate planned ACs with matches from the project's `.feature` files.
|
|
76
|
+
* Empty array when the project has not adopted BDD; the scanner is
|
|
77
|
+
* best-effort and never throws on filesystem errors.
|
|
78
|
+
*
|
|
79
|
+
* @returns {Array<object>}
|
|
80
|
+
*/
|
|
81
|
+
function scanBddScenariosBestEffort() {
|
|
82
|
+
try {
|
|
83
|
+
const featureRoots = resolveFeatureRoots({ cwd: PROJECT_ROOT });
|
|
84
|
+
return scanBddScenarios({ featureRoots });
|
|
85
|
+
} catch (err) {
|
|
86
|
+
Logger.warn(`[plan-context] BDD scenario scan skipped: ${err.message}`);
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
98
91
|
/**
|
|
99
92
|
* Build the authoring context the host LLM (or the
|
|
100
93
|
* `/plan` author step) needs to write the Tech Spec.
|
|
@@ -130,51 +123,54 @@ export async function buildAuthoringContext(
|
|
|
130
123
|
|
|
131
124
|
const { cwd = PROJECT_ROOT } = opts;
|
|
132
125
|
|
|
133
|
-
const docsContext = await buildPlanningDocsContext({
|
|
134
|
-
seedIssueId: epic.id,
|
|
135
|
-
settings,
|
|
136
|
-
cwd,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// Story #2094 Task #2103 — verify the project's BDD runner pending-tag
|
|
140
|
-
// support so the acceptance-spec body can record either the verified tag
|
|
141
|
-
// (features-first ordering) or "fallback: dependencies-first ordering"
|
|
142
|
-
// when no supported runner is present.
|
|
143
|
-
const bddRunner = await verifyBddRunnerPendingTag({ cwd: PROJECT_ROOT });
|
|
144
|
-
|
|
145
|
-
// Story #2637 — index existing BDD scenarios so the Acceptance Engineer
|
|
146
|
-
// step can annotate planned ACs with matches from the project's
|
|
147
|
-
// `.feature` files. Empty array when the project has not adopted BDD;
|
|
148
|
-
// the scanner is best-effort and never throws on filesystem errors.
|
|
149
|
-
let bddScenarios = [];
|
|
150
|
-
try {
|
|
151
|
-
const featureRoots = resolveFeatureRoots({ cwd: PROJECT_ROOT });
|
|
152
|
-
bddScenarios = scanBddScenarios({ featureRoots });
|
|
153
|
-
} catch (err) {
|
|
154
|
-
Logger.warn(`[plan-context] BDD scenario scan skipped: ${err.message}`);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Story #2557 — memory-freshness pre-flight runs BEFORE the prior-feedback
|
|
158
|
-
// fetch so the planner sees a deduplicated, currently-actionable memory
|
|
159
|
-
// store. The scanner is best-effort: missing memory dir or gh-CLI failures
|
|
160
|
-
// land in `memoryFreshness.errors[]` and never throw.
|
|
161
126
|
const githubCfg = opts.github ?? null;
|
|
162
|
-
const memoryDir = resolveMemoryDir({ github: githubCfg });
|
|
163
|
-
const memoryFreshness = await scanMemoryFreshness({
|
|
164
|
-
memoryDir,
|
|
165
|
-
owner: githubCfg?.owner,
|
|
166
|
-
repo: githubCfg?.repo,
|
|
167
|
-
projectRoot: PROJECT_ROOT,
|
|
168
|
-
});
|
|
169
127
|
|
|
170
|
-
// Story #
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
128
|
+
// Story #4952 — these five gathers share no data, so they run under bounded
|
|
129
|
+
// concurrency instead of five sequential awaits on the interactive `/plan`
|
|
130
|
+
// path. `concurrentMap` preserves input order, so the destructuring is
|
|
131
|
+
// positional and the produced context is identical to the serial build:
|
|
132
|
+
//
|
|
133
|
+
// 1. the digest-first `docsContext` pointer (Story #4433);
|
|
134
|
+
// 2. Story #2094 Task #2103 — the project's BDD runner pending-tag
|
|
135
|
+
// support, so the acceptance-spec body records either the verified tag
|
|
136
|
+
// (features-first ordering) or "fallback: dependencies-first ordering"
|
|
137
|
+
// when no supported runner is present;
|
|
138
|
+
// 3. the best-effort `.feature` scenario index;
|
|
139
|
+
// 4. Story #4919 — the memory-pool advisory that replaced the retired
|
|
140
|
+
// memory-freshness pre-flight (#2557 / #4414) in the same slot. The
|
|
141
|
+
// scanner marked an entry stale when a cited issue was closed, but the
|
|
142
|
+
// memory corpus is delivery retrospectives whose subject IS a delivered
|
|
143
|
+
// Story — and its directory (`~/.claude/projects/<repo>/memory/`) never
|
|
144
|
+
// resolved, because harness project dirs are cwd-slugs. This renders no
|
|
145
|
+
// per-entry verdict at all: it stats and counts, and the `/plan` spine
|
|
146
|
+
// surfaces `recommend` at Gate #1. Filesystem-only and total;
|
|
147
|
+
// 5. Story #2554 — open meta feedback issues, so retro signals are routed
|
|
148
|
+
// into durable substrates rather than lost in chat. Best-effort:
|
|
149
|
+
// missing owner/repo or gh-CLI failures land in `errors[]`, never throw.
|
|
150
|
+
const [
|
|
151
|
+
docsContext,
|
|
152
|
+
bddRunner,
|
|
153
|
+
bddScenarios,
|
|
154
|
+
memoryPoolAdvisory,
|
|
155
|
+
priorFeedback,
|
|
156
|
+
] = await concurrentMap(
|
|
157
|
+
[
|
|
158
|
+
() => buildPlanningDocsContext({ seedIssueId: epic.id, settings, cwd }),
|
|
159
|
+
() => verifyBddRunnerPendingTag({ cwd: PROJECT_ROOT }),
|
|
160
|
+
() => scanBddScenariosBestEffort(),
|
|
161
|
+
() => buildMemoryPoolAdvisory({ cwd: PROJECT_ROOT }),
|
|
162
|
+
() =>
|
|
163
|
+
fetchPriorFeedback({
|
|
164
|
+
owner: githubCfg?.owner,
|
|
165
|
+
repo: githubCfg?.repo,
|
|
166
|
+
}),
|
|
167
|
+
],
|
|
168
|
+
(gather) => gather(),
|
|
169
|
+
// The independent context gathers (Story #4952): a handful of local
|
|
170
|
+
// probes plus one `gh` read, so this rides the shared fan-out bound
|
|
171
|
+
// rather than declaring its own.
|
|
172
|
+
{ concurrency: FANOUT_CONCURRENCY },
|
|
173
|
+
);
|
|
178
174
|
|
|
179
175
|
// Story #4811 — the codebase snapshot (#2634), its authoring grounding
|
|
180
176
|
// (#4139 F10) and the spec-freshness helpers behind it are retired. The
|
|
@@ -211,7 +207,7 @@ export async function buildAuthoringContext(
|
|
|
211
207
|
docsContext,
|
|
212
208
|
bddRunner,
|
|
213
209
|
bddScenarios,
|
|
214
|
-
|
|
210
|
+
memoryPoolAdvisory,
|
|
215
211
|
priorFeedback,
|
|
216
212
|
};
|
|
217
213
|
}
|