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
|
@@ -36,18 +36,20 @@ import { parseArgs } from 'node:util';
|
|
|
36
36
|
import { buildStoryBody } from './lib/audit-to-stories/build-story-body.js';
|
|
37
37
|
import { classifyGroupsAgainstGitHub } from './lib/audit-to-stories/dedupe-against-github.js';
|
|
38
38
|
import { formatEpicGrouping } from './lib/audit-to-stories/epic-grouping-directive.js';
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
toCanonicalFinding,
|
|
41
|
+
withFingerprints,
|
|
42
|
+
} from './lib/audit-to-stories/finding-adapter.js';
|
|
40
43
|
import { groupFindings } from './lib/audit-to-stories/group-findings.js';
|
|
41
44
|
import {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
writeLedger,
|
|
46
|
-
} from './lib/audit-to-stories/ledger.js';
|
|
45
|
+
loadIssuesFile,
|
|
46
|
+
normaliseIssueHit,
|
|
47
|
+
} from './lib/audit-to-stories/issues-file.js';
|
|
47
48
|
import {
|
|
48
49
|
resolveLedgerSummary,
|
|
49
50
|
runLedgerCommit,
|
|
50
51
|
} from './lib/audit-to-stories/ledger-commit.js';
|
|
52
|
+
import { recordFiledIssues } from './lib/audit-to-stories/ledger-record.js';
|
|
51
53
|
import {
|
|
52
54
|
parseAuditReports,
|
|
53
55
|
readSeverityTally,
|
|
@@ -55,6 +57,12 @@ import {
|
|
|
55
57
|
import { buildPlanSeedMarkdown } from './lib/audit-to-stories/seed-from-findings.js';
|
|
56
58
|
import { wireAuditStoryEdges } from './lib/audit-to-stories/wire-dependencies.js';
|
|
57
59
|
import { runAsCli } from './lib/cli-utils.js';
|
|
60
|
+
import {
|
|
61
|
+
DEFAULT_LEDGER_PATH,
|
|
62
|
+
readLedger,
|
|
63
|
+
reconcileLedger,
|
|
64
|
+
writeLedger,
|
|
65
|
+
} from './lib/findings/audit-ledger.js';
|
|
58
66
|
import { searchSemanticCandidates } from './lib/findings/semantic-issue-search.js';
|
|
59
67
|
import {
|
|
60
68
|
normalizeSeverity,
|
|
@@ -366,28 +374,6 @@ class ProviderUnavailableError extends Error {
|
|
|
366
374
|
}
|
|
367
375
|
}
|
|
368
376
|
|
|
369
|
-
/**
|
|
370
|
-
* Flatten one raw `searchIssues` hit onto the `{ number, state, title, body }`
|
|
371
|
-
* shape the dedupe module reads, collapsing every closed-ish state spelling
|
|
372
|
-
* (`CLOSED`, `state_reason: not_planned`, …) onto `'closed'`.
|
|
373
|
-
*
|
|
374
|
-
* @param {object} hit
|
|
375
|
-
* @returns {{ number: number, state: 'open'|'closed', title: string, body: string }}
|
|
376
|
-
*/
|
|
377
|
-
function normaliseIssueHit(hit) {
|
|
378
|
-
return {
|
|
379
|
-
number: hit.number,
|
|
380
|
-
state: (hit.state ?? hit.state_reason ?? 'open')
|
|
381
|
-
.toString()
|
|
382
|
-
.toLowerCase()
|
|
383
|
-
.includes('closed')
|
|
384
|
-
? 'closed'
|
|
385
|
-
: 'open',
|
|
386
|
-
title: hit.title ?? '',
|
|
387
|
-
body: hit.body ?? '',
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
|
|
391
377
|
/**
|
|
392
378
|
* Walk the list endpoint once per label and merge the pages into one
|
|
393
379
|
* deduplicated, normalised issue list.
|
|
@@ -649,6 +635,136 @@ function dedupDegradedWarning(entries) {
|
|
|
649
635
|
);
|
|
650
636
|
}
|
|
651
637
|
|
|
638
|
+
/**
|
|
639
|
+
* Render the operator-visible line naming a host-supplied index and its size.
|
|
640
|
+
*
|
|
641
|
+
* Always emitted for a `--issues-file` run, because "how many issues did you
|
|
642
|
+
* actually check against" is the one number that separates a real dedup from a
|
|
643
|
+
* plan that merely looks checked. At zero it is the load-bearing case: an empty
|
|
644
|
+
* corpus is a legitimate first sweep AND exactly what a broken fetch writes, so
|
|
645
|
+
* the operator — not the run — decides which this was. Deliberately distinct in
|
|
646
|
+
* wording from both `dedupSkippedWarning` and `dedupDegradedWarning` so the
|
|
647
|
+
* three are never confused in a scrollback.
|
|
648
|
+
*
|
|
649
|
+
* @param {{ source?: string, size?: number }} dedupIndex
|
|
650
|
+
* @returns {string}
|
|
651
|
+
*/
|
|
652
|
+
function dedupIndexWarning({ size = 0 } = {}) {
|
|
653
|
+
if (size === 0) {
|
|
654
|
+
return (
|
|
655
|
+
'dedup index: 0 issues supplied via --issues-file. Dedup DID run and ' +
|
|
656
|
+
'every group is correctly "create" — but that is also what a fetch that ' +
|
|
657
|
+
'returned nothing looks like. If audit issues already exist, the fetch ' +
|
|
658
|
+
'that wrote this file is broken and this run will re-file them.'
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
return `dedup index: ${size} issue(s) supplied via --issues-file; every exact-fingerprint lookup was answered from it.`;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Render the warning for a pre-fetch of the issue index that could not
|
|
666
|
+
* complete. The run still dedups — it falls back to a per-finding search — but
|
|
667
|
+
* it loses the one-list saving, and until Story #5301 this failure was
|
|
668
|
+
* swallowed whole: the operator saw only the downstream per-group degradation
|
|
669
|
+
* and could not tell that the pre-fetch itself was the cause.
|
|
670
|
+
*
|
|
671
|
+
* @param {string} reason
|
|
672
|
+
* @returns {string}
|
|
673
|
+
*/
|
|
674
|
+
function dedupIndexDegradedWarning(reason) {
|
|
675
|
+
return (
|
|
676
|
+
`dedup index unavailable: ${reason}. Dedup fell back to a per-finding ` +
|
|
677
|
+
'search, which is slower and rate-limited — if those searches also fail, ' +
|
|
678
|
+
'every affected group is classified "create" WITHOUT a dedup check.'
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Phase 6: classify every group against GitHub, and say loudly whichever way
|
|
684
|
+
* it went.
|
|
685
|
+
*
|
|
686
|
+
* Extracted from `buildPlan` because the gate has three outcomes, not two, and
|
|
687
|
+
* inlining them pushed the caller past its complexity ceiling. The three:
|
|
688
|
+
*
|
|
689
|
+
* - **deduped** — a provider resolved, or the host supplied a corpus, or
|
|
690
|
+
* both. A host-supplied corpus is a dedup source in its own right, which is
|
|
691
|
+
* the whole point: the gate asks "can we dedup at all", not "did a provider
|
|
692
|
+
* resolve". While it asked the latter, `--no-provider --issues-file` — the
|
|
693
|
+
* one invocation a `gh`-less host can run — short-circuited to the seeded
|
|
694
|
+
* all-`create` classifications however well the dedupe module worked.
|
|
695
|
+
* - **skipped, no port** — a provider was wanted but could not be adapted.
|
|
696
|
+
* - **skipped, disabled** — `--no-provider` with no corpus to fall back on.
|
|
697
|
+
*
|
|
698
|
+
* Every outcome warns on stderr, so the `--scan` JSON on stdout stays clean and
|
|
699
|
+
* a create-only plan is never read as "checked, found nothing".
|
|
700
|
+
*
|
|
701
|
+
* @param {{ groups: Array<object>, useProvider?: boolean,
|
|
702
|
+
* issues?: Array<object>|null }} params
|
|
703
|
+
* @param {{ loadProviderImpl: Function, classifyGroupsImpl: Function,
|
|
704
|
+
* logger: { warn: Function } }} deps
|
|
705
|
+
* @returns {Promise<{ classifications: Array<object>, summary: object,
|
|
706
|
+
* dedupApplied: boolean }>}
|
|
707
|
+
*/
|
|
708
|
+
async function runDedupPhase(
|
|
709
|
+
{ groups, useProvider, issues },
|
|
710
|
+
{ loadProviderImpl, classifyGroupsImpl, logger },
|
|
711
|
+
) {
|
|
712
|
+
const provider = useProvider ? await loadProviderImpl() : null;
|
|
713
|
+
if (!provider && !issues) {
|
|
714
|
+
logger.warn(
|
|
715
|
+
dedupSkippedWarning(useProvider ? 'no-provider-port' : 'disabled'),
|
|
716
|
+
);
|
|
717
|
+
return {
|
|
718
|
+
classifications: groups.map((group) => ({
|
|
719
|
+
group,
|
|
720
|
+
action: 'create',
|
|
721
|
+
matchedIssues: [],
|
|
722
|
+
matchedFingerprints: [],
|
|
723
|
+
})),
|
|
724
|
+
summary: { create: groups.length, skipOpen: 0, skipReoccurring: 0 },
|
|
725
|
+
dedupApplied: false,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const { classifications, summary } = await classifyGroupsImpl({
|
|
730
|
+
groups,
|
|
731
|
+
provider,
|
|
732
|
+
searchCandidates: provider?.searchCandidates,
|
|
733
|
+
listAuditIssues: provider?.listAuditIssues,
|
|
734
|
+
issues,
|
|
735
|
+
});
|
|
736
|
+
for (const warning of dedupPhaseWarnings({ issues, summary })) {
|
|
737
|
+
logger.warn(warning);
|
|
738
|
+
}
|
|
739
|
+
return { classifications, summary, dedupApplied: true };
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Every warning a completed dedup pass owes the operator, in order. Pure, so
|
|
744
|
+
* the wording stays unit-testable and `runDedupPhase` keeps one write site.
|
|
745
|
+
*
|
|
746
|
+
* @param {{ issues?: Array<object>|null, summary: object }} params
|
|
747
|
+
* @returns {string[]}
|
|
748
|
+
*/
|
|
749
|
+
function dedupPhaseWarnings({ issues, summary }) {
|
|
750
|
+
const warnings = [];
|
|
751
|
+
if (issues) warnings.push(dedupIndexWarning(summary.dedupIndex));
|
|
752
|
+
// The pre-fetch failing is a distinct fact from any group's lookup failing,
|
|
753
|
+
// and used to be invisible: the operator saw only the downstream per-group
|
|
754
|
+
// degradation and could not tell what had caused it.
|
|
755
|
+
if (summary.dedupDegraded?.indexPrefetch) {
|
|
756
|
+
warnings.push(
|
|
757
|
+
dedupIndexDegradedWarning(summary.dedupDegraded.indexPrefetch),
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
// A partially-checked plan is a useful result — name the groups that degraded
|
|
761
|
+
// to create because their lookup could not complete (Story #4678).
|
|
762
|
+
if (summary.dedupDegraded?.count > 0) {
|
|
763
|
+
warnings.push(dedupDegradedWarning(summary.dedupDegraded.groups));
|
|
764
|
+
}
|
|
765
|
+
return warnings;
|
|
766
|
+
}
|
|
767
|
+
|
|
652
768
|
/**
|
|
653
769
|
* Scan → group → dedup → (optionally) reconcile the cross-run ledger, and
|
|
654
770
|
* return the plan envelope.
|
|
@@ -657,12 +773,14 @@ function dedupDegradedWarning(entries) {
|
|
|
657
773
|
* implementation (`.agents/rules/test-seams.md` rules 1-2, 4), so `main`,
|
|
658
774
|
* `runAuto`, and every production caller are unchanged.
|
|
659
775
|
*
|
|
660
|
-
* @param {{ glob?: string, severity?: string, useProvider?: boolean,
|
|
776
|
+
* @param {{ glob?: string, severity?: string, useProvider?: boolean,
|
|
777
|
+
* issuesFile?: string, ledger?: object }} params
|
|
661
778
|
* @param {{
|
|
662
779
|
* collectReportPathsImpl?: typeof collectReportPaths,
|
|
663
780
|
* readReportsImpl?: typeof readReports,
|
|
664
781
|
* loadProviderImpl?: typeof loadProviderOrNull,
|
|
665
782
|
* classifyGroupsImpl?: typeof classifyGroupsAgainstGitHub,
|
|
783
|
+
* loadIssuesFileImpl?: typeof loadIssuesFile,
|
|
666
784
|
* reconcileScanLedgerImpl?: typeof reconcileScanLedger,
|
|
667
785
|
* logger?: { warn: Function },
|
|
668
786
|
* }} [deps]
|
|
@@ -673,6 +791,7 @@ async function buildPlan(
|
|
|
673
791
|
glob: pattern,
|
|
674
792
|
severity,
|
|
675
793
|
useProvider,
|
|
794
|
+
issuesFile,
|
|
676
795
|
ledger,
|
|
677
796
|
allowMissingTally,
|
|
678
797
|
failOnReportFailures,
|
|
@@ -684,9 +803,14 @@ async function buildPlan(
|
|
|
684
803
|
readReportsImpl = readReports,
|
|
685
804
|
loadProviderImpl = loadProviderOrNull,
|
|
686
805
|
classifyGroupsImpl = classifyGroupsAgainstGitHub,
|
|
806
|
+
loadIssuesFileImpl = loadIssuesFile,
|
|
687
807
|
reconcileScanLedgerImpl = reconcileScanLedger,
|
|
688
808
|
logger = Logger,
|
|
689
809
|
} = deps;
|
|
810
|
+
// Deliberately BEFORE the reports are read: an unusable corpus is a usage
|
|
811
|
+
// error, and failing fast costs the operator nothing, where failing late
|
|
812
|
+
// would tempt a fallback that silently dedups nothing.
|
|
813
|
+
const issues = issuesFile ? loadIssuesFileImpl(issuesFile) : null;
|
|
690
814
|
const reportPaths = await collectReportPathsImpl(pattern ?? DEFAULT_GLOB);
|
|
691
815
|
if (reportPaths.length === 0) {
|
|
692
816
|
return {
|
|
@@ -724,45 +848,10 @@ async function buildPlan(
|
|
|
724
848
|
const stamped = withFingerprints(filtered.filter((f) => Boolean(f.severity)));
|
|
725
849
|
const { groups, edges } = groupFindings(stamped);
|
|
726
850
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
matchedFingerprints: [],
|
|
732
|
-
}));
|
|
733
|
-
let summary = { create: groups.length, skipOpen: 0, skipReoccurring: 0 };
|
|
734
|
-
let dedupApplied = false;
|
|
735
|
-
|
|
736
|
-
if (useProvider) {
|
|
737
|
-
const provider = await loadProviderImpl();
|
|
738
|
-
if (provider) {
|
|
739
|
-
const result = await classifyGroupsImpl({
|
|
740
|
-
groups,
|
|
741
|
-
provider,
|
|
742
|
-
searchCandidates: provider.searchCandidates,
|
|
743
|
-
listAuditIssues: provider.listAuditIssues,
|
|
744
|
-
});
|
|
745
|
-
classifications = result.classifications;
|
|
746
|
-
summary = result.summary;
|
|
747
|
-
dedupApplied = true;
|
|
748
|
-
// A partially-checked plan is a useful result — warn loudly (stderr, so
|
|
749
|
-
// the --scan JSON on stdout stays clean) naming the groups that degraded
|
|
750
|
-
// to create because their lookup could not complete (Story #4678).
|
|
751
|
-
if (summary.dedupDegraded?.count > 0) {
|
|
752
|
-
logger.warn(dedupDegradedWarning(summary.dedupDegraded.groups));
|
|
753
|
-
}
|
|
754
|
-
} else {
|
|
755
|
-
// The provider could not resolve a searchIssues port — the dedup gate
|
|
756
|
-
// is silently a no-op without this. Surface it loudly (stderr, so the
|
|
757
|
-
// --scan JSON on stdout stays clean) so the operator does not read a
|
|
758
|
-
// create-only plan as "no duplicates found".
|
|
759
|
-
logger.warn(dedupSkippedWarning('no-provider-port'));
|
|
760
|
-
}
|
|
761
|
-
} else {
|
|
762
|
-
// Operator explicitly opted out via --no-provider. Still warn so a
|
|
763
|
-
// duplicate-opening re-run is never a surprise.
|
|
764
|
-
logger.warn(dedupSkippedWarning('disabled'));
|
|
765
|
-
}
|
|
851
|
+
const { classifications, summary, dedupApplied } = await runDedupPhase(
|
|
852
|
+
{ groups, useProvider, issues },
|
|
853
|
+
{ loadProviderImpl, classifyGroupsImpl, logger },
|
|
854
|
+
);
|
|
766
855
|
|
|
767
856
|
// Cross-run ledger (Story #4626): fold this scan onto the committed memory,
|
|
768
857
|
// suppress findings a prior run recorded as accepted-risk, and (unless the
|
|
@@ -832,6 +921,9 @@ function reconcileScanLedger({ ledgerPath, findings, classifications, write }) {
|
|
|
832
921
|
ledger: prior,
|
|
833
922
|
findings,
|
|
834
923
|
issueStates,
|
|
924
|
+
// The ledger lives in the shared findings layer and cannot import the
|
|
925
|
+
// audit adapter without closing a cycle, so the projection is ours to pass.
|
|
926
|
+
toCanonical: toCanonicalFinding,
|
|
835
927
|
});
|
|
836
928
|
if (write !== false) writeLedger(ledgerPath, next);
|
|
837
929
|
return new Set(
|
|
@@ -918,6 +1010,7 @@ async function runAuto({
|
|
|
918
1010
|
severity,
|
|
919
1011
|
dryRun,
|
|
920
1012
|
useProvider,
|
|
1013
|
+
issuesFile,
|
|
921
1014
|
ledgerPath,
|
|
922
1015
|
ledgerCommit,
|
|
923
1016
|
git,
|
|
@@ -930,6 +1023,7 @@ async function runAuto({
|
|
|
930
1023
|
glob,
|
|
931
1024
|
severity: floor,
|
|
932
1025
|
useProvider,
|
|
1026
|
+
issuesFile,
|
|
933
1027
|
ledger: { path: resolvedLedgerPath, write: !dryRun },
|
|
934
1028
|
// `--auto` never accepts `--allow-missing-tally`: an unattended sweep has
|
|
935
1029
|
// no operator to read a warning, so every report failure is fatal here.
|
|
@@ -967,6 +1061,11 @@ async function runAuto({
|
|
|
967
1061
|
skipReoccurring: byAction.skipReoccurring.length,
|
|
968
1062
|
suppressedByLedger: byAction.suppressed.length,
|
|
969
1063
|
},
|
|
1064
|
+
// `--auto` opens no Issues itself — the caller does, from the `--emit-stories`
|
|
1065
|
+
// drafts — so these keys are the only thing standing between its summary and
|
|
1066
|
+
// the `--wire-edges --ids` map. Without them an unattended sweep cannot
|
|
1067
|
+
// record what it filed, and the ledger stays empty however well it works.
|
|
1068
|
+
createGroupKeys: eligible.map((g) => g?.groupKey).filter(Boolean),
|
|
970
1069
|
// Re-detected open Issues the operator may want a "re-detected" comment on.
|
|
971
1070
|
reDetected: byAction.skipOpen
|
|
972
1071
|
.flatMap((c) => c.matchedIssues ?? [])
|
|
@@ -1076,20 +1175,50 @@ function wireEdgesPreconditionError(reason, detail) {
|
|
|
1076
1175
|
* re-rendered with a canonical `blocked by #N` footer and the same edges are
|
|
1077
1176
|
* mirrored as native `blocked_by` relations.
|
|
1078
1177
|
*
|
|
1178
|
+
* The same map is what the cross-run ledger needs to record what this run
|
|
1179
|
+
* filed, so the record rides along here rather than arriving as a second
|
|
1180
|
+
* command an operator must remember (Story #5305).
|
|
1181
|
+
*
|
|
1079
1182
|
* @param {object} params
|
|
1080
1183
|
* @param {object} params.plan A `--scan` plan envelope.
|
|
1081
1184
|
* @param {Record<string, number>} params.issueByGroupKey
|
|
1185
|
+
* @param {string} [params.ledgerPath] — ledger to record into; defaults to
|
|
1186
|
+
* `DEFAULT_LEDGER_PATH` inside the record.
|
|
1187
|
+
* @param {boolean} [params.write] — `false` computes the record without
|
|
1188
|
+
* persisting it (what `--dry-run` passes).
|
|
1082
1189
|
* @param {object} [deps]
|
|
1083
1190
|
* @param {Function} [deps.loadProviderImpl]
|
|
1084
1191
|
* @param {Function} [deps.wireImpl]
|
|
1085
|
-
* @
|
|
1192
|
+
* @param {Function} [deps.recordFiledIssuesImpl]
|
|
1193
|
+
* @returns {Promise<object>} the wiring summary, with the ledger record on
|
|
1194
|
+
* `ledger`.
|
|
1086
1195
|
*/
|
|
1087
|
-
async function wireEdges(
|
|
1088
|
-
|
|
1089
|
-
|
|
1196
|
+
async function wireEdges(
|
|
1197
|
+
{ plan, issueByGroupKey, ledgerPath, write },
|
|
1198
|
+
deps = {},
|
|
1199
|
+
) {
|
|
1200
|
+
const {
|
|
1201
|
+
loadProviderImpl = loadProvider,
|
|
1202
|
+
wireImpl = wireAuditStoryEdges,
|
|
1203
|
+
recordFiledIssuesImpl = recordFiledIssues,
|
|
1204
|
+
} = deps;
|
|
1090
1205
|
const groups = (plan.classifications ?? [])
|
|
1091
1206
|
.filter((c) => c.action === 'create')
|
|
1092
1207
|
.map((c) => c.group);
|
|
1208
|
+
|
|
1209
|
+
// Record BEFORE the provider is loaded. The record is pure local filesystem
|
|
1210
|
+
// work, while the wiring below needs a provider exposing `updateTicket` — and
|
|
1211
|
+
// the host most likely to lack one is the `gh`-less host where the ledger is
|
|
1212
|
+
// the only duplicate protection there is. Recording first means such a run
|
|
1213
|
+
// still remembers what it filed, and the precondition error below still
|
|
1214
|
+
// surfaces unchanged afterwards.
|
|
1215
|
+
const ledger = recordFiledIssuesImpl({
|
|
1216
|
+
ledgerPath,
|
|
1217
|
+
groups,
|
|
1218
|
+
issueByGroupKey,
|
|
1219
|
+
write,
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1093
1222
|
let provider;
|
|
1094
1223
|
try {
|
|
1095
1224
|
provider = await loadProviderImpl();
|
|
@@ -1099,7 +1228,7 @@ async function wireEdges({ plan, issueByGroupKey }, deps = {}) {
|
|
|
1099
1228
|
if (typeof provider?.updateTicket !== 'function') {
|
|
1100
1229
|
throw wireEdgesPreconditionError('fixture-no-write-port');
|
|
1101
1230
|
}
|
|
1102
|
-
|
|
1231
|
+
const wired = await wireImpl({
|
|
1103
1232
|
groups,
|
|
1104
1233
|
edges: plan.edges ?? [],
|
|
1105
1234
|
issueByGroupKey,
|
|
@@ -1107,6 +1236,7 @@ async function wireEdges({ plan, issueByGroupKey }, deps = {}) {
|
|
|
1107
1236
|
updateBody: (issueNumber, body) =>
|
|
1108
1237
|
provider.updateTicket(issueNumber, { body }),
|
|
1109
1238
|
});
|
|
1239
|
+
return { ...wired, ledger };
|
|
1110
1240
|
}
|
|
1111
1241
|
|
|
1112
1242
|
/**
|
|
@@ -1161,6 +1291,8 @@ export const __testing = {
|
|
|
1161
1291
|
loadProviderOrNull,
|
|
1162
1292
|
dedupSkippedWarning,
|
|
1163
1293
|
dedupDegradedWarning,
|
|
1294
|
+
dedupIndexWarning,
|
|
1295
|
+
dedupIndexDegradedWarning,
|
|
1164
1296
|
buildAndGateStories,
|
|
1165
1297
|
runAuto,
|
|
1166
1298
|
resolveSeverityFloor,
|
|
@@ -1291,6 +1423,7 @@ export async function runAuditToStories(
|
|
|
1291
1423
|
plan: { type: 'string' },
|
|
1292
1424
|
out: { type: 'string' },
|
|
1293
1425
|
'no-provider': { type: 'boolean' },
|
|
1426
|
+
'issues-file': { type: 'string' },
|
|
1294
1427
|
'allow-missing-tally': { type: 'boolean' },
|
|
1295
1428
|
json: { type: 'boolean' },
|
|
1296
1429
|
},
|
|
@@ -1308,6 +1441,7 @@ export async function runAuditToStories(
|
|
|
1308
1441
|
severity: values.severity,
|
|
1309
1442
|
dryRun: values['dry-run'],
|
|
1310
1443
|
useProvider: !values['no-provider'],
|
|
1444
|
+
issuesFile: values['issues-file'],
|
|
1311
1445
|
ledgerPath: values.ledger,
|
|
1312
1446
|
ledgerCommit: values['ledger-commit'],
|
|
1313
1447
|
})
|
|
@@ -1334,6 +1468,7 @@ export async function runAuditToStories(
|
|
|
1334
1468
|
glob: values.glob,
|
|
1335
1469
|
severity: values.severity,
|
|
1336
1470
|
useProvider: !values['no-provider'],
|
|
1471
|
+
issuesFile: values['issues-file'],
|
|
1337
1472
|
allowMissingTally: values['allow-missing-tally'],
|
|
1338
1473
|
});
|
|
1339
1474
|
|
|
@@ -1359,6 +1494,11 @@ export async function runAuditToStories(
|
|
|
1359
1494
|
wireEdgesImpl({
|
|
1360
1495
|
plan: loadPlanImpl(values.plan),
|
|
1361
1496
|
issueByGroupKey: parseIssueMapImpl(values.ids),
|
|
1497
|
+
ledgerPath: values.ledger,
|
|
1498
|
+
// `--scan` still never writes the ledger; `--wire-edges` runs only after
|
|
1499
|
+
// the Issues were really opened, where recording is never wrong — so the
|
|
1500
|
+
// record is on by default here and `--dry-run` is what suppresses it.
|
|
1501
|
+
write: !values['dry-run'],
|
|
1362
1502
|
});
|
|
1363
1503
|
|
|
1364
1504
|
// One table, not a chain of `if (values.X) { …; return; }`. Each entry
|
|
@@ -1435,7 +1575,7 @@ runAsCli(import.meta.url, main, {
|
|
|
1435
1575
|
['--emit-stories', 'Emit the Story drafts as JSON.'],
|
|
1436
1576
|
[
|
|
1437
1577
|
'--wire-edges',
|
|
1438
|
-
'Second pass: resolve the detected group edges to blocked by #N footers plus native blocked_by relations. Needs --plan and --ids.',
|
|
1578
|
+
'Second pass: resolve the detected group edges to blocked by #N footers plus native blocked_by relations, and record the mapped issues in the cross-run ledger as filed. Needs --plan and --ids; --dry-run suppresses the ledger write.',
|
|
1439
1579
|
],
|
|
1440
1580
|
[
|
|
1441
1581
|
'--ids <json|path>',
|
|
@@ -1443,7 +1583,10 @@ runAsCli(import.meta.url, main, {
|
|
|
1443
1583
|
],
|
|
1444
1584
|
['--glob <pattern>', 'Override the audit-results glob.'],
|
|
1445
1585
|
['--severity <level>', 'Lowest severity to include (high|medium|low).'],
|
|
1446
|
-
[
|
|
1586
|
+
[
|
|
1587
|
+
'--ledger <path>',
|
|
1588
|
+
`Path to the cross-run dedup ledger (default ${DEFAULT_LEDGER_PATH}).`,
|
|
1589
|
+
],
|
|
1447
1590
|
[
|
|
1448
1591
|
'--ledger-commit',
|
|
1449
1592
|
'After the --auto summary prints, commit a changed ledger onto chore/audit-ledger-<date>, push it, and open a PR against the base branch (never auto-merged). Ignored under --dry-run.',
|
|
@@ -1454,6 +1597,10 @@ runAsCli(import.meta.url, main, {
|
|
|
1454
1597
|
],
|
|
1455
1598
|
['--out <path>', 'Write output to a file instead of stdout.'],
|
|
1456
1599
|
['--no-provider', 'Skip live GitHub dedup lookups (offline).'],
|
|
1600
|
+
[
|
|
1601
|
+
'--issues-file <path>',
|
|
1602
|
+
'Dedup against a JSON array of issues the host already fetched (every issue labelled audit::*, state all) instead of listing them through the provider. Lets dedup run where there is no gh CLI; composes with --no-provider.',
|
|
1603
|
+
],
|
|
1457
1604
|
[
|
|
1458
1605
|
'--allow-missing-tally',
|
|
1459
1606
|
'Downgrade a missing "Severity tally:" line to a warning (--scan only; --auto ignores it).',
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ceremony-derive.js — derive a Story's acceptance ceremony from ground truth
|
|
5
|
+
* (Story #5313).
|
|
6
|
+
*
|
|
7
|
+
* The deliver digest used to hand the worker a three-module import block to
|
|
8
|
+
* paste into `node --input-type=module -e` — compute the change set, derive
|
|
9
|
+
* the level, resolve the ceremony — and every hand-carried incantation is a
|
|
10
|
+
* transcription risk (the object-vs-string `derivedLevel` slip alone routed a
|
|
11
|
+
* whole class of Stories to the null fail-safe). This CLI is that block,
|
|
12
|
+
* scripted: one invocation, one JSON object, computed from the branch rather
|
|
13
|
+
* than recalled.
|
|
14
|
+
*
|
|
15
|
+
* 1. Compute the change set ONCE for `<base>...story-<id>`
|
|
16
|
+
* (`lib/orchestration/change-set.js`).
|
|
17
|
+
* 2. Derive the change level and the sensitive-path classes from it
|
|
18
|
+
* (`lib/orchestration/review-depth.js#deriveChangeLevel`).
|
|
19
|
+
* 3. Resolve the ceremony for the configured profile
|
|
20
|
+
* (`lib/orchestration/ceremony-routing.js#resolveCeremonyForRisk`).
|
|
21
|
+
*
|
|
22
|
+
* Stdout: a single JSON object —
|
|
23
|
+
* { storyId, baseRef, headRef, files, enumerated, level, classes, profile,
|
|
24
|
+
* mode, reason, verdictOwner }
|
|
25
|
+
*
|
|
26
|
+
* `files` is the one change set every acceptance critic must be handed; the
|
|
27
|
+
* caller never lets a critic re-enumerate it. `files: null` means the diff
|
|
28
|
+
* could not be enumerated, which routes to the fail-safe fresh critic.
|
|
29
|
+
*
|
|
30
|
+
* Exit codes: 0 on a derived decision (including the `null` fail-safe — an
|
|
31
|
+
* unenumerable diff is a decision, not an error), 1 on a usage error.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { parseArgs } from 'node:util';
|
|
35
|
+
|
|
36
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
37
|
+
import { getDeliveryRouting } from './lib/config/delivery-routing.js';
|
|
38
|
+
import { resolveConfig } from './lib/config-resolver.js';
|
|
39
|
+
import { resolveCeremonyForRisk } from './lib/orchestration/ceremony-routing.js';
|
|
40
|
+
import { computeChangeSet } from './lib/orchestration/change-set.js';
|
|
41
|
+
import { deriveChangeLevel } from './lib/orchestration/review-depth.js';
|
|
42
|
+
|
|
43
|
+
const USAGE = {
|
|
44
|
+
invocation:
|
|
45
|
+
'node .agents/scripts/ceremony-derive.js --story <id> [--base <ref>] [--cwd <path>]',
|
|
46
|
+
summary:
|
|
47
|
+
'Compute the Story change set once, derive its change level and sensitive-path classes, resolve the acceptance ceremony, and print one JSON object.',
|
|
48
|
+
flags: [
|
|
49
|
+
['--story <id>', 'Story issue number; the head ref is story-<id>.'],
|
|
50
|
+
[
|
|
51
|
+
'--base <ref>',
|
|
52
|
+
'Base ref for the three-dot diff (default: project.baseBranch, normally main).',
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
'--cwd <path>',
|
|
56
|
+
'Checkout to run the diff in (default: the current directory).',
|
|
57
|
+
],
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse the CLI argv. Exported for tests.
|
|
63
|
+
*
|
|
64
|
+
* @param {string[]} argv
|
|
65
|
+
* @returns {{ storyId: number|null, base: string|null, cwd: string|null }}
|
|
66
|
+
*/
|
|
67
|
+
export function parseArgv(argv) {
|
|
68
|
+
const { values } = parseArgs({
|
|
69
|
+
args: argv,
|
|
70
|
+
options: {
|
|
71
|
+
story: { type: 'string' },
|
|
72
|
+
base: { type: 'string' },
|
|
73
|
+
cwd: { type: 'string' },
|
|
74
|
+
},
|
|
75
|
+
strict: false,
|
|
76
|
+
});
|
|
77
|
+
const storyId = Number.parseInt(values.story ?? '', 10);
|
|
78
|
+
return {
|
|
79
|
+
storyId: Number.isInteger(storyId) && storyId > 0 ? storyId : null,
|
|
80
|
+
base: values.base ?? null,
|
|
81
|
+
cwd: values.cwd ?? null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The derivation itself — pure over its injected collaborators, so tests can
|
|
87
|
+
* pin the envelope without spawning git or reading a config.
|
|
88
|
+
*
|
|
89
|
+
* @param {{ storyId: number, baseRef: string, cwd: string, ceremonyProfile: string }} input
|
|
90
|
+
* @param {{
|
|
91
|
+
* computeChangeSetImpl?: typeof computeChangeSet,
|
|
92
|
+
* deriveChangeLevelImpl?: typeof deriveChangeLevel,
|
|
93
|
+
* resolveCeremonyImpl?: typeof resolveCeremonyForRisk,
|
|
94
|
+
* }} [deps]
|
|
95
|
+
* @returns {{
|
|
96
|
+
* storyId: number, baseRef: string, headRef: string,
|
|
97
|
+
* files: string[]|null, enumerated: boolean,
|
|
98
|
+
* level: 'low'|'high'|null, classes: string[],
|
|
99
|
+
* profile: string, mode: 'fresh'|'inline', reason: string,
|
|
100
|
+
* verdictOwner: 'fresh-critic'|'inline-self-eval',
|
|
101
|
+
* }}
|
|
102
|
+
*/
|
|
103
|
+
export function deriveCeremony(
|
|
104
|
+
{ storyId, baseRef, cwd, ceremonyProfile },
|
|
105
|
+
deps = {},
|
|
106
|
+
) {
|
|
107
|
+
const {
|
|
108
|
+
computeChangeSetImpl = computeChangeSet,
|
|
109
|
+
deriveChangeLevelImpl = deriveChangeLevel,
|
|
110
|
+
resolveCeremonyImpl = resolveCeremonyForRisk,
|
|
111
|
+
} = deps;
|
|
112
|
+
const headRef = `story-${storyId}`;
|
|
113
|
+
const changeSet = computeChangeSetImpl({ baseRef, headRef, cwd });
|
|
114
|
+
const { level, classes } = deriveChangeLevelImpl({
|
|
115
|
+
changedFiles: changeSet.files,
|
|
116
|
+
});
|
|
117
|
+
// `derivedLevel` is the level STRING — handing the `{ level, classes }`
|
|
118
|
+
// object here was the transcription slip this CLI exists to retire.
|
|
119
|
+
const ceremony = resolveCeremonyImpl({
|
|
120
|
+
derivedLevel: level,
|
|
121
|
+
clusterIndex: 0,
|
|
122
|
+
ceremonyProfile,
|
|
123
|
+
});
|
|
124
|
+
return {
|
|
125
|
+
storyId,
|
|
126
|
+
baseRef,
|
|
127
|
+
headRef,
|
|
128
|
+
files: changeSet.files,
|
|
129
|
+
enumerated: changeSet.enumerated,
|
|
130
|
+
level,
|
|
131
|
+
classes,
|
|
132
|
+
profile: ceremony.profile,
|
|
133
|
+
mode: ceremony.mode,
|
|
134
|
+
reason: ceremony.reason,
|
|
135
|
+
verdictOwner: ceremony.verdictOwner,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* CLI shell: resolve the config-backed defaults, derive, print.
|
|
141
|
+
*
|
|
142
|
+
* @param {string[]} [argv]
|
|
143
|
+
* @param {{
|
|
144
|
+
* resolveConfigImpl?: typeof resolveConfig,
|
|
145
|
+
* stdout?: { write: (s: string) => void },
|
|
146
|
+
* cwd?: string,
|
|
147
|
+
* } & Parameters<typeof deriveCeremony>[1]} [deps]
|
|
148
|
+
* @returns {Promise<object>} the printed envelope
|
|
149
|
+
*/
|
|
150
|
+
export async function runCeremonyDeriveCli(
|
|
151
|
+
argv = process.argv.slice(2),
|
|
152
|
+
deps = {},
|
|
153
|
+
) {
|
|
154
|
+
const {
|
|
155
|
+
resolveConfigImpl = resolveConfig,
|
|
156
|
+
stdout = process.stdout,
|
|
157
|
+
cwd: defaultCwd = process.cwd(),
|
|
158
|
+
...derivationDeps
|
|
159
|
+
} = deps;
|
|
160
|
+
const { storyId, base, cwd } = parseArgv(argv);
|
|
161
|
+
if (!storyId) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
'ceremony-derive: --story <id> is required (a positive integer).',
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const workCwd = cwd ?? defaultCwd;
|
|
167
|
+
const config = resolveConfigImpl({ cwd: workCwd });
|
|
168
|
+
const envelope = deriveCeremony(
|
|
169
|
+
{
|
|
170
|
+
storyId,
|
|
171
|
+
baseRef: base ?? config?.project?.baseBranch ?? 'main',
|
|
172
|
+
cwd: workCwd,
|
|
173
|
+
ceremonyProfile: getDeliveryRouting(config).ceremonyProfile,
|
|
174
|
+
},
|
|
175
|
+
derivationDeps,
|
|
176
|
+
);
|
|
177
|
+
stdout.write(`${JSON.stringify(envelope)}\n`);
|
|
178
|
+
return envelope;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function main() {
|
|
182
|
+
await runCeremonyDeriveCli();
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
runAsCli(import.meta.url, main, {
|
|
187
|
+
source: 'ceremony-derive',
|
|
188
|
+
propagateExitCode: true,
|
|
189
|
+
errorPrefix: '[ceremony-derive] ❌ Fatal error',
|
|
190
|
+
usage: USAGE,
|
|
191
|
+
});
|