@opengsd/gsd-core 1.9.1 → 1.10.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -3
- package/.opencode/plugins/gsd-core.js +8 -1
- package/agents/gsd-code-fixer.md +27 -3
- package/agents/gsd-debugger.md +12 -246
- package/agents/gsd-executor.md +7 -5
- package/agents/gsd-integration-checker.md +3 -0
- package/agents/gsd-plan-checker.md +9 -0
- package/agents/gsd-planner.md +5 -8
- package/agents/gsd-roadmapper.md +21 -3
- package/agents/gsd-verifier.md +14 -70
- package/bin/install.js +453 -289
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/new-milestone.md +1 -1
- package/commands/gsd/plan-phase.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +579 -66
- package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
- package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
- package/gsd-core/bin/lib/api-coverage.cjs +120 -0
- package/gsd-core/bin/lib/audit.cjs +89 -1
- package/gsd-core/bin/lib/broken-windows.cjs +36 -6
- package/gsd-core/bin/lib/capability-registry.cjs +96 -110
- package/gsd-core/bin/lib/capability-validator.cjs +12 -2
- package/gsd-core/bin/lib/check-command-router.cjs +43 -1
- package/gsd-core/bin/lib/command-aliases.cjs +72 -0
- package/gsd-core/bin/lib/commands.cjs +26 -25
- package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +12 -1
- package/gsd-core/bin/lib/context-composer.cjs +278 -0
- package/gsd-core/bin/lib/context-predicates.cjs +506 -0
- package/gsd-core/bin/lib/core-utils.cjs +91 -12
- package/gsd-core/bin/lib/docs.cjs +3 -2
- package/gsd-core/bin/lib/external-job.cjs +19 -4
- package/gsd-core/bin/lib/frontmatter.cjs +84 -12
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
- package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
- package/gsd-core/bin/lib/graphify.cjs +142 -27
- package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
- package/gsd-core/bin/lib/host-integration.cjs +13 -1
- package/gsd-core/bin/lib/init-command-router.cjs +83 -8
- package/gsd-core/bin/lib/init.cjs +1021 -57
- package/gsd-core/bin/lib/install-engine.cjs +64 -10
- package/gsd-core/bin/lib/install-profiles.cjs +27 -1
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
- package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
- package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
- package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
- package/gsd-core/bin/lib/io.cjs +28 -3
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
- package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
- package/gsd-core/bin/lib/mcp-server.cjs +135 -3
- package/gsd-core/bin/lib/milestone.cjs +75 -47
- package/gsd-core/bin/lib/phase-id.cjs +63 -0
- package/gsd-core/bin/lib/phase-locator.cjs +138 -45
- package/gsd-core/bin/lib/phase.cjs +260 -28
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
- package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
- package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
- package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
- package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
- package/gsd-core/bin/lib/roadmap.cjs +37 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
- package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
- package/gsd-core/bin/lib/section-manifest.cjs +209 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
- package/gsd-core/bin/lib/smart-entry.cjs +12 -0
- package/gsd-core/bin/lib/state-transition.cjs +73 -8
- package/gsd-core/bin/lib/state.cjs +151 -62
- package/gsd-core/bin/lib/surface.cjs +12 -1
- package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
- package/gsd-core/bin/lib/uat.cjs +320 -21
- package/gsd-core/bin/lib/unusable-input.cjs +9 -0
- package/gsd-core/bin/lib/verification.cjs +29 -12
- package/gsd-core/bin/lib/verify.cjs +10 -2
- package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
- package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
- package/gsd-core/bin/lib/workstream.cjs +6 -0
- package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/references/artifact-types.md +10 -3
- package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
- package/gsd-core/references/debugger-techniques.md +255 -0
- package/gsd-core/references/research-documentation-lookup.md +5 -3
- package/gsd-core/references/specless-probe-fallback.md +7 -6
- package/gsd-core/references/verifier-wiring-patterns.md +100 -0
- package/gsd-core/references/worktree-branch-check.md +2 -2
- package/gsd-core/templates/summary-complex.md +2 -0
- package/gsd-core/templates/summary-minimal.md +2 -0
- package/gsd-core/templates/summary-standard.md +2 -0
- package/gsd-core/templates/summary.md +2 -0
- package/gsd-core/workflows/audit-milestone.md +3 -0
- package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
- package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
- package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
- package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
- package/gsd-core/workflows/autonomous.md +32 -69
- package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
- package/gsd-core/workflows/code-review.md +42 -160
- package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
- package/gsd-core/workflows/complete-milestone.md +23 -81
- package/gsd-core/workflows/debug.md +9 -12
- package/gsd-core/workflows/diagnose-issues.md +22 -0
- package/gsd-core/workflows/discovery-phase.md +4 -4
- package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
- package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
- package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
- package/gsd-core/workflows/docs-update.md +8 -51
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
- package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
- package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
- package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
- package/gsd-core/workflows/execute-phase.md +65 -137
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +6 -1
- package/gsd-core/workflows/ingest-docs.md +2 -1
- package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
- package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
- package/gsd-core/workflows/new-milestone.md +21 -38
- package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
- package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
- package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
- package/gsd-core/workflows/new-project.md +13 -226
- package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
- package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
- package/gsd-core/workflows/plan-phase.md +49 -193
- package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
- package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
- package/gsd-core/workflows/progress.md +11 -153
- package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
- package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
- package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
- package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
- package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
- package/gsd-core/workflows/quick.md +20 -390
- package/gsd-core/workflows/resume-project.md +3 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
- package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
- package/gsd-core/workflows/review.md +15 -8
- package/gsd-core/workflows/section-manifest.json +219 -0
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +17 -14
- package/gsd-core/workflows/spike-wrap-up.md +20 -5
- package/gsd-core/workflows/spike.md +50 -16
- package/gsd-core/workflows/sync-skills.md +49 -11
- package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
- package/gsd-core/workflows/transition.md +8 -21
- package/gsd-core/workflows/ui-phase.md +8 -7
- package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
- package/gsd-core/workflows/update.md +18 -7
- package/gsd-core/workflows/verify-phase.md +4 -7
- package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
- package/gsd-core/workflows/verify-work.md +8 -58
- package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
- package/hooks/dist/gsd-check-update-worker.js +14 -5
- package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
- package/hooks/dist/gsd-read-injection-scanner.js +7 -0
- package/hooks/dist/gsd-statusline.js +72 -6
- package/hooks/dist/gsd-worktree-path-guard.js +2 -1
- package/hooks/dist/gsd-write-guard.js +359 -0
- package/hooks/dist/lib/isolation-sentinel.js +268 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-agent-isolation-guard.js +428 -0
- package/hooks/gsd-check-update-worker.js +14 -5
- package/hooks/gsd-cursor-subagent-start.js +532 -26
- package/hooks/gsd-read-injection-scanner.js +7 -0
- package/hooks/gsd-statusline.js +72 -6
- package/hooks/gsd-worktree-path-guard.js +2 -1
- package/hooks/gsd-write-guard.js +359 -0
- package/hooks/hooks.json +12 -0
- package/hooks/lib/isolation-sentinel.js +268 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +14 -5
- package/pi/gsd.cjs +57 -12
- package/scripts/build-hooks.js +9 -0
- package/scripts/changeset/lint.cjs +9 -2
- package/scripts/changeset/serialize.cjs +5 -1
- package/scripts/gen-capability-matrix.cjs +1 -1
- package/scripts/gen-context-index.cjs +448 -0
- package/scripts/gen-inventory-manifest.cjs +101 -1
- package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
- package/scripts/gen-section-manifest.cjs +638 -0
- package/scripts/generate-package-identity.cjs +4 -2
- package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
- package/scripts/lint-compiled-artifact-sync.cjs +6 -1
- package/scripts/lint-docs-command-form.cjs +195 -0
- package/scripts/lint-docs-required.cjs +9 -1
- package/scripts/lint-emitted-drift-ack.cjs +215 -20
- package/scripts/lint-example-parser-parity.cjs +395 -0
- package/scripts/lint-test-file-count.allowlist.json +27 -1
- package/scripts/mutation-matrix.cjs +13 -0
- package/scripts/prompt-injection-scan.sh +27 -6
- package/scripts/run-tests.cjs +3 -2
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-execute-phase/SKILL.md +1 -1
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-plan-phase/SKILL.md +2 -2
- package/vscode/package.json +1 -1
- package/scripts/gen-emitted-baseline.cjs +0 -145
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Plan Dependency Graph — shared halt-propagation over a plan's depends_on DAG (#2830).
|
|
4
|
+
*
|
|
5
|
+
* Two independent "which plans are incomplete" readers exist in this codebase:
|
|
6
|
+
* phase.cts's wave-grouping (`cmdPhasePlanIndex`) and phase-locator.cts's
|
|
7
|
+
* phase-location primitive (`searchPhaseInDir`, consumed by ~50 symbols across
|
|
8
|
+
* five command routers). Before #2830, only the former parsed `depends_on` —
|
|
9
|
+
* and even it used the DAG only for topological wave assignment, never to
|
|
10
|
+
* propagate a halted plan's block onto its dependents. The latter never parsed
|
|
11
|
+
* `depends_on` at all; it derived completion from summary-file presence only.
|
|
12
|
+
* A plan that reaches a designed stop still writes a SUMMARY (recording the
|
|
13
|
+
* halt in prose only — no prior structured status existed for it), so both
|
|
14
|
+
* readers saw it as ordinary "complete" and reported its dependents as
|
|
15
|
+
* ordinary incomplete work, available to spawn against.
|
|
16
|
+
*
|
|
17
|
+
* This module is the SINGLE topological-order + halt-propagation engine both
|
|
18
|
+
* readers call, so the two can never re-diverge on this rule again. Each
|
|
19
|
+
* caller resolves its own raw `depends_on` tokens to canonical plan ids
|
|
20
|
+
* (case-fold + `extractCanonicalPlanId` fallback — the same resolution
|
|
21
|
+
* already performed by phase.cts's `computeDependencyLevels`) before
|
|
22
|
+
* building `PlanHaltNode[]`; this module owns the graph traversal (exactly
|
|
23
|
+
* one pass — Kahn's algorithm, or none at all when the caller already has a
|
|
24
|
+
* valid topological order, see `computeHaltPropagation`'s `precomputedOrder`
|
|
25
|
+
* parameter) plus the two small pure helpers below (`isHaltedStatus`,
|
|
26
|
+
* `buildSummaryFileIndex`) that both callers would otherwise duplicate
|
|
27
|
+
* identically — the exact "two implementations, one drifts" failure mode
|
|
28
|
+
* this fix exists to close. Each caller still owns its own file I/O (the
|
|
29
|
+
* actual `fs.readFileSync` + `extractFrontmatter` calls); only the
|
|
30
|
+
* interpretive logic is centralized here — except the SUMMARY-file
|
|
31
|
+
* read+extract wrapper itself (`isSummaryFileHalted`), which both callers
|
|
32
|
+
* previously duplicated near-identically and which is now centralized here
|
|
33
|
+
* too, for the same reason.
|
|
34
|
+
*/
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
|
|
40
|
+
const frontmatterMod = require("./frontmatter.cjs");
|
|
41
|
+
const { extractFrontmatter } = frontmatterMod;
|
|
42
|
+
/**
|
|
43
|
+
* The one place "does this SUMMARY status value mean halted" is decided.
|
|
44
|
+
* Case-insensitive, trims whitespace. Both `phase.cts`'s `cmdPhasePlanIndex`
|
|
45
|
+
* and `phase-locator.cts`'s `searchPhaseInDir` call this after reading a
|
|
46
|
+
* completed plan's SUMMARY frontmatter `status` field, so the definition of
|
|
47
|
+
* "halted" cannot drift between the two readers.
|
|
48
|
+
*/
|
|
49
|
+
function isHaltedStatus(status) {
|
|
50
|
+
if (typeof status !== 'string')
|
|
51
|
+
return false;
|
|
52
|
+
// #2830 review (defect 2): strip an unquoted trailing YAML comment (a run
|
|
53
|
+
// of whitespace followed by `#` and the rest of the line) before
|
|
54
|
+
// trimming/lowercasing. YAML scalars don't need quoting to carry an inline
|
|
55
|
+
// comment (`status: halted # designed stop`), but `extractFrontmatter`
|
|
56
|
+
// does not strip one — and all four summary templates literally show that
|
|
57
|
+
// spelling as guidance on the value line. Without this, an executor that
|
|
58
|
+
// mimics the template's own presentation would write a halt that silently
|
|
59
|
+
// reads back as not-halted. A `#` with no preceding whitespace is NOT a
|
|
60
|
+
// YAML comment start, so `halted#nospace` intentionally still fails to match.
|
|
61
|
+
const withoutTrailingComment = status.replace(/\s+#.*$/, '');
|
|
62
|
+
return withoutTrailingComment.trim().toLowerCase() === 'halted';
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Read a plan's SUMMARY file and report whether it declares `status: halted`
|
|
66
|
+
* (a designed stop, not an ordinary completion). Returns false — never
|
|
67
|
+
* throws — on a missing/unreadable/malformed SUMMARY, so an unreadable file
|
|
68
|
+
* degrades to the pre-#2830 behavior ("has a SUMMARY = complete") rather
|
|
69
|
+
* than breaking either caller.
|
|
70
|
+
*
|
|
71
|
+
* Two callers share this wrapper: `phase.cts`'s `cmdPhasePlanIndex` and
|
|
72
|
+
* `phase-locator.cts`'s `searchPhaseInDir` (the phase-location primitive
|
|
73
|
+
* consumed by ~50 symbols across five command routers). Both previously
|
|
74
|
+
* carried a near-identical local copy (read file -> `extractFrontmatter` ->
|
|
75
|
+
* `isHaltedStatus` -> swallow errors) that this module's own header comment
|
|
76
|
+
* calls out as the exact "two implementations, one drifts" failure mode it
|
|
77
|
+
* exists to prevent — centralizing the read+extract wrapper here, not just
|
|
78
|
+
* the `isHaltedStatus` predicate, closes that gap.
|
|
79
|
+
*
|
|
80
|
+
* Takes a single resolved `summaryPath` (not a `dir` + `filename` pair) —
|
|
81
|
+
* `phase-locator.cts`'s prior local copy took the two parts separately and
|
|
82
|
+
* `path.join`'d them internally; that caller now does the join itself
|
|
83
|
+
* before calling in, so both callers share one signature.
|
|
84
|
+
*
|
|
85
|
+
* @param summaryPath - absolute or relative path to a `*-SUMMARY.md` file.
|
|
86
|
+
*/
|
|
87
|
+
function isSummaryFileHalted(summaryPath) {
|
|
88
|
+
try {
|
|
89
|
+
const content = node_fs_1.default.readFileSync(summaryPath, 'utf-8');
|
|
90
|
+
const fm = extractFrontmatter(content, summaryPath);
|
|
91
|
+
return isHaltedStatus(fm['status']);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Build a planId -> summary-filename lookup from a phase's summary file
|
|
99
|
+
* list, keyed by both the exact SUMMARY-file-derived id and its canonical
|
|
100
|
+
* form (mirrors the `completedPlanIds` construction each caller already
|
|
101
|
+
* performs for its own SUMMARY-presence check — same `summaryFiles` list,
|
|
102
|
+
* same exact/canonical key pair — so the two can never disagree about which
|
|
103
|
+
* summary file belongs to which plan id). `extractCanonicalPlanId` is
|
|
104
|
+
* supplied by the caller (each module owns its own resolution helper).
|
|
105
|
+
*/
|
|
106
|
+
function buildSummaryFileIndex(summaryFiles, extractCanonicalPlanId) {
|
|
107
|
+
const index = new Map();
|
|
108
|
+
for (const s of summaryFiles) {
|
|
109
|
+
const exact = s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
|
110
|
+
const canonical = extractCanonicalPlanId(s);
|
|
111
|
+
index.set(exact, s);
|
|
112
|
+
if (canonical !== exact)
|
|
113
|
+
index.set(canonical, s);
|
|
114
|
+
}
|
|
115
|
+
return index;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Computes halt-propagation over a plan dependency DAG.
|
|
119
|
+
*
|
|
120
|
+
* `precomputedOrder`: when the caller ALREADY has a valid topological order
|
|
121
|
+
* for these exact node ids (e.g. phase.cts's `cmdPhasePlanIndex`, which runs
|
|
122
|
+
* `computeDependencyLevels`'s Kahn's-algorithm pass for wave assignment
|
|
123
|
+
* before ever calling this function), pass it here and this function skips
|
|
124
|
+
* running Kahn's algorithm a second time — the halt-propagation forward pass
|
|
125
|
+
* below only needs A valid topological order, not to derive one itself.
|
|
126
|
+
* Omit it (as phase-locator.cts's `searchPhaseInDir` does — it has no prior
|
|
127
|
+
* traversal of this DAG) and this function derives the order itself; either
|
|
128
|
+
* way, exactly one Kahn's-algorithm pass runs per caller, never two.
|
|
129
|
+
*
|
|
130
|
+
* Diamond-safe (a plan blocked via two different halted ancestors gets both
|
|
131
|
+
* in its `blockedBy` list, deduplicated) and transitive-safe (a dependent of
|
|
132
|
+
* a dependent of a halted plan is blocked, at any depth).
|
|
133
|
+
*/
|
|
134
|
+
function computeHaltPropagation(nodes, precomputedOrder) {
|
|
135
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
136
|
+
let order;
|
|
137
|
+
let visited;
|
|
138
|
+
if (precomputedOrder) {
|
|
139
|
+
// Caller already ran Kahn's algorithm over this exact node set (same ids)
|
|
140
|
+
// — trust its order and skip re-deriving one. `visited` mirrors the same
|
|
141
|
+
// "count of nodes reachable in valid topological order" contract a fresh
|
|
142
|
+
// derivation would produce.
|
|
143
|
+
order = precomputedOrder;
|
|
144
|
+
visited = precomputedOrder.length;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
const inDeg = new Map();
|
|
148
|
+
const adj = new Map(); // dependency id -> dependent ids
|
|
149
|
+
for (const n of nodes) {
|
|
150
|
+
if (!inDeg.has(n.id))
|
|
151
|
+
inDeg.set(n.id, 0);
|
|
152
|
+
if (!adj.has(n.id))
|
|
153
|
+
adj.set(n.id, []);
|
|
154
|
+
for (const depId of n.resolvedDependsOn) {
|
|
155
|
+
if (!byId.has(depId))
|
|
156
|
+
continue; // fail-safe: caller should have filtered these already
|
|
157
|
+
if (!adj.has(depId))
|
|
158
|
+
adj.set(depId, []);
|
|
159
|
+
adj.get(depId).push(n.id);
|
|
160
|
+
inDeg.set(n.id, (inDeg.get(n.id) ?? 0) + 1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const queue = [];
|
|
164
|
+
for (const n of nodes) {
|
|
165
|
+
if ((inDeg.get(n.id) ?? 0) === 0)
|
|
166
|
+
queue.push(n.id);
|
|
167
|
+
}
|
|
168
|
+
// Dequeue by head index, not Array.shift() — O(1) amortized, same
|
|
169
|
+
// rationale as computeDependencyLevels (#307).
|
|
170
|
+
let head = 0;
|
|
171
|
+
let v = 0;
|
|
172
|
+
while (head < queue.length) {
|
|
173
|
+
const cur = queue[head++];
|
|
174
|
+
v++;
|
|
175
|
+
for (const dep of adj.get(cur) ?? []) {
|
|
176
|
+
inDeg.set(dep, inDeg.get(dep) - 1);
|
|
177
|
+
if (inDeg.get(dep) === 0)
|
|
178
|
+
queue.push(dep);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
order = queue;
|
|
182
|
+
visited = v;
|
|
183
|
+
}
|
|
184
|
+
// `order` is a valid topological order for every visited node: for edge
|
|
185
|
+
// dep -> dependent, dep appears before dependent. A single forward pass
|
|
186
|
+
// over it — using each node's own already-resolved dependsOn ids —
|
|
187
|
+
// computes blockedBy without any further graph traversal.
|
|
188
|
+
const blockedBy = new Map();
|
|
189
|
+
for (const id of order) {
|
|
190
|
+
const node = byId.get(id);
|
|
191
|
+
if (!node)
|
|
192
|
+
continue;
|
|
193
|
+
const causes = new Set();
|
|
194
|
+
for (const depId of node.resolvedDependsOn) {
|
|
195
|
+
const depNode = byId.get(depId);
|
|
196
|
+
if (!depNode)
|
|
197
|
+
continue;
|
|
198
|
+
if (depNode.halted)
|
|
199
|
+
causes.add(depId);
|
|
200
|
+
const depCauses = blockedBy.get(depId);
|
|
201
|
+
if (depCauses) {
|
|
202
|
+
for (const c of depCauses)
|
|
203
|
+
causes.add(c);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (causes.size > 0)
|
|
207
|
+
blockedBy.set(id, Array.from(causes));
|
|
208
|
+
}
|
|
209
|
+
// #2830 review (defect 1): a node involved in a depends_on cycle (or
|
|
210
|
+
// downstream of one) never reaches indegree 0, so it never appears in
|
|
211
|
+
// `order` and the forward pass above never visits it — it would otherwise
|
|
212
|
+
// end up absent from BOTH `blockedBy` and any cycle diagnostic, i.e.
|
|
213
|
+
// reported as ordinary runnable. A node whose position in the topological
|
|
214
|
+
// order is undecidable cannot be shown to be safe to run: silently
|
|
215
|
+
// dropping it is the exact silent-disappearance failure #2830 exists to
|
|
216
|
+
// prevent. Fail closed — give every such non-halted node an explicit,
|
|
217
|
+
// non-empty `blockedBy` entry so no consumer (present or future) can
|
|
218
|
+
// re-admit it as runnable merely by checking "absent from blockedBy".
|
|
219
|
+
// (A node that is itself halted is not "blocked" — it IS the blocker —
|
|
220
|
+
// so it is left out here exactly as the normal forward pass leaves it out.)
|
|
221
|
+
if (order.length < nodes.length) {
|
|
222
|
+
const orderSet = new Set(order);
|
|
223
|
+
for (const n of nodes) {
|
|
224
|
+
if (orderSet.has(n.id) || n.halted)
|
|
225
|
+
continue;
|
|
226
|
+
const causes = Array.from(new Set(n.resolvedDependsOn.filter((depId) => byId.has(depId)))).sort();
|
|
227
|
+
blockedBy.set(n.id, causes.length > 0 ? causes : [n.id]);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { order, visited, blockedBy };
|
|
231
|
+
}
|
|
232
|
+
module.exports = { computeHaltPropagation, isHaltedStatus, buildSummaryFileIndex, isSummaryFileHalted };
|
|
@@ -146,6 +146,10 @@ function planningPaths(cwd, ws) {
|
|
|
146
146
|
config: node_path_1.default.join(base, 'config.json'),
|
|
147
147
|
phases: node_path_1.default.join(base, 'phases'),
|
|
148
148
|
requirements: node_path_1.default.join(base, 'REQUIREMENTS.md'),
|
|
149
|
+
// #3149: the debug-session directory. Single source for both `state.load`'s
|
|
150
|
+
// `debug_dir` field and `init.debug`'s — previously each composed its own
|
|
151
|
+
// `path.join(planning, 'debug')` (DEFECT.GENERATIVE-FIX).
|
|
152
|
+
debug: node_path_1.default.join(base, 'debug'),
|
|
149
153
|
};
|
|
150
154
|
}
|
|
151
155
|
/**
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Used by the review pipeline to support small-context models.
|
|
12
12
|
*
|
|
13
|
-
* Trim priority (in order — never violate)
|
|
13
|
+
* Trim priority (in order — never violate). As of #2929 (epic #1671 Phase 2)
|
|
14
|
+
* the ladder itself is executed by the shared `context-composer` seam
|
|
15
|
+
* (`composeWithinBudget`); this module builds the fragment list in this
|
|
16
|
+
* exact order, delegates the decision of what survives/shrinks/truncates,
|
|
17
|
+
* and keeps only the prompt-specific rendering (`renderNote`,
|
|
18
|
+
* `assemblePrompt`) and the hard-fail response shapes here:
|
|
14
19
|
* 1. Instructions: ALWAYS kept verbatim
|
|
15
20
|
* 2. Reserve note tokens FIRST when any trim is anticipated
|
|
16
21
|
* 3. Roadmap: ALWAYS kept verbatim
|
|
@@ -22,9 +27,20 @@
|
|
|
22
27
|
* 9. Hard-fail: if minimum-set exceeds effectiveBudget
|
|
23
28
|
*/
|
|
24
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.PLAN_FLOOR_CHARS = void 0;
|
|
25
31
|
exports.estimateTokens = estimateTokens;
|
|
32
|
+
exports.buildBudgetFragments = buildBudgetFragments;
|
|
26
33
|
exports.applyBudget = applyBudget;
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- context-composer.cjs is an export= CommonJS module
|
|
35
|
+
const contextComposer = require("./context-composer.cjs");
|
|
27
36
|
const NOTE_RESERVE_TOKENS = 80;
|
|
37
|
+
/**
|
|
38
|
+
* Floor per plan when proportionally truncating and for the minimum-set
|
|
39
|
+
* check. Exported so consumers (notably the load-bearing contract gate,
|
|
40
|
+
* issue #3065) never need to re-declare this value locally.
|
|
41
|
+
*/
|
|
42
|
+
exports.PLAN_FLOOR_CHARS = 1024;
|
|
43
|
+
const MIN_PLAN_BYTES = exports.PLAN_FLOOR_CHARS;
|
|
28
44
|
const DEFAULT_NOTE_TEMPLATE = [
|
|
29
45
|
'<note>',
|
|
30
46
|
'Prompt automatically trimmed to fit a {budget}-token budget.',
|
|
@@ -51,30 +67,6 @@ function renderNote(template, budget, omitted, planTruncationPct) {
|
|
|
51
67
|
.replace('{omittedList}', omittedList)
|
|
52
68
|
.replace('{planTruncationPct}', String(Math.round(planTruncationPct)));
|
|
53
69
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Head-shrink a string to at most `maxLines` lines.
|
|
56
|
-
*/
|
|
57
|
-
function headShrink(text, maxLines) {
|
|
58
|
-
if (maxLines <= 0)
|
|
59
|
-
return '';
|
|
60
|
-
let idx = -1;
|
|
61
|
-
let seen = 0;
|
|
62
|
-
while (seen < maxLines) {
|
|
63
|
-
idx = text.indexOf('\n', idx + 1);
|
|
64
|
-
if (idx === -1)
|
|
65
|
-
return text;
|
|
66
|
-
seen += 1;
|
|
67
|
-
}
|
|
68
|
-
return text.slice(0, idx);
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Tail-truncate a string to at most `maxChars` characters.
|
|
72
|
-
*/
|
|
73
|
-
function tailTruncate(text, maxChars) {
|
|
74
|
-
if (text.length <= maxChars)
|
|
75
|
-
return text;
|
|
76
|
-
return text.slice(0, maxChars);
|
|
77
|
-
}
|
|
78
70
|
/**
|
|
79
71
|
* Assemble the final prompt string from its sections.
|
|
80
72
|
*/
|
|
@@ -99,38 +91,92 @@ function assemblePrompt(parts) {
|
|
|
99
91
|
blocks.push('## Requirements\n\n' + requirements);
|
|
100
92
|
return blocks.join('\n\n');
|
|
101
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Build the `composeWithinBudget` fragment array for a set of review prompt
|
|
96
|
+
* sections, with each fragment's declared trim strategy attached.
|
|
97
|
+
*
|
|
98
|
+
* Extracted (issue #3065) so the load-bearing contract gate
|
|
99
|
+
* (tests/load-bearing-contract-gate.test.cjs) can assert directly over the
|
|
100
|
+
* REAL declared strategies used by `applyBudget`, rather than a hand-copied
|
|
101
|
+
* duplicate array. A duplicate would let production silently diverge from
|
|
102
|
+
* the gate (e.g. flipping `roadmap` from `verbatim` to `drop` would keep the
|
|
103
|
+
* gate green if it read from a copy) — exactly the
|
|
104
|
+
* `DEFECT.GENERATIVE-FIX` divergence class this extraction exists to
|
|
105
|
+
* prevent. Pure; performs no I/O and has no side effects.
|
|
106
|
+
*/
|
|
107
|
+
function buildBudgetFragments(sections, projectMdHeadLines) {
|
|
108
|
+
const { instructions, roadmap, plans, projectMd: projectMdRaw = null, context: contextRaw = null, research: researchRaw = null, requirements: requirementsRaw = null, } = sections;
|
|
109
|
+
// Unique per-plan fragment ids: plain `plan:<file>` unless a filename
|
|
110
|
+
// collision forces disambiguation by index — composeWithinBudget throws on
|
|
111
|
+
// duplicate ids, and legitimate input may repeat a filename across plans.
|
|
112
|
+
const usedPlanIds = new Set();
|
|
113
|
+
const planIds = plans.map((p, idx) => {
|
|
114
|
+
let id = `plan:${p.file}`;
|
|
115
|
+
if (usedPlanIds.has(id))
|
|
116
|
+
id = `plan:${idx}:${p.file}`;
|
|
117
|
+
usedPlanIds.add(id);
|
|
118
|
+
return id;
|
|
119
|
+
});
|
|
120
|
+
const planFragments = plans.map((p, idx) => ({
|
|
121
|
+
id: planIds[idx],
|
|
122
|
+
content: p.content,
|
|
123
|
+
wrapper: `### ${p.file}\n\n`,
|
|
124
|
+
strategy: { kind: 'proportional-truncate', floorChars: MIN_PLAN_BYTES },
|
|
125
|
+
group: 'plans',
|
|
126
|
+
required: true,
|
|
127
|
+
}));
|
|
128
|
+
return [
|
|
129
|
+
{ id: 'instructions', content: instructions, wrapper: '', strategy: { kind: 'verbatim' }, required: true },
|
|
130
|
+
{ id: 'roadmap', content: roadmap, wrapper: '## Roadmap\n\n', strategy: { kind: 'verbatim' }, required: true },
|
|
131
|
+
{
|
|
132
|
+
id: 'projectMd',
|
|
133
|
+
content: projectMdRaw ?? '',
|
|
134
|
+
wrapper: '## Project\n\n',
|
|
135
|
+
strategy: { kind: 'head-shrink', maxLines: projectMdHeadLines },
|
|
136
|
+
},
|
|
137
|
+
{ id: 'plans-header', content: '', wrapper: '## Plans\n\n', strategy: { kind: 'verbatim' }, required: true },
|
|
138
|
+
...planFragments,
|
|
139
|
+
{ id: 'context', content: contextRaw ?? '', wrapper: '## Context\n\n', strategy: { kind: 'drop' } },
|
|
140
|
+
{ id: 'research', content: researchRaw ?? '', wrapper: '## Research\n\n', strategy: { kind: 'drop' } },
|
|
141
|
+
{ id: 'requirements', content: requirementsRaw ?? '', wrapper: '## Requirements\n\n', strategy: { kind: 'drop' } },
|
|
142
|
+
];
|
|
143
|
+
}
|
|
102
144
|
/**
|
|
103
145
|
* Apply a token budget to a set of review prompt sections.
|
|
104
146
|
* Returns the trimmed prompt and structured metadata.
|
|
105
147
|
*/
|
|
106
148
|
function applyBudget({ sections, budget, options = {} }) {
|
|
107
149
|
const { safetyMarginPct = 10, noteTemplate = DEFAULT_NOTE_TEMPLATE, projectMdHeadLines = 40, } = options;
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
150
|
+
const { plans } = sections;
|
|
151
|
+
const fragments = buildBudgetFragments(sections, projectMdHeadLines);
|
|
152
|
+
// Recover the per-plan fragment ids buildBudgetFragments assigned (in
|
|
153
|
+
// `plans` declaration order) rather than re-deriving the id-collision
|
|
154
|
+
// logic here — a single source of truth for id assignment.
|
|
155
|
+
const planIds = fragments.filter((f) => f.group === 'plans').map((f) => f.id);
|
|
156
|
+
const composed = contextComposer.composeWithinBudget({
|
|
157
|
+
fragments,
|
|
158
|
+
budget,
|
|
159
|
+
measure: estimateTokens,
|
|
160
|
+
options: {
|
|
161
|
+
safetyMarginPct,
|
|
162
|
+
reserve: NOTE_RESERVE_TOKENS,
|
|
163
|
+
charsPerUnit: 4,
|
|
164
|
+
// Minimum-set floor: instructions + roadmap (in full) + 1KB per plan.
|
|
165
|
+
// NOTE_RESERVE_TOKENS is intentionally excluded — a note is only
|
|
166
|
+
// injected when trimming actually occurs, and a prompt that fits
|
|
167
|
+
// without any trim needs no note at all. Wrappers are also excluded
|
|
168
|
+
// (this checks the floor, not the steady-state budget).
|
|
169
|
+
minimumFor: (f) => {
|
|
170
|
+
if (f.id === 'instructions' || f.id === 'roadmap')
|
|
171
|
+
return f.content;
|
|
172
|
+
if (f.id.startsWith('plan:'))
|
|
173
|
+
return f.content.slice(0, MIN_PLAN_BYTES);
|
|
174
|
+
return null;
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
const effectiveBudget = composed.metadata.effectiveBudget;
|
|
179
|
+
if (composed.metadata.hardFailed) {
|
|
134
180
|
return {
|
|
135
181
|
prompt: '',
|
|
136
182
|
metadata: {
|
|
@@ -145,127 +191,45 @@ function applyBudget({ sections, budget, options = {} }) {
|
|
|
145
191
|
},
|
|
146
192
|
};
|
|
147
193
|
}
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const getCurrentBaseTokens = () => staticBaseTokens +
|
|
175
|
-
projectTokens +
|
|
176
|
-
planContentTokens +
|
|
177
|
-
contextTokens +
|
|
178
|
-
researchTokens +
|
|
179
|
-
requirementsTokens;
|
|
180
|
-
let currentBaseTokens = getCurrentBaseTokens();
|
|
181
|
-
// Detect budget pressure: is ANY trim needed?
|
|
182
|
-
// Pressure exists when the current base tokens already exceed the effective
|
|
183
|
-
// budget. Only when pressure is real do we reserve NOTE_RESERVE_TOKENS so
|
|
184
|
-
// the note itself fits after trimming. Checking against
|
|
185
|
-
// effectiveBudget - NOTE_RESERVE_TOKENS (the old threshold) would cause
|
|
186
|
-
// spurious pressure 80 tokens early, dropping sections that fit fine.
|
|
187
|
-
const baseTokens = currentBaseTokens;
|
|
188
|
-
const budgetUnderPressure = baseTokens > effectiveBudget;
|
|
189
|
-
// Available for content (reserve note slot when under pressure)
|
|
190
|
-
const contentBudget = budgetUnderPressure
|
|
191
|
-
? effectiveBudget - NOTE_RESERVE_TOKENS
|
|
192
|
-
: effectiveBudget;
|
|
193
|
-
// ── Trim step 1: head-shrink PROJECT.md ───────────────────────────────────
|
|
194
|
-
if (currentBaseTokens > contentBudget && projectMd) {
|
|
195
|
-
const shrunk = headShrink(projectMd, projectMdHeadLines);
|
|
196
|
-
if (shrunk !== projectMd) {
|
|
197
|
-
projectMd = shrunk;
|
|
198
|
-
projectMdShrunk = true;
|
|
199
|
-
projectTokens = TOKENS_PROJECT_HEADER + estimateTokens(projectMd);
|
|
200
|
-
currentBaseTokens = getCurrentBaseTokens();
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
// ── Trim step 2: proportional plan truncation ─────────────────────────────
|
|
204
|
-
if (currentBaseTokens > contentBudget) {
|
|
205
|
-
// Compute tokens available for plan content only
|
|
206
|
-
const overhead = staticBaseTokens +
|
|
207
|
-
projectTokens +
|
|
208
|
-
contextTokens +
|
|
209
|
-
researchTokens +
|
|
210
|
-
requirementsTokens;
|
|
211
|
-
const planBudgetTokens = contentBudget - overhead;
|
|
212
|
-
const totalPlanTokens = planContentTokens;
|
|
213
|
-
if (planBudgetTokens > 0 && planBudgetTokens < totalPlanTokens) {
|
|
214
|
-
// Proportional share per plan (at least 1KB per plan)
|
|
215
|
-
const totalOriginalChars = plans.reduce((sum, p) => sum + p.content.length, 0);
|
|
216
|
-
const totalPlanCharsBudget = planBudgetTokens * 4;
|
|
217
|
-
workingPlans = workingPlans.map((p) => {
|
|
218
|
-
const proportionalShare = totalOriginalChars > 0
|
|
219
|
-
? Math.floor((p.content.length / totalOriginalChars) * totalPlanCharsBudget)
|
|
220
|
-
: 0;
|
|
221
|
-
const maxChars = Math.max(proportionalShare, MIN_PLAN_BYTES);
|
|
222
|
-
return { file: p.file, content: tailTruncate(p.content, maxChars) };
|
|
223
|
-
});
|
|
224
|
-
const newTotalChars = workingPlans.reduce((sum, p) => sum + p.content.length, 0);
|
|
225
|
-
if (totalOriginalChars > 0) {
|
|
226
|
-
planTruncationPct =
|
|
227
|
-
((totalOriginalChars - newTotalChars) / totalOriginalChars) * 100;
|
|
228
|
-
}
|
|
229
|
-
planContentTokens = workingPlans.reduce((sum, p) => sum + estimateTokens(p.content), 0);
|
|
230
|
-
currentBaseTokens = getCurrentBaseTokens();
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
// ── Trim step 3: drop context ─────────────────────────────────────────────
|
|
234
|
-
if (currentBaseTokens > contentBudget && context) {
|
|
235
|
-
context = null;
|
|
236
|
-
omitted.push('context');
|
|
237
|
-
contextTokens = 0;
|
|
238
|
-
currentBaseTokens = getCurrentBaseTokens();
|
|
239
|
-
}
|
|
240
|
-
// ── Trim step 4: drop research ────────────────────────────────────────────
|
|
241
|
-
if (currentBaseTokens > contentBudget && research) {
|
|
242
|
-
research = null;
|
|
243
|
-
omitted.push('research');
|
|
244
|
-
researchTokens = 0;
|
|
245
|
-
currentBaseTokens = getCurrentBaseTokens();
|
|
246
|
-
}
|
|
247
|
-
// ── Trim step 5: drop requirements (last resort) ──────────────────────────
|
|
248
|
-
if (currentBaseTokens > contentBudget && requirements) {
|
|
249
|
-
requirements = null;
|
|
250
|
-
omitted.push('requirements');
|
|
251
|
-
requirementsTokens = 0;
|
|
252
|
-
currentBaseTokens = getCurrentBaseTokens();
|
|
253
|
-
}
|
|
194
|
+
const byId = new Map(composed.fragments.map((f) => [f.id, f]));
|
|
195
|
+
const get = (id) => {
|
|
196
|
+
const found = byId.get(id);
|
|
197
|
+
if (!found)
|
|
198
|
+
throw new Error(`applyBudget: missing composed fragment "${id}"`);
|
|
199
|
+
return found;
|
|
200
|
+
};
|
|
201
|
+
const instructionsOut = get('instructions').content;
|
|
202
|
+
const roadmapOut = get('roadmap').content;
|
|
203
|
+
const projectMdFragment = get('projectMd');
|
|
204
|
+
const projectMd = projectMdFragment.present ? projectMdFragment.content : null;
|
|
205
|
+
const contextFragment = get('context');
|
|
206
|
+
const context = contextFragment.present ? contextFragment.content : null;
|
|
207
|
+
const researchFragment = get('research');
|
|
208
|
+
const research = researchFragment.present ? researchFragment.content : null;
|
|
209
|
+
const requirementsFragment = get('requirements');
|
|
210
|
+
const requirements = requirementsFragment.present ? requirementsFragment.content : null;
|
|
211
|
+
// Plans are never dropped — only their content may be truncated — so map
|
|
212
|
+
// deliberately by original index/file rather than relying on `present`.
|
|
213
|
+
const workingPlans = plans.map((p, idx) => ({
|
|
214
|
+
file: p.file,
|
|
215
|
+
content: get(planIds[idx]).content,
|
|
216
|
+
}));
|
|
217
|
+
const omitted = composed.metadata.omitted;
|
|
218
|
+
const projectMdShrunk = composed.metadata.shrunk.includes('projectMd');
|
|
219
|
+
const planTruncationPct = composed.metadata.truncationPct;
|
|
254
220
|
// ── Decide whether note is actually needed ────────────────────────────────
|
|
255
221
|
const anyTrimOccurred = omitted.length > 0 || projectMdShrunk || planTruncationPct > 0;
|
|
256
222
|
let note = null;
|
|
223
|
+
let noteInjected = false;
|
|
257
224
|
if (anyTrimOccurred) {
|
|
258
225
|
note = renderNote(noteTemplate, budget, omitted, planTruncationPct);
|
|
259
226
|
noteInjected = true;
|
|
260
227
|
}
|
|
261
|
-
// Suppress unused variable warning — currentBaseTokens is used via the
|
|
262
|
-
// closure in getCurrentBaseTokens(); the final value is not used directly.
|
|
263
|
-
void currentBaseTokens;
|
|
264
228
|
// ── Assemble ──────────────────────────────────────────────────────────────
|
|
265
229
|
const prompt = assemblePrompt({
|
|
266
|
-
instructions,
|
|
230
|
+
instructions: instructionsOut,
|
|
267
231
|
note,
|
|
268
|
-
roadmap,
|
|
232
|
+
roadmap: roadmapOut,
|
|
269
233
|
projectMd,
|
|
270
234
|
plans: workingPlans,
|
|
271
235
|
context,
|
|
@@ -274,7 +238,6 @@ function applyBudget({ sections, budget, options = {} }) {
|
|
|
274
238
|
});
|
|
275
239
|
const estimatedTokens = estimateTokens(prompt);
|
|
276
240
|
if (estimatedTokens > effectiveBudget) {
|
|
277
|
-
hardFailed = true;
|
|
278
241
|
return {
|
|
279
242
|
prompt: '',
|
|
280
243
|
metadata: {
|
|
@@ -284,7 +247,7 @@ function applyBudget({ sections, budget, options = {} }) {
|
|
|
284
247
|
omitted,
|
|
285
248
|
projectMdShrunk,
|
|
286
249
|
planTruncationPct,
|
|
287
|
-
hardFailed,
|
|
250
|
+
hardFailed: true,
|
|
288
251
|
noteInjected,
|
|
289
252
|
},
|
|
290
253
|
};
|
|
@@ -298,7 +261,7 @@ function applyBudget({ sections, budget, options = {} }) {
|
|
|
298
261
|
omitted,
|
|
299
262
|
projectMdShrunk,
|
|
300
263
|
planTruncationPct,
|
|
301
|
-
hardFailed,
|
|
264
|
+
hardFailed: false,
|
|
302
265
|
noteInjected,
|
|
303
266
|
},
|
|
304
267
|
};
|