mandrel 1.58.0 → 1.60.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/README.md +100 -98
- package/.agents/docs/SDLC.md +140 -141
- package/.agents/docs/configuration.md +16 -16
- package/.agents/docs/workflows.md +7 -8
- package/.agents/instructions.md +12 -11
- package/.agents/personas/architect.md +1 -1
- package/.agents/personas/product.md +1 -1
- package/.agents/personas/project-manager.md +14 -14
- package/.agents/personas/technical-writer.md +1 -1
- package/.agents/rules/changelog-style.md +5 -5
- package/.agents/rules/git-conventions.md +3 -3
- package/.agents/schemas/agentrc.schema.json +3 -3
- package/.agents/schemas/audit-rules.json +20 -0
- package/.agents/schemas/dispatch-manifest.json +4 -4
- package/.agents/schemas/epic-spec.schema.json +15 -45
- package/.agents/schemas/lifecycle/README.md +1 -1
- package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +1 -1
- package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +1 -1
- package/.agents/schemas/lifecycle/story.heartbeat.schema.json +1 -1
- package/.agents/schemas/validation-evidence.schema.json +1 -1
- package/.agents/scripts/README.md +1 -1
- package/.agents/scripts/acceptance-eval.js +21 -4
- package/.agents/scripts/acceptance-spec-reconciler.js +2 -2
- package/.agents/scripts/analyze-execution.js +2 -2
- package/.agents/scripts/assert-branch.js +1 -3
- package/.agents/scripts/audit-to-stories.js +1 -1
- package/.agents/scripts/bootstrap.js +1 -1
- package/.agents/scripts/check-arch-cycles.js +360 -0
- package/.agents/scripts/check-doc-links.js +2 -3
- package/.agents/scripts/coverage-capture.js +24 -3
- package/.agents/scripts/diagnose-friction.js +1 -1
- package/.agents/scripts/dispatcher.js +2 -2
- package/.agents/scripts/drain-pending-cleanup.js +1 -1
- package/.agents/scripts/epic-audit-prepare.js +3 -3
- package/.agents/scripts/epic-deliver-note-intervention.js +2 -2
- package/.agents/scripts/epic-deliver-preflight.js +11 -9
- package/.agents/scripts/epic-deliver-prepare.js +13 -5
- package/.agents/scripts/epic-execute-record-wave.js +5 -5
- package/.agents/scripts/epic-plan-healthcheck.js +6 -10
- package/.agents/scripts/epic-plan-spec-validate.js +1 -1
- package/.agents/scripts/epic-reconcile.js +11 -29
- package/.agents/scripts/evidence-gate.js +2 -2
- package/.agents/scripts/generate-workflows-doc.js +1 -1
- package/.agents/scripts/git-rebase-and-resolve.js +1 -1
- package/.agents/scripts/hierarchy-gate.js +40 -24
- package/.agents/scripts/lib/ITicketingProvider.js +1 -1
- package/.agents/scripts/lib/audit-suite/selector.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/seed-epic-from-findings.js +2 -2
- package/.agents/scripts/lib/baseline-snapshot.js +7 -7
- package/.agents/scripts/lib/baselines/kinds/coverage.js +33 -149
- package/.agents/scripts/lib/baselines/kinds/duplication.js +27 -116
- package/.agents/scripts/lib/baselines/kinds/kind-factory.js +192 -0
- package/.agents/scripts/lib/baselines/kinds/lighthouse.js +34 -133
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +31 -124
- package/.agents/scripts/lib/baselines/kinds/mutation.js +25 -111
- package/.agents/scripts/lib/baselines/maintainability-baseline-io.js +59 -0
- package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +37 -0
- package/.agents/scripts/lib/baselines/writer.js +1 -1
- package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
- package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
- package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +1 -1
- package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +1 -1
- package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
- package/.agents/scripts/lib/close-validation/commands.js +188 -0
- package/.agents/scripts/lib/close-validation/gates.js +235 -0
- package/.agents/scripts/lib/close-validation/process.js +101 -0
- package/.agents/scripts/lib/close-validation/projections/maintainability.js +1 -1
- package/.agents/scripts/lib/close-validation/runner.js +325 -0
- package/.agents/scripts/lib/close-validation/telemetry.js +70 -0
- package/.agents/scripts/lib/codebase-snapshot.js +1 -1
- package/.agents/scripts/lib/config/explain.js +1 -1
- package/.agents/scripts/lib/config/quality.js +6 -6
- package/.agents/scripts/lib/config/runners.js +2 -2
- package/.agents/scripts/lib/config/runtime.js +1 -1
- package/.agents/scripts/lib/config/temp-paths.js +2 -2
- package/.agents/scripts/lib/config-resolver.js +2 -5
- package/.agents/scripts/lib/config-settings-schema-delivery.js +2 -2
- package/.agents/scripts/lib/config-settings-schema-quality.js +1 -1
- package/.agents/scripts/lib/config-settings-schema.js +3 -3
- package/.agents/scripts/lib/coverage-capture.js +147 -4
- package/.agents/scripts/lib/cpu-pool.js +14 -0
- package/.agents/scripts/lib/crap-utils.js +6 -11
- package/.agents/scripts/lib/duplicate-search.js +1 -1
- package/.agents/scripts/lib/dynamic-workflow/capability.js +1 -1
- package/.agents/scripts/lib/dynamic-workflow/documentation-report-contract.js +87 -0
- package/.agents/scripts/lib/epic-plan-clarity.js +1 -1
- package/.agents/scripts/lib/epic-plan-ideation.js +1 -1
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +1 -1
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +1 -1
- package/.agents/scripts/lib/findings/classify-finding.js +1 -1
- package/.agents/scripts/lib/findings/promote-finding.js +10 -10
- package/.agents/scripts/lib/git-utils.js +24 -22
- package/.agents/scripts/lib/label-constants.js +3 -4
- package/.agents/scripts/lib/label-taxonomy.js +3 -8
- package/.agents/scripts/lib/maintainability-engine.js +1 -1
- package/.agents/scripts/lib/maintainability-utils.js +4 -187
- package/.agents/scripts/lib/observability/perf-report-readers.js +32 -23
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +81 -7
- package/.agents/scripts/lib/orchestration/code-review.js +95 -82
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +8 -9
- package/.agents/scripts/lib/orchestration/dependency-analyzer.js +3 -3
- package/.agents/scripts/lib/orchestration/detectors-phase.js +2 -2
- package/.agents/scripts/lib/orchestration/dispatch-engine.js +30 -38
- package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +14 -37
- package/.agents/scripts/lib/orchestration/epic-cleanup.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +22 -22
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +7 -21
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +3 -3
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +206 -58
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +27 -3
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +28 -8
- package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-run-state-store.js +3 -3
- package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +4 -4
- package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +3 -3
- package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +13 -41
- package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +7 -7
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +2 -3
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +2 -8
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +4 -4
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +103 -0
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +22 -64
- package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +38 -76
- package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +4 -4
- package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +10 -10
- package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +8 -20
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +7 -15
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +72 -41
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +2 -4
- package/.agents/scripts/lib/orchestration/file-assumptions.js +6 -5
- package/.agents/scripts/lib/orchestration/finalize/close-planning-tickets.js +1 -1
- package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +2 -2
- package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +1 -1
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +144 -0
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +3 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +8 -8
- package/.agents/scripts/lib/orchestration/manifest-builder.js +5 -5
- package/.agents/scripts/lib/orchestration/parked-follow-ons.js +2 -2
- package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +5 -5
- package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +3 -3
- package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +3 -3
- package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +7 -7
- package/.agents/scripts/lib/orchestration/preflight-cache.js +36 -13
- package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +1 -1
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +1 -1
- package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +2 -2
- package/.agents/scripts/lib/orchestration/retro-runner.js +3 -3
- package/.agents/scripts/lib/orchestration/review-depth.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +5 -60
- package/.agents/scripts/lib/orchestration/review-providers/native.js +7 -6
- package/.agents/scripts/lib/orchestration/review-providers/parse-findings.js +105 -0
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +7 -59
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +2 -4
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -4
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +32 -35
- package/.agents/scripts/lib/orchestration/skill-capsule-loader.js +1 -2
- package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
- package/.agents/scripts/lib/orchestration/spec-renderer.js +36 -73
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +451 -503
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +8 -2
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +47 -2
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +2 -2
- package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +358 -54
- package/.agents/scripts/lib/orchestration/story-close/phases/close.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +3 -2
- package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +32 -5
- package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +5 -18
- package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +3 -3
- package/.agents/scripts/lib/orchestration/story-close-recovery.js +33 -16
- package/.agents/scripts/lib/orchestration/story-reachability.js +47 -0
- package/.agents/scripts/lib/orchestration/task-body-validator.js +6 -6
- package/.agents/scripts/lib/orchestration/ticket-lease.js +1 -1
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +4 -35
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -10
- package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -70
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +44 -73
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +16 -7
- package/.agents/scripts/lib/orchestration/ticketing/state.js +53 -439
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +471 -0
- package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
- package/.agents/scripts/lib/orchestration/wave-record-notifications.js +3 -3
- package/.agents/scripts/lib/orchestration/wave-record-projection.js +2 -8
- package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
- package/.agents/scripts/lib/preflight-runner.js +1 -1
- package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +4 -5
- package/.agents/scripts/lib/presentation/manifest-builder.js +28 -34
- package/.agents/scripts/lib/presentation/manifest-formatter.js +3 -4
- package/.agents/scripts/lib/presentation/manifest-helpers.js +1 -1
- package/.agents/scripts/lib/presentation/manifest-procedures.js +4 -4
- package/.agents/scripts/lib/presentation/manifest-render-waves.js +4 -23
- package/.agents/scripts/lib/presentation/manifest-renderer.js +1 -1
- package/.agents/scripts/lib/presentation/manifest-story-views.js +2 -11
- package/.agents/scripts/lib/project-root.js +17 -0
- package/.agents/scripts/lib/signals/schema.js +1 -1
- package/.agents/scripts/lib/spec/index.js +1 -1
- package/.agents/scripts/lib/spec/loader.js +2 -2
- package/.agents/scripts/lib/spec/state.js +7 -16
- package/.agents/scripts/lib/story-adjacency.js +76 -0
- package/.agents/scripts/lib/story-init/context-resolver.js +3 -3
- package/.agents/scripts/lib/story-init/state-transitioner.js +2 -2
- package/.agents/scripts/lib/story-init/task-graph-builder.js +7 -7
- package/.agents/scripts/lib/story-lifecycle.js +9 -9
- package/.agents/scripts/lib/story-plan.js +1 -1
- package/.agents/scripts/lib/templates/decomposer-prompts.js +59 -52
- package/.agents/scripts/lib/transpile.js +93 -0
- package/.agents/scripts/lib/wave-runner/tick.js +4 -153
- package/.agents/scripts/lib/workers/crap-worker.js +1 -1
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +1 -1
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +20 -2
- package/.agents/scripts/lib/worktree/lifecycle/force-drain.js +90 -0
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +26 -8
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +74 -0
- package/.agents/scripts/lifecycle-emit-story-dispatch.js +1 -1
- package/.agents/scripts/lifecycle-emit.js +1 -1
- package/.agents/scripts/providers/github/board-add.js +1 -1
- package/.agents/scripts/providers/github/errors.js +1 -1
- package/.agents/scripts/providers/github/mappers.js +2 -2
- package/.agents/scripts/providers/github/tickets.js +114 -10
- package/.agents/scripts/resync-status-column.js +1 -1
- package/.agents/scripts/retro-run.js +2 -2
- package/.agents/scripts/run-lint.js +10 -1
- package/.agents/scripts/run-tests.js +24 -4
- package/.agents/scripts/single-story-init.js +1 -1
- package/.agents/scripts/stories-wave-tick.js +13 -10
- package/.agents/scripts/story-close.js +1 -1
- package/.agents/scripts/story-init.js +162 -26
- package/.agents/scripts/story-phase.js +5 -5
- package/.agents/scripts/story-plan.js +3 -3
- package/.agents/scripts/sync-branch-from-base.js +2 -2
- package/.agents/scripts/validate-docs-freshness.js +1 -1
- package/.agents/scripts/wave-tick.js +1 -1
- package/.agents/skills/core/analyze-execution/SKILL.md +2 -2
- package/.agents/skills/core/epic-plan-consolidate/SKILL.md +21 -26
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +23 -56
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +4 -4
- package/.agents/skills/core/hydrate-context/SKILL.md +2 -2
- package/.agents/skills/core/idea-refinement/SKILL.md +4 -4
- package/.agents/skills/core/knowledge-transfer/SKILL.md +2 -2
- package/.agents/skills/core/planning-and-task-breakdown/SKILL.md +1 -1
- package/.agents/skills/core/scope-triage/SKILL.md +9 -10
- package/.agents/skills/core/using-agent-skills/SKILL.md +1 -1
- package/.agents/skills/skills.index.json +7 -7
- package/.agents/skills/stack/qa/lighthouse-baseline/SKILL.md +1 -1
- package/.agents/templates/agent-protocol.md +2 -2
- package/.agents/workflows/agents-update.md +2 -2
- package/.agents/workflows/audit-architecture.md +2 -2
- package/.agents/workflows/audit-clean-code.md +2 -2
- package/.agents/workflows/audit-dependencies.md +1 -1
- package/.agents/workflows/audit-devops.md +1 -1
- package/.agents/workflows/audit-documentation.md +226 -0
- package/.agents/workflows/audit-lighthouse.md +1 -1
- package/.agents/workflows/audit-performance.md +2 -2
- package/.agents/workflows/audit-privacy.md +1 -1
- package/.agents/workflows/audit-quality.md +2 -2
- package/.agents/workflows/audit-security.md +2 -2
- package/.agents/workflows/audit-seo.md +1 -1
- package/.agents/workflows/audit-sre.md +1 -1
- package/.agents/workflows/audit-to-stories.md +10 -10
- package/.agents/workflows/audit-ux-ui.md +1 -1
- package/.agents/workflows/deliver.md +85 -0
- package/.agents/workflows/explain.md +3 -3
- package/.agents/workflows/git-merge-pr.md +1 -1
- package/.agents/workflows/git-pr-all.md +13 -10
- package/.agents/workflows/git-push.md +6 -3
- package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
- package/.agents/workflows/helpers/acceptance-self-eval.md +1 -1
- package/.agents/workflows/helpers/code-review.md +5 -5
- package/.agents/workflows/{epic-deliver.md → helpers/deliver-epic.md} +59 -66
- package/.agents/workflows/{story-deliver.md → helpers/deliver-stories.md} +25 -25
- package/.agents/workflows/helpers/diagnose.md +1 -1
- package/.agents/workflows/helpers/epic-audit.md +6 -6
- package/.agents/workflows/helpers/epic-deliver-story.md +28 -39
- package/.agents/workflows/helpers/epic-plan-decompose.md +23 -23
- package/.agents/workflows/helpers/epic-plan-spec.md +6 -6
- package/.agents/workflows/helpers/epic-testing.md +3 -3
- package/.agents/workflows/helpers/parallel-tooling.md +1 -1
- package/.agents/workflows/{epic-plan.md → helpers/plan-epic.md} +84 -84
- package/.agents/workflows/{story-plan.md → helpers/plan-story.md} +43 -43
- package/.agents/workflows/helpers/signals.md +1 -1
- package/.agents/workflows/helpers/single-story-deliver.md +12 -11
- package/.agents/workflows/helpers/worktree-lifecycle.md +18 -18
- package/.agents/workflows/onboard.md +21 -20
- package/.agents/workflows/plan.md +89 -0
- package/.agents/workflows/qa-explore.md +1 -1
- package/.agents/workflows/qa-run-harness.md +1 -1
- package/README.md +17 -20
- package/docs/CHANGELOG.md +1149 -0
- package/lib/cli/__tests__/update-changelog-surface.test.js +357 -0
- package/lib/cli/__tests__/update-reexec.test.js +513 -0
- package/lib/cli/init.js +338 -0
- package/lib/cli/update.js +413 -52
- package/package.json +3 -1
- package/.agents/scripts/lib/auto-refresh-baselines.js +0 -308
- package/.agents/scripts/lib/close-validation.js +0 -897
- package/.agents/scripts/lib/orchestration/cascade-grouping.js +0 -275
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter.js +0 -69
- package/.agents/scripts/lib/orchestration/reconciler.js +0 -137
- package/.agents/scripts/lib/orchestration/story-close/format-autofix-scoped.js +0 -221
- package/.agents/scripts/lib/orchestration/story-close/format-autofix-shared.js +0 -123
- package/.agents/scripts/lib/task-utils.js +0 -26
- package/.agents/scripts/story-deliver-prepare.js +0 -267
|
@@ -118,10 +118,89 @@ export function findHoldersInPath(wtPath, opts = {}) {
|
|
|
118
118
|
}));
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Pure: compute the set of pids that must never be killed — `selfPid` plus
|
|
123
|
+
* its full ancestor chain — from a process table of `{ pid, ppid }` rows.
|
|
124
|
+
* Cycle-guarded (a corrupt/raced table cannot loop forever). `selfPid` is
|
|
125
|
+
* always included even when the table is empty or missing its row, so the
|
|
126
|
+
* guard fails safe.
|
|
127
|
+
*
|
|
128
|
+
* @param {number} selfPid
|
|
129
|
+
* @param {Array<{ pid: number, ppid?: number }>} table
|
|
130
|
+
* @returns {Set<number>}
|
|
131
|
+
*/
|
|
132
|
+
export function computeProtectedPids(selfPid, table) {
|
|
133
|
+
const protectedPids = new Set([selfPid]);
|
|
134
|
+
if (!Array.isArray(table) || table.length === 0) return protectedPids;
|
|
135
|
+
const parentOf = new Map();
|
|
136
|
+
for (const row of table) {
|
|
137
|
+
if (row && typeof row.pid === 'number' && typeof row.ppid === 'number') {
|
|
138
|
+
parentOf.set(row.pid, row.ppid);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
let cursor = selfPid;
|
|
142
|
+
while (parentOf.has(cursor)) {
|
|
143
|
+
const ppid = parentOf.get(cursor);
|
|
144
|
+
if (protectedPids.has(ppid)) break; // cycle guard
|
|
145
|
+
protectedPids.add(ppid);
|
|
146
|
+
cursor = ppid;
|
|
147
|
+
}
|
|
148
|
+
return protectedPids;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Enumerate the full Windows process table as `{ pid, ppid }` rows so the
|
|
153
|
+
* kill set can exclude the invoking shell / orchestrator ancestry.
|
|
154
|
+
* Best-effort: any failure returns `[]` (callers still protect `selfPid`).
|
|
155
|
+
*
|
|
156
|
+
* @param {object} [opts]
|
|
157
|
+
* @param {Function} [opts.spawn] Injection point for tests (default `spawnSync`).
|
|
158
|
+
* @param {string} [opts.platform] Override `process.platform` for tests.
|
|
159
|
+
* @returns {Array<{ pid: number, ppid: number }>}
|
|
160
|
+
*/
|
|
161
|
+
export function fetchProcessTable(opts = {}) {
|
|
162
|
+
const spawn = opts.spawn ?? spawnSync;
|
|
163
|
+
const platform = opts.platform ?? process.platform;
|
|
164
|
+
if (platform !== 'win32') return [];
|
|
165
|
+
|
|
166
|
+
const script =
|
|
167
|
+
'Get-CimInstance Win32_Process | Select-Object ProcessId, ParentProcessId | ConvertTo-Json -Compress';
|
|
168
|
+
let res;
|
|
169
|
+
try {
|
|
170
|
+
res = spawn(
|
|
171
|
+
'powershell.exe',
|
|
172
|
+
['-NoProfile', '-NonInteractive', '-Command', script],
|
|
173
|
+
{ encoding: 'utf8', timeout: 15_000 },
|
|
174
|
+
);
|
|
175
|
+
} catch {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
if (!res || res.status !== 0 || !res.stdout) return [];
|
|
179
|
+
let parsed;
|
|
180
|
+
try {
|
|
181
|
+
parsed = JSON.parse(String(res.stdout).trim());
|
|
182
|
+
} catch {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
const list = Array.isArray(parsed) ? parsed : [parsed];
|
|
186
|
+
return list
|
|
187
|
+
.filter((p) => p && typeof p.ProcessId === 'number')
|
|
188
|
+
.map((p) => ({
|
|
189
|
+
pid: p.ProcessId,
|
|
190
|
+
ppid: typeof p.ParentProcessId === 'number' ? p.ParentProcessId : -1,
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
|
|
121
194
|
/**
|
|
122
195
|
* `taskkill /T /F /PID <pid>` for each holder. Returns the pids reported
|
|
123
196
|
* as terminated. Per-pid failures are logged but do not throw — caller
|
|
124
197
|
* decides whether the partial kill is enough to retry.
|
|
198
|
+
*
|
|
199
|
+
* Self-preservation (Story #4018): holders are matched by command-line
|
|
200
|
+
* substring, which can select the invoking shell or the orchestrator's own
|
|
201
|
+
* ancestor chain (any ancestor whose command line mentions the worktree
|
|
202
|
+
* path). Before any `taskkill /T /F`, the kill set excludes `selfPid` and
|
|
203
|
+
* its full ancestor chain — `/T` on an ancestor would kill this process too.
|
|
125
204
|
*/
|
|
126
205
|
export function terminateHolders(holders, opts = {}) {
|
|
127
206
|
const spawn = opts.spawn ?? spawnSync;
|
|
@@ -130,9 +209,20 @@ export function terminateHolders(holders, opts = {}) {
|
|
|
130
209
|
if (platform !== 'win32') return [];
|
|
131
210
|
if (!Array.isArray(holders) || holders.length === 0) return [];
|
|
132
211
|
|
|
212
|
+
const selfPid = opts.selfPid ?? process.pid;
|
|
213
|
+
const protectedPids =
|
|
214
|
+
opts.protectedPids ??
|
|
215
|
+
computeProtectedPids(selfPid, fetchProcessTable({ spawn, platform }));
|
|
216
|
+
|
|
133
217
|
const killed = [];
|
|
134
218
|
for (const h of holders) {
|
|
135
219
|
if (!h || typeof h.pid !== 'number') continue;
|
|
220
|
+
if (protectedPids.has(h.pid)) {
|
|
221
|
+
logger.warn(
|
|
222
|
+
`force-drain: skipping pid=${h.pid} name=${h.name ?? '?'} — self/ancestor of this process (never killed)`,
|
|
223
|
+
);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
136
226
|
let res;
|
|
137
227
|
try {
|
|
138
228
|
res = spawn('taskkill.exe', ['/T', '/F', '/PID', String(h.pid)], {
|
|
@@ -134,7 +134,7 @@ async function fsRmWithRetry(
|
|
|
134
134
|
return { success: true, attempts: attempt };
|
|
135
135
|
} catch (err) {
|
|
136
136
|
lastErr = err;
|
|
137
|
-
if (attempt < maxRetries) {
|
|
137
|
+
if (attempt < maxRetries && retryDelay > 0) {
|
|
138
138
|
await new Promise((r) => setTimeout(r, retryDelay));
|
|
139
139
|
}
|
|
140
140
|
}
|
|
@@ -160,8 +160,8 @@ function handleRemoveFailure(
|
|
|
160
160
|
classification,
|
|
161
161
|
attempt,
|
|
162
162
|
maxAttempts,
|
|
163
|
+
{ retryDelaysMs = [0, 150, 350, 700, 1200, 2000], sleepFn = sleepSync } = {},
|
|
163
164
|
) {
|
|
164
|
-
const retryDelaysMs = [0, 150, 350, 700, 1200, 2000];
|
|
165
165
|
const { isLockLike, isCwdLike } = classification;
|
|
166
166
|
if ((isLockLike || isCwdLike) && attempt < maxAttempts) {
|
|
167
167
|
const delay = retryDelaysMs[attempt] ?? 300;
|
|
@@ -169,13 +169,13 @@ function handleRemoveFailure(
|
|
|
169
169
|
ctx.logger.warn(
|
|
170
170
|
`worktree.reap remove hit ${reasonClass} error; retrying in ${delay}ms (${attempt}/${maxAttempts})`,
|
|
171
171
|
);
|
|
172
|
-
|
|
172
|
+
sleepFn(delay);
|
|
173
173
|
return 'continue';
|
|
174
174
|
}
|
|
175
175
|
return 'break';
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
async function runGitWorktreeRemoveLoop(ctx, wtPath) {
|
|
178
|
+
async function runGitWorktreeRemoveLoop(ctx, wtPath, retryOpts = {}) {
|
|
179
179
|
const maxAttempts = ctx.platform === 'win32' ? 6 : 2;
|
|
180
180
|
let lastReason = 'worktree-remove-failed';
|
|
181
181
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
@@ -193,6 +193,7 @@ async function runGitWorktreeRemoveLoop(ctx, wtPath) {
|
|
|
193
193
|
classification,
|
|
194
194
|
attempt,
|
|
195
195
|
maxAttempts,
|
|
196
|
+
retryOpts,
|
|
196
197
|
);
|
|
197
198
|
if (action === 'break') break;
|
|
198
199
|
}
|
|
@@ -205,6 +206,7 @@ function tryForceRemoveFallback(
|
|
|
205
206
|
lastReason,
|
|
206
207
|
forceRemoveBackoffMs,
|
|
207
208
|
maxAttempts,
|
|
209
|
+
sleepFn = sleepSync,
|
|
208
210
|
) {
|
|
209
211
|
if (!(WINDOWS_LOCK_RE.test(lastReason) || WINDOWS_CWD_RE.test(lastReason))) {
|
|
210
212
|
return { handled: false, lastReason };
|
|
@@ -212,7 +214,7 @@ function tryForceRemoveFallback(
|
|
|
212
214
|
ctx.logger.warn(
|
|
213
215
|
`worktree.reap remove exhausted Windows lock retry; retrying with --force in ${forceRemoveBackoffMs}ms path=${wtPath}`,
|
|
214
216
|
);
|
|
215
|
-
|
|
217
|
+
sleepFn(forceRemoveBackoffMs);
|
|
216
218
|
const forced = ctx.git.gitSpawn(
|
|
217
219
|
ctx.repoRoot,
|
|
218
220
|
'worktree',
|
|
@@ -248,8 +250,9 @@ async function tryStage15WindowsFsRm({
|
|
|
248
250
|
push,
|
|
249
251
|
lastReason,
|
|
250
252
|
priorAttempts,
|
|
253
|
+
sleepFn = sleepSync,
|
|
251
254
|
}) {
|
|
252
|
-
|
|
255
|
+
sleepFn(forceRemoveBackoffMs);
|
|
253
256
|
try {
|
|
254
257
|
await fsRm(wtPath, {
|
|
255
258
|
recursive: true,
|
|
@@ -304,6 +307,7 @@ async function handleFsRmFailure({
|
|
|
304
307
|
lastReason,
|
|
305
308
|
forceRemoveBackoffMs,
|
|
306
309
|
fsRm,
|
|
310
|
+
sleepFn,
|
|
307
311
|
}) {
|
|
308
312
|
// Stage 1.5 — coverage-leak quiesce + extended fs.rm budget (Windows only).
|
|
309
313
|
if (ctx.platform === 'win32') {
|
|
@@ -316,6 +320,7 @@ async function handleFsRmFailure({
|
|
|
316
320
|
push,
|
|
317
321
|
lastReason,
|
|
318
322
|
priorAttempts: rmResult.attempts,
|
|
323
|
+
sleepFn,
|
|
319
324
|
});
|
|
320
325
|
if (stage15) return stage15;
|
|
321
326
|
}
|
|
@@ -347,7 +352,12 @@ async function handleFsRmFailure({
|
|
|
347
352
|
export async function removeWorktreeWithRecovery(ctx, wtPath, opts = {}) {
|
|
348
353
|
const { storyId = null, branch = null, push = false } = opts;
|
|
349
354
|
const forceRemoveBackoffMs = opts.forceRemoveBackoffMs ?? 3000;
|
|
350
|
-
const
|
|
355
|
+
const retryDelay = opts.retryDelay ?? 200;
|
|
356
|
+
const { retryDelaysMs, sleepFn } = opts;
|
|
357
|
+
const removeLoop = await runGitWorktreeRemoveLoop(ctx, wtPath, {
|
|
358
|
+
...(retryDelaysMs ? { retryDelaysMs } : {}),
|
|
359
|
+
...(sleepFn ? { sleepFn } : {}),
|
|
360
|
+
});
|
|
351
361
|
if (removeLoop.removed) return { removed: true };
|
|
352
362
|
let { lastReason } = removeLoop;
|
|
353
363
|
if (ctx.platform === 'win32' && opts.forceRemoveFallback !== false) {
|
|
@@ -357,6 +367,7 @@ export async function removeWorktreeWithRecovery(ctx, wtPath, opts = {}) {
|
|
|
357
367
|
lastReason,
|
|
358
368
|
forceRemoveBackoffMs,
|
|
359
369
|
removeLoop.maxAttempts,
|
|
370
|
+
sleepFn,
|
|
360
371
|
);
|
|
361
372
|
if (fallback.handled) return fallback.result;
|
|
362
373
|
lastReason = fallback.lastReason;
|
|
@@ -367,7 +378,7 @@ export async function removeWorktreeWithRecovery(ctx, wtPath, opts = {}) {
|
|
|
367
378
|
const fsRm = ctx.fsRm ?? fsPromisesRm;
|
|
368
379
|
const rmResult = await fsRmWithRetry(fsRm, wtPath, {
|
|
369
380
|
maxRetries: 5,
|
|
370
|
-
retryDelay
|
|
381
|
+
retryDelay,
|
|
371
382
|
});
|
|
372
383
|
if (!rmResult.success) {
|
|
373
384
|
return handleFsRmFailure({
|
|
@@ -380,6 +391,7 @@ export async function removeWorktreeWithRecovery(ctx, wtPath, opts = {}) {
|
|
|
380
391
|
lastReason,
|
|
381
392
|
forceRemoveBackoffMs,
|
|
382
393
|
fsRm,
|
|
394
|
+
sleepFn,
|
|
383
395
|
});
|
|
384
396
|
}
|
|
385
397
|
finalizeGitWorktreeRemove(ctx);
|
|
@@ -571,6 +583,12 @@ export async function reap(ctx, storyId, opts = {}) {
|
|
|
571
583
|
storyId: storyIdN,
|
|
572
584
|
branch,
|
|
573
585
|
push: opts.push === true,
|
|
586
|
+
...(opts.retryDelaysMs ? { retryDelaysMs: opts.retryDelaysMs } : {}),
|
|
587
|
+
...(opts.retryDelay !== undefined ? { retryDelay: opts.retryDelay } : {}),
|
|
588
|
+
...(opts.sleepFn ? { sleepFn: opts.sleepFn } : {}),
|
|
589
|
+
...(opts.forceRemoveBackoffMs !== undefined
|
|
590
|
+
? { forceRemoveBackoffMs: opts.forceRemoveBackoffMs }
|
|
591
|
+
: {}),
|
|
574
592
|
});
|
|
575
593
|
if (!removeResult.removed) {
|
|
576
594
|
return {
|
|
@@ -119,6 +119,80 @@ export function selectInstallCommand(strategy, wtPath, fsLike = fs) {
|
|
|
119
119
|
return { cmd: 'npm', args: ['ci'] };
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Per-package-manager "install completed" marker files written into
|
|
124
|
+
* `node_modules/` by the install command itself. Their presence (and
|
|
125
|
+
* freshness relative to the lockfile) is the cheapest reliable signal that a
|
|
126
|
+
* prior install ran to completion — a failed/interrupted install leaves
|
|
127
|
+
* `node_modules` partially populated without (or with a stale) marker.
|
|
128
|
+
*/
|
|
129
|
+
const INSTALL_MARKERS = [
|
|
130
|
+
'.package-lock.json', // npm ci / npm install
|
|
131
|
+
'.modules.yaml', // pnpm
|
|
132
|
+
'.yarn-state.yml', // yarn berry (node-modules linker)
|
|
133
|
+
'.yarn-integrity', // yarn classic
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const LOCKFILES = ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock'];
|
|
137
|
+
|
|
138
|
+
function safeMtimeMs(fsLike, p) {
|
|
139
|
+
try {
|
|
140
|
+
return fsLike.statSync(p).mtimeMs;
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Pure: probe whether a **reused** worktree already carries a completed,
|
|
148
|
+
* up-to-date install. Worktree reuse must not blindly report
|
|
149
|
+
* `skipped/worktree-reused` — when the prior run's install *failed*, that
|
|
150
|
+
* status defeats the install retry exactly when it matters
|
|
151
|
+
* (`deriveInstallAction('skipped')` treats it as "nothing to do").
|
|
152
|
+
*
|
|
153
|
+
* Returns the same shape as `installDependencies`:
|
|
154
|
+
* - `{ status: 'skipped', reason: 'worktree-reused' }` — a completed
|
|
155
|
+
* install was detected (or the strategy never installs per-tree);
|
|
156
|
+
* safe to skip.
|
|
157
|
+
* - `{ status: 'failed', reason }` — missing/incomplete/stale install
|
|
158
|
+
* detected; callers should retry the install.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} strategy One of `per-worktree | pnpm-store | symlink`.
|
|
161
|
+
* @param {string} wtPath Absolute worktree path.
|
|
162
|
+
* @param {{ existsSync: Function, statSync: Function }} [fsLike] Injectable for tests.
|
|
163
|
+
* @returns {{ status: 'skipped' | 'failed', reason: string }}
|
|
164
|
+
*/
|
|
165
|
+
export function probeReusedInstall(strategy, wtPath, fsLike = fs) {
|
|
166
|
+
// `symlink` re-points node_modules at a donor — no per-tree install to probe.
|
|
167
|
+
if (strategy === 'symlink') {
|
|
168
|
+
return { status: 'skipped', reason: 'worktree-reused' };
|
|
169
|
+
}
|
|
170
|
+
if (!fsLike.existsSync(path.join(wtPath, 'package.json'))) {
|
|
171
|
+
return { status: 'skipped', reason: 'no-package-json' };
|
|
172
|
+
}
|
|
173
|
+
const nmPath = path.join(wtPath, 'node_modules');
|
|
174
|
+
if (!fsLike.existsSync(nmPath)) {
|
|
175
|
+
return { status: 'failed', reason: 'reuse-node-modules-missing' };
|
|
176
|
+
}
|
|
177
|
+
const marker = INSTALL_MARKERS.map((m) => path.join(nmPath, m)).find((p) =>
|
|
178
|
+
fsLike.existsSync(p),
|
|
179
|
+
);
|
|
180
|
+
if (!marker) {
|
|
181
|
+
return { status: 'failed', reason: 'reuse-install-incomplete' };
|
|
182
|
+
}
|
|
183
|
+
const markerMtime = safeMtimeMs(fsLike, marker);
|
|
184
|
+
const lockfile = LOCKFILES.map((l) => path.join(wtPath, l)).find((p) =>
|
|
185
|
+
fsLike.existsSync(p),
|
|
186
|
+
);
|
|
187
|
+
if (lockfile && markerMtime !== null) {
|
|
188
|
+
const lockMtime = safeMtimeMs(fsLike, lockfile);
|
|
189
|
+
if (lockMtime !== null && lockMtime > markerMtime) {
|
|
190
|
+
return { status: 'failed', reason: 'reuse-node-modules-stale' };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { status: 'skipped', reason: 'worktree-reused' };
|
|
194
|
+
}
|
|
195
|
+
|
|
122
196
|
/** Pure: retry policy keyed off the chosen command. pnpm gets 3× + 5min. */
|
|
123
197
|
export function installRetryPolicy(cmd) {
|
|
124
198
|
const isPnpm = cmd === 'pnpm';
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Thin host-loop CLI that appends a single `story.dispatch.start`
|
|
7
7
|
* NDJSON record to `temp/epic-<id>/lifecycle.ndjson`. Invoked by
|
|
8
|
-
* `/
|
|
8
|
+
* `/deliver` Phase 2 immediately BEFORE each per-Story Agent
|
|
9
9
|
* tool call so the lifecycle ledger durably records every dispatch
|
|
10
10
|
* attempt. The `wave-tick.js` reconciler (Story #2891 Task #2901)
|
|
11
11
|
* then derives `nextAction['in-flight']` from this ledger to surface
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Replaces the three single-purpose emit shims
|
|
8
8
|
* (`epic-deliver-finalize.js`, `epic-deliver-automerge.js`,
|
|
9
|
-
* `epic-deliver-cleanup.js`) that the `/
|
|
9
|
+
* `epic-deliver-cleanup.js`) that the `/deliver` workflow markdown
|
|
10
10
|
* invoked in Phase 6, 7.5, and 8. Those shims each did exactly one
|
|
11
11
|
* thing: construct a bus and emit one event. Collapsing them into a
|
|
12
12
|
* single argv-driven CLI lets the workflow stay declarative ("fire
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Story #3822 — single source of truth for the post-create board-add
|
|
5
5
|
* step. Issues created through any create path (`createTicket`,
|
|
6
|
-
* `createIssue` — which backs `/
|
|
6
|
+
* `createIssue` — which backs `/plan` persist and the `/plan`
|
|
7
7
|
* Phase 4 Epic open) must land on the configured Projects V2 board
|
|
8
8
|
* without relying on GitHub's "Auto-add to project" built-in workflow,
|
|
9
9
|
* which is off by default on fresh boards and cannot be enabled via API.
|
|
@@ -109,7 +109,7 @@ export function classifyGithubError(err) {
|
|
|
109
109
|
// callers (paginateRest, getTicket, getNativeSubIssues, …) absorb the same
|
|
110
110
|
// jittered exponential backoff on transient GitHub errors instead of
|
|
111
111
|
// bubbling a one-shot 502/429/ECONNRESET that kills a longer pipeline
|
|
112
|
-
// (e.g. the /
|
|
112
|
+
// (e.g. the /deliver Phase E retro).
|
|
113
113
|
|
|
114
114
|
export const TRANSIENT_RETRY_DEFAULTS = Object.freeze({
|
|
115
115
|
maxAttempts: 6,
|
|
@@ -57,7 +57,7 @@ export function issueToEpic(issue) {
|
|
|
57
57
|
export function subIssueNodeToTicket(node) {
|
|
58
58
|
// Story #3097 (Wave-0 additive, Epic #3078 Strategy B) — return `null`
|
|
59
59
|
// for absent sub-issue nodes instead of dereferencing properties on
|
|
60
|
-
// `null`/`undefined`. In
|
|
60
|
+
// `null`/`undefined`. In 2-tier mode a Story can legitimately have zero
|
|
61
61
|
// Task children, which surfaces as an empty / missing sub-issue node
|
|
62
62
|
// when callers iterate the GraphQL response and pass each entry through
|
|
63
63
|
// this mapper. The legacy 4-tier path also benefits — a transient
|
|
@@ -83,7 +83,7 @@ export function subIssueNodeToTicket(node) {
|
|
|
83
83
|
* any null/undefined entries. Story #3097 (Wave-0 additive, Epic #3078
|
|
84
84
|
* Strategy B) — gives callers a single Storyless-tolerant entry point so
|
|
85
85
|
* the existing per-node mappers can stay strict for the 4-tier path while
|
|
86
|
-
* the
|
|
86
|
+
* the 2-tier path (Storyless: a Story with zero child Tasks) gets a
|
|
87
87
|
* well-defined empty-array result.
|
|
88
88
|
*
|
|
89
89
|
* @param {Array<object|null|undefined>|null|undefined} nodes
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { parseBlockedBy, parseBlocks } from '../../lib/dependency-parser.js';
|
|
25
|
+
import { Logger } from '../../lib/Logger.js';
|
|
25
26
|
import { addIssueToBoard } from './board-add.js';
|
|
26
27
|
import { createInlineTicketCache } from './cache.js';
|
|
27
28
|
import { withTransientRetry } from './errors.js';
|
|
@@ -33,8 +34,16 @@ import {
|
|
|
33
34
|
} from './request-helpers.js';
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
37
|
+
* GitHub Search API hard ceiling is 1000 results per query; at
|
|
38
|
+
* `per_page=100` that is 10 pages. An Epic never has anywhere near 1000
|
|
39
|
+
* children, so hitting this cap means the query is degenerate — we stop
|
|
40
|
+
* rather than throw (the regex post-filter keeps results correct).
|
|
41
|
+
*/
|
|
42
|
+
const SEARCH_PAGE_CAP = 10;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Compose the final markdown body for a created ticket. Under the 2-tier
|
|
46
|
+
* hierarchy (Epic → Story), `body` is always a string supplied
|
|
38
47
|
* by the caller (the decomposer, the spec planner, or the reconciler-apply
|
|
39
48
|
* engine). This helper appends the canonical orchestrator footer
|
|
40
49
|
* (`parent: #<n>` / `Epic: #<m>` / `blocked by #<x>`) byte-stable with the
|
|
@@ -100,6 +109,14 @@ export class TicketGateway {
|
|
|
100
109
|
this.repo = repo;
|
|
101
110
|
this._hooks = hooks;
|
|
102
111
|
this._cache = cache ?? createInlineTicketCache();
|
|
112
|
+
/**
|
|
113
|
+
* Per-instance memo of `getTickets(epicId, filters)` results (Story
|
|
114
|
+
* #3988). The planning-state-manager fetches the same child list twice
|
|
115
|
+
* per planning pass; without this memo each fetch re-pays the full
|
|
116
|
+
* search/list round-trip. Invalidated on every write surface.
|
|
117
|
+
* @type {Map<string, object[]>}
|
|
118
|
+
*/
|
|
119
|
+
this._listCache = new Map();
|
|
103
120
|
}
|
|
104
121
|
|
|
105
122
|
/**
|
|
@@ -142,29 +159,113 @@ export class TicketGateway {
|
|
|
142
159
|
}
|
|
143
160
|
|
|
144
161
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
162
|
+
* Run one Search API query (`/search/issues`) to completion, returning
|
|
163
|
+
* the raw issue items. Search responses are `{ total_count, items }`
|
|
164
|
+
* envelopes rather than bare arrays, so this paginates manually instead
|
|
165
|
+
* of going through `paginateRest`.
|
|
166
|
+
*/
|
|
167
|
+
async _searchIssues(query) {
|
|
168
|
+
const items = [];
|
|
169
|
+
for (let page = 1; page <= SEARCH_PAGE_CAP; page++) {
|
|
170
|
+
const params = new URLSearchParams({
|
|
171
|
+
q: query,
|
|
172
|
+
per_page: '100',
|
|
173
|
+
page: String(page),
|
|
174
|
+
});
|
|
175
|
+
const result = await withTransientRetry(
|
|
176
|
+
() =>
|
|
177
|
+
this._gh.api({
|
|
178
|
+
method: 'GET',
|
|
179
|
+
endpoint: `/search/issues?${params}`,
|
|
180
|
+
}),
|
|
181
|
+
{ label: `searchIssues page ${page}`, onRetry: defaultRetryWarn },
|
|
182
|
+
);
|
|
183
|
+
const parsed = parseApiJson(result);
|
|
184
|
+
const batch = Array.isArray(parsed?.items) ? parsed.items : [];
|
|
185
|
+
items.push(...batch);
|
|
186
|
+
if (batch.length < 100) break;
|
|
187
|
+
}
|
|
188
|
+
return items;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Server-side narrowed child lookup (Story #3988): two Search API
|
|
193
|
+
* queries — `"Epic: #N" in:body` and `"parent: #N" in:body` — deduped
|
|
194
|
+
* by issue number. Replaces the repo-wide `state=all` pagination that
|
|
195
|
+
* cost ~1 spawn per 100 repo issues and hard-failed past the
|
|
196
|
+
* `paginateRest` page cap. Search tokenization can over-match (e.g.
|
|
197
|
+
* `#10` vs `#100`), so callers MUST keep the word-boundary regex
|
|
198
|
+
* post-filter.
|
|
199
|
+
*/
|
|
200
|
+
async _searchEpicChildren(epicId, filters) {
|
|
201
|
+
const qualifiers = [`repo:${this.owner}/${this.repo}`, 'is:issue'];
|
|
202
|
+
const state = filters.state ?? 'all';
|
|
203
|
+
if (state === 'open' || state === 'closed') {
|
|
204
|
+
qualifiers.push(`state:${state}`);
|
|
205
|
+
}
|
|
206
|
+
if (filters.label) qualifiers.push(`label:"${filters.label}"`);
|
|
207
|
+
const base = qualifiers.join(' ');
|
|
208
|
+
|
|
209
|
+
const [epicRefs, parentRefs] = await Promise.all([
|
|
210
|
+
this._searchIssues(`${base} "Epic: #${epicId}" in:body`),
|
|
211
|
+
this._searchIssues(`${base} "parent: #${epicId}" in:body`),
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
const byNumber = new Map();
|
|
215
|
+
for (const issue of [...epicRefs, ...parentRefs]) {
|
|
216
|
+
if (!byNumber.has(issue.number)) byNumber.set(issue.number, issue);
|
|
217
|
+
}
|
|
218
|
+
return Array.from(byNumber.values());
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Repo-wide listing fallback — the pre-#3988 shape. Only used when the
|
|
223
|
+
* Search API path fails (search outage, search-specific rate limit).
|
|
147
224
|
*/
|
|
148
225
|
/* node:coverage ignore next */
|
|
149
|
-
async
|
|
226
|
+
async _listAllIssues(filters) {
|
|
150
227
|
const params = new URLSearchParams({ state: filters.state ?? 'all' });
|
|
151
228
|
if (filters.label) params.set('labels', filters.label);
|
|
152
|
-
|
|
153
229
|
const endpoint = `/repos/${this.owner}/${this.repo}/issues?${params}`;
|
|
154
|
-
|
|
230
|
+
return paginateRest(this._gh, endpoint);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @field-manifest /search/issues?q=...: number, id, node_id, title,
|
|
235
|
+
* body, labels, state, pull_request
|
|
236
|
+
* @field-manifest /repos/{owner}/{repo}/issues?state=...&labels=...:
|
|
237
|
+
* number, body, labels, state, pull_request
|
|
238
|
+
*/
|
|
239
|
+
async getTickets(epicId, filters = {}) {
|
|
240
|
+
const memoKey = `${epicId}|${filters.state ?? 'all'}|${filters.label ?? ''}`;
|
|
241
|
+
if (this._listCache.has(memoKey)) return this._listCache.get(memoKey);
|
|
242
|
+
|
|
243
|
+
let issues;
|
|
244
|
+
try {
|
|
245
|
+
issues = await this._searchEpicChildren(epicId, filters);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
const msg = typeof err?.message === 'string' ? err.message : String(err);
|
|
248
|
+
Logger.warn(
|
|
249
|
+
`[TicketGateway] search-based getTickets(#${epicId}) failed (${msg}); ` +
|
|
250
|
+
'falling back to repo-wide issue listing',
|
|
251
|
+
);
|
|
252
|
+
issues = await this._listAllIssues(filters);
|
|
253
|
+
}
|
|
155
254
|
|
|
156
255
|
// Word-boundary regex prevents #1 matching #10, #100, etc.
|
|
157
256
|
const epicRefRe = new RegExp(
|
|
158
257
|
`(?:Epic:\\s*#${epicId}|parent:\\s*#${epicId})(?:\\s|$|[,.)\\]])`,
|
|
159
258
|
);
|
|
160
259
|
|
|
161
|
-
|
|
260
|
+
const tickets = issues
|
|
162
261
|
.filter((issue) => {
|
|
163
262
|
if (issue.pull_request) return false;
|
|
164
263
|
const body = issue.body ?? '';
|
|
165
264
|
return epicRefRe.test(body);
|
|
166
265
|
})
|
|
167
266
|
.map(issueToListItem);
|
|
267
|
+
this._listCache.set(memoKey, tickets);
|
|
268
|
+
return tickets;
|
|
168
269
|
}
|
|
169
270
|
|
|
170
271
|
/* node:coverage ignore next */
|
|
@@ -187,6 +288,7 @@ export class TicketGateway {
|
|
|
187
288
|
|
|
188
289
|
invalidateTicket(ticketId) {
|
|
189
290
|
this._cache.invalidate(ticketId);
|
|
291
|
+
this._listCache.clear();
|
|
190
292
|
}
|
|
191
293
|
|
|
192
294
|
// ---------------------------------------------------------------------------
|
|
@@ -226,6 +328,7 @@ export class TicketGateway {
|
|
|
226
328
|
},
|
|
227
329
|
});
|
|
228
330
|
const issue = parseApiJson(result);
|
|
331
|
+
this._listCache.clear();
|
|
229
332
|
|
|
230
333
|
let subIssueLinked = false;
|
|
231
334
|
let subIssueError = null;
|
|
@@ -258,8 +361,8 @@ export class TicketGateway {
|
|
|
258
361
|
/**
|
|
259
362
|
* Create a **bare** issue — no `parent: #N` footer composition and no
|
|
260
363
|
* sub-issue link. Serves the standalone create paths that bypass
|
|
261
|
-
* `createTicket`'s Story-shaped body rendering: the `/
|
|
262
|
-
* persist step and the `/
|
|
364
|
+
* `createTicket`'s Story-shaped body rendering: the `/plan`
|
|
365
|
+
* persist step and the `/plan` Phase 4 Epic open
|
|
263
366
|
* (`openEpicFromOnePager`'s `createIssue` port).
|
|
264
367
|
*
|
|
265
368
|
* After the POST, the new issue is added to the configured Project V2
|
|
@@ -288,6 +391,7 @@ export class TicketGateway {
|
|
|
288
391
|
body: { title, body, labels },
|
|
289
392
|
});
|
|
290
393
|
const issue = parseApiJson(result);
|
|
394
|
+
this._listCache.clear();
|
|
291
395
|
|
|
292
396
|
const boardAdd = await addIssueToBoard({
|
|
293
397
|
nodeId: issue.node_id,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* resync-status-column.js — re-assert the GitHub Projects v2 Status
|
|
6
6
|
* column for a ticket after auto-merge has fired (Story #2845).
|
|
7
7
|
*
|
|
8
|
-
* The `/single-story-deliver` and `/
|
|
8
|
+
* The `/single-story-deliver` and `/deliver` workflow docs call
|
|
9
9
|
* this CLI after Step 5 confirms `state: "MERGED"` so the orchestrator
|
|
10
10
|
* wins the race against the GitHub built-in `Pull request merged`
|
|
11
11
|
* workflow, which would otherwise overwrite Status to whatever value
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
/* node:coverage ignore file */
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* retro-run.js — execute the `/
|
|
5
|
+
* retro-run.js — execute the `/deliver` Phase 6 retro from a CLI.
|
|
6
6
|
*
|
|
7
|
-
* Phase 6 of `/
|
|
7
|
+
* Phase 6 of `/deliver` posts the Epic retro by invoking the
|
|
8
8
|
* in-process retro module (`lib/orchestration/retro-runner.js`'s
|
|
9
9
|
* `runRetro`). That module is a library entry point — it has no CLI
|
|
10
10
|
* wrapper and hard-requires both a GitHub `provider` and a lifecycle
|
|
@@ -64,11 +64,20 @@ const tasks = [
|
|
|
64
64
|
// only the canonical `<axis>::<value>` separator from
|
|
65
65
|
// `lib/label-constants.js` appears. Closes the drift gap that
|
|
66
66
|
// let the original `type/epic` typo land at
|
|
67
|
-
// `.agents/workflows/epic
|
|
67
|
+
// `.agents/workflows/helpers/plan-epic.md:49`.
|
|
68
68
|
name: 'label-vocabulary',
|
|
69
69
|
cmd: 'node',
|
|
70
70
|
args: ['.agents/scripts/lint-label-vocabulary.js'],
|
|
71
71
|
},
|
|
72
|
+
{
|
|
73
|
+
// Architecture cycle ratchet (Story #3991). Detects directed import
|
|
74
|
+
// cycles under `.agents/scripts/` and fails on any cycle not in the
|
|
75
|
+
// committed allowlist (`baselines/arch-cycles.json`). Mirrors the
|
|
76
|
+
// ratchet-down semantics of `check-dead-exports.js`.
|
|
77
|
+
name: 'arch-cycles',
|
|
78
|
+
cmd: 'node',
|
|
79
|
+
args: ['.agents/scripts/check-arch-cycles.js'],
|
|
80
|
+
},
|
|
72
81
|
];
|
|
73
82
|
|
|
74
83
|
function runTask({ name, cmd, args }) {
|
|
@@ -84,11 +84,31 @@ export const TEST_RUNNER_FLAGS = Object.freeze([
|
|
|
84
84
|
* quoting). Budgeting the targets to 8 000 chars keeps every spawn's full
|
|
85
85
|
* command line at roughly a quarter of the ceiling — ample headroom for the
|
|
86
86
|
* exe path and any pass-through `--test-name-pattern` args — while keeping
|
|
87
|
-
* the chunk count (and thus the extra `node` start-ups) low.
|
|
88
|
-
*
|
|
89
|
-
* Windows.
|
|
87
|
+
* the chunk count (and thus the extra `node` start-ups) low.
|
|
88
|
+
*
|
|
89
|
+
* `MAX_TARGET_CHARS` is the **Windows** budget. On POSIX hosts `ARG_MAX` is
|
|
90
|
+
* far higher (~256 KB on macOS, ~1 MB on Linux), so applying the Windows
|
|
91
|
+
* budget there needlessly serializes the quick tier into several sequential
|
|
92
|
+
* `node --test` spawns — each chunk pays a fresh runner start-up, and cores
|
|
93
|
+
* idle at every chunk's tail. `POSIX_MAX_TARGET_CHARS` (100 000) lets the
|
|
94
|
+
* whole quick-tier target list (~33 000 chars today) collapse into a single
|
|
95
|
+
* spawn on POSIX while staying far below `ARG_MAX`.
|
|
96
|
+
* `resolveMaxTargetChars` picks the budget per platform; the Windows
|
|
97
|
+
* semantics of `MAX_TARGET_CHARS` are unchanged.
|
|
90
98
|
*/
|
|
91
99
|
export const MAX_TARGET_CHARS = 8000;
|
|
100
|
+
export const POSIX_MAX_TARGET_CHARS = 100_000;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the per-spawn target-character budget for the host platform.
|
|
104
|
+
* The `platform` parameter is injected in tests.
|
|
105
|
+
*
|
|
106
|
+
* @param {NodeJS.Platform} [platform]
|
|
107
|
+
* @returns {number}
|
|
108
|
+
*/
|
|
109
|
+
export function resolveMaxTargetChars(platform = process.platform) {
|
|
110
|
+
return platform === 'win32' ? MAX_TARGET_CHARS : POSIX_MAX_TARGET_CHARS;
|
|
111
|
+
}
|
|
92
112
|
|
|
93
113
|
/**
|
|
94
114
|
* Partition an ordered list of test-file targets into chunks whose joined
|
|
@@ -144,7 +164,7 @@ export function runTestSuite({
|
|
|
144
164
|
spawn = spawnSync,
|
|
145
165
|
cleanup = cleanupRepoTestTempArtifacts,
|
|
146
166
|
listTargets = listTestFilesForTier,
|
|
147
|
-
maxTargetChars =
|
|
167
|
+
maxTargetChars = resolveMaxTargetChars(),
|
|
148
168
|
} = {}) {
|
|
149
169
|
const { tier, rest } = parseTierArgv(argv);
|
|
150
170
|
const targets = listTargets(tier, cwd);
|