supipowers 1.5.2 → 2.0.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/README.md +14 -8
- package/bin/install.mjs +20 -5
- package/bin/install.ts +95 -0
- package/package.json +8 -4
- package/skills/context-mode/SKILL.md +17 -10
- package/skills/harness/SKILL.md +94 -0
- package/skills/ui-design/SKILL.md +63 -0
- package/skills/ui-design/sub-agent-templates/component-builder.md +29 -0
- package/skills/ui-design/sub-agent-templates/design-critic.md +46 -0
- package/skills/ui-design/sub-agent-templates/pencil/component-builder.md +29 -0
- package/skills/ui-design/sub-agent-templates/pencil/design-critic.md +42 -0
- package/skills/ui-design/sub-agent-templates/pencil/section-assembler.md +27 -0
- package/skills/ui-design/sub-agent-templates/section-assembler.md +27 -0
- package/skills/ultraplan-discover/SKILL.md +96 -0
- package/skills/ultraplan-intake/SKILL.md +89 -0
- package/skills/ultraplan-research/SKILL.md +129 -0
- package/skills/ultraplan-review/SKILL.md +86 -0
- package/skills/ultraplan-review-scope/SKILL.md +111 -0
- package/skills/ultraplan-review-structure/SKILL.md +120 -0
- package/skills/ultraplan-review-tdd/SKILL.md +142 -0
- package/skills/ultraplan-scout/SKILL.md +110 -0
- package/skills/ultraplan-synthesize/SKILL.md +124 -0
- package/src/{quality/ai-session.ts → ai/final-message.ts} +27 -0
- package/src/ai/schema-text.ts +129 -0
- package/src/ai/structured-output.ts +274 -0
- package/src/ai/template.ts +27 -0
- package/src/bootstrap.ts +63 -28
- package/src/commands/agents.ts +149 -45
- package/src/commands/ai-review.ts +251 -30
- package/src/commands/clear.ts +434 -0
- package/src/commands/commit.ts +1 -0
- package/src/commands/config.ts +242 -44
- package/src/commands/context.ts +55 -28
- package/src/commands/doctor.ts +234 -6
- package/src/commands/fix-pr.ts +306 -131
- package/src/commands/generate.ts +111 -21
- package/src/commands/memory.ts +192 -0
- package/src/commands/model-picker.ts +28 -21
- package/src/commands/model.ts +19 -9
- package/src/commands/optimize-context.ts +408 -29
- package/src/commands/plan.ts +2 -0
- package/src/commands/qa.ts +312 -137
- package/src/commands/release.ts +259 -76
- package/src/commands/review.ts +293 -59
- package/src/commands/status.ts +200 -13
- package/src/commands/supi.ts +3 -35
- package/src/commands/ui-design.ts +394 -0
- package/src/commands/ultraplan.ts +1518 -0
- package/src/commands/update.ts +86 -0
- package/src/config/defaults.ts +62 -0
- package/src/config/loader.ts +448 -60
- package/src/config/schema.ts +108 -2
- package/src/context/optimizer.ts +25 -33
- package/src/context/rule-renderer.ts +223 -0
- package/src/context/savings.ts +258 -0
- package/src/context/startup-check.ts +380 -0
- package/src/context/startup-optimizer.ts +355 -0
- package/src/context/tokenignore.ts +146 -0
- package/src/context-mode/cache-handle.ts +49 -0
- package/src/context-mode/cache-preview.ts +71 -0
- package/src/context-mode/cache-store.ts +738 -0
- package/src/context-mode/compressor.ts +131 -26
- package/src/context-mode/dedup.ts +108 -0
- package/src/context-mode/detector.ts +35 -4
- package/src/context-mode/event-extractor.ts +14 -12
- package/src/context-mode/event-store.ts +91 -36
- package/src/context-mode/hooks.ts +798 -56
- package/src/context-mode/knowledge/store.ts +255 -11
- package/src/context-mode/memory-store.ts +325 -0
- package/src/context-mode/metrics-recorder.ts +158 -0
- package/src/context-mode/metrics-store.ts +765 -0
- package/src/context-mode/model.ts +24 -0
- package/src/context-mode/processor-keys.ts +29 -0
- package/src/context-mode/processors/build.ts +66 -0
- package/src/context-mode/processors/docker.ts +57 -0
- package/src/context-mode/processors/git.ts +111 -0
- package/src/context-mode/processors/json.ts +112 -0
- package/src/context-mode/processors/k8s.ts +67 -0
- package/src/context-mode/processors/lint.ts +67 -0
- package/src/context-mode/processors/log.ts +86 -0
- package/src/context-mode/processors/registry.ts +116 -0
- package/src/context-mode/processors/test-runner.ts +102 -0
- package/src/context-mode/processors/types.ts +20 -0
- package/src/context-mode/repomap.ts +400 -0
- package/src/context-mode/routing.ts +97 -24
- package/src/context-mode/sandbox/runners.ts +5 -1
- package/src/context-mode/snapshot-builder.ts +106 -11
- package/src/context-mode/source-hash.ts +173 -0
- package/src/context-mode/tool-name.ts +11 -0
- package/src/context-mode/tools.ts +654 -22
- package/src/context-mode/web/fetcher.ts +31 -12
- package/src/debug/logger.ts +2 -1
- package/src/deps/registry.ts +1 -1
- package/src/discipline/failure-summarizer.ts +170 -0
- package/src/discipline/failure-taxonomy.ts +131 -0
- package/src/discipline/workflow-invariants.ts +125 -0
- package/src/discovery/index.ts +31 -0
- package/src/discovery/lsp.ts +87 -0
- package/src/discovery/rank.ts +144 -0
- package/src/discovery/sources.ts +89 -0
- package/src/discovery/workflow.ts +87 -0
- package/src/docs/contracts.ts +39 -0
- package/src/docs/drift.ts +117 -87
- package/src/fix-pr/assessment.ts +200 -0
- package/src/fix-pr/contracts.ts +47 -0
- package/src/fix-pr/fetch-comments.ts +80 -0
- package/src/fix-pr/prompt-builder.ts +58 -40
- package/src/fix-pr/scripts/exec.ts +34 -0
- package/src/fix-pr/scripts/trigger-review.ts +106 -0
- package/src/fix-pr/scripts/wait-and-check.ts +108 -0
- package/src/fix-pr/types.ts +4 -0
- package/src/git/branch-finish.ts +5 -0
- package/src/git/commit-contract.ts +83 -0
- package/src/git/commit.ts +121 -184
- package/src/git/status.ts +62 -8
- package/src/harness/anti_slop/architecture-parser.ts +210 -0
- package/src/harness/anti_slop/backend-factory.ts +30 -0
- package/src/harness/anti_slop/backend.ts +140 -0
- package/src/harness/anti_slop/desloppify-adapter.ts +319 -0
- package/src/harness/anti_slop/fallow-adapter.ts +305 -0
- package/src/harness/anti_slop/installer.ts +227 -0
- package/src/harness/anti_slop/queue.ts +216 -0
- package/src/harness/anti_slop/recommend.ts +84 -0
- package/src/harness/anti_slop/score.ts +180 -0
- package/src/harness/anti_slop/synthetic-edit-test.ts +128 -0
- package/src/harness/artifacts/agents-md.ts +88 -0
- package/src/harness/artifacts/checks-wiring.ts +57 -0
- package/src/harness/artifacts/docs-tree.ts +79 -0
- package/src/harness/artifacts/lint-configs.ts +136 -0
- package/src/harness/artifacts/review-agents.ts +67 -0
- package/src/harness/bare-entry.ts +108 -0
- package/src/harness/command.ts +1010 -0
- package/src/harness/default-agents/design.md +23 -0
- package/src/harness/default-agents/discover.md +18 -0
- package/src/harness/default-agents/implement.md +24 -0
- package/src/harness/default-agents/plan.md +19 -0
- package/src/harness/default-agents/research.md +21 -0
- package/src/harness/default-agents/validate.md +22 -0
- package/src/harness/gc/reporter.ts +28 -0
- package/src/harness/gc/runner.ts +136 -0
- package/src/harness/hooks/layer-context-inject.ts +155 -0
- package/src/harness/hooks/post-session-sweep.ts +130 -0
- package/src/harness/hooks/pre-edit-dupe-probe.ts +224 -0
- package/src/harness/hooks/register.ts +118 -0
- package/src/harness/model.ts +117 -0
- package/src/harness/pipeline.ts +348 -0
- package/src/harness/project-paths.ts +235 -0
- package/src/harness/stage-runner.ts +107 -0
- package/src/harness/stages/design.ts +386 -0
- package/src/harness/stages/discover.ts +454 -0
- package/src/harness/stages/implement.ts +162 -0
- package/src/harness/stages/plan.ts +335 -0
- package/src/harness/stages/research.ts +263 -0
- package/src/harness/stages/validate.ts +684 -0
- package/src/harness/storage.ts +467 -0
- package/src/harness/tools.ts +426 -0
- package/src/lsp/bridge.ts +56 -95
- package/src/lsp/capabilities.ts +108 -0
- package/src/lsp/contracts.ts +35 -0
- package/src/lsp/detector.ts +8 -12
- package/src/markdown-frontmatter.ts +68 -0
- package/src/mempalace/bridge.ts +129 -0
- package/src/mempalace/config.ts +75 -0
- package/src/mempalace/format.ts +163 -0
- package/src/mempalace/hooks.ts +370 -0
- package/src/mempalace/installer-helper.ts +194 -0
- package/src/mempalace/python/mempalace_bridge.py +440 -0
- package/src/mempalace/runtime.ts +565 -0
- package/src/mempalace/schema.ts +264 -0
- package/src/mempalace/session-summary.ts +198 -0
- package/src/mempalace/tool.ts +186 -0
- package/src/mempalace/uv.ts +256 -0
- package/src/migrate/runner.ts +354 -0
- package/src/planning/approval-flow.ts +206 -9
- package/src/planning/plan-writer-prompt.ts +4 -3
- package/src/planning/planning-ask-tool.ts +39 -0
- package/src/planning/render-markdown.ts +74 -0
- package/src/planning/spec.ts +42 -0
- package/src/planning/system-prompt.ts +11 -8
- package/src/planning/validate.ts +84 -0
- package/src/platform/omp.ts +15 -2
- package/src/platform/system-prompt.ts +37 -0
- package/src/platform/test-utils.ts +3 -0
- package/src/platform/types.ts +6 -1
- package/src/qa/config.ts +12 -6
- package/src/qa/detect-app-type.ts +13 -6
- package/src/qa/matrix.ts +12 -6
- package/src/qa/prompt-builder.ts +28 -30
- package/src/qa/scripts/dev-server-utils.ts +72 -0
- package/src/qa/scripts/run-e2e-tests.ts +226 -0
- package/src/qa/scripts/start-dev-server.ts +138 -0
- package/src/qa/scripts/stop-dev-server.ts +77 -0
- package/src/qa/session.ts +13 -7
- package/src/quality/ai-setup.ts +27 -25
- package/src/quality/contracts.ts +34 -0
- package/src/quality/gates/ai-review.ts +20 -58
- package/src/quality/gates/command.ts +249 -46
- package/src/quality/review-gates.ts +18 -2
- package/src/quality/runner.ts +63 -22
- package/src/quality/schemas.ts +37 -2
- package/src/quality/setup.ts +96 -16
- package/src/release/changelog.ts +1 -1
- package/src/release/channels/custom.ts +13 -3
- package/src/release/channels/types.ts +5 -0
- package/src/release/contracts.ts +90 -0
- package/src/release/executor.ts +122 -45
- package/src/release/prompt.ts +18 -2
- package/src/release/targets.ts +86 -0
- package/src/release/version.ts +96 -71
- package/src/review/agent-loader.ts +298 -127
- package/src/review/fixer.ts +10 -6
- package/src/review/multi-agent-runner.ts +115 -14
- package/src/review/output.ts +12 -139
- package/src/review/runner.ts +12 -6
- package/src/review/scope.ts +144 -24
- package/src/review/types.ts +11 -20
- package/src/review/validator.ts +12 -6
- package/src/storage/fix-pr-sessions.ts +21 -14
- package/src/storage/plans.ts +14 -5
- package/src/storage/qa-sessions.ts +25 -19
- package/src/storage/reliability-metrics.ts +180 -0
- package/src/storage/reports.ts +8 -7
- package/src/storage/review-sessions.ts +55 -20
- package/src/tool-catalog/active-tool-controller.ts +164 -0
- package/src/tool-catalog/active-tool-planner.ts +212 -0
- package/src/tool-catalog/tool-groups.ts +102 -0
- package/src/types.ts +1401 -5
- package/src/ui-design/backend-adapter.ts +78 -0
- package/src/ui-design/backends/local-html.ts +82 -0
- package/src/ui-design/backends/pencil-mcp.ts +111 -0
- package/src/ui-design/components-scanner.ts +124 -0
- package/src/ui-design/config.ts +55 -0
- package/src/ui-design/pen-scanner.ts +95 -0
- package/src/ui-design/pen-selector.ts +72 -0
- package/src/ui-design/prompt-builder.ts +73 -0
- package/src/ui-design/scanner.ts +136 -0
- package/src/ui-design/session.ts +974 -0
- package/src/ui-design/system-prompt.ts +312 -0
- package/src/ui-design/tokens-scanner.ts +181 -0
- package/src/ui-design/types.ts +96 -0
- package/src/ultraplan/agent-catalog.ts +522 -0
- package/src/ultraplan/authoring/agent-catalog.ts +310 -0
- package/src/ultraplan/authoring/authoring-tools.ts +552 -0
- package/src/ultraplan/authoring/command-handlers.ts +339 -0
- package/src/ultraplan/authoring/markdown.ts +510 -0
- package/src/ultraplan/authoring/model.ts +162 -0
- package/src/ultraplan/authoring/pipeline.ts +319 -0
- package/src/ultraplan/authoring/stage-runner.ts +141 -0
- package/src/ultraplan/authoring/stages/approve.ts +249 -0
- package/src/ultraplan/authoring/stages/discover.ts +289 -0
- package/src/ultraplan/authoring/stages/intake.ts +203 -0
- package/src/ultraplan/authoring/stages/research.ts +399 -0
- package/src/ultraplan/authoring/stages/review.ts +333 -0
- package/src/ultraplan/authoring/stages/scout.ts +188 -0
- package/src/ultraplan/authoring/stages/synthesize.ts +348 -0
- package/src/ultraplan/authoring/storage.ts +594 -0
- package/src/ultraplan/authoring/synth-gate.ts +165 -0
- package/src/ultraplan/authoring-draft.ts +653 -0
- package/src/ultraplan/authoring-persist.ts +180 -0
- package/src/ultraplan/authoring-tool.ts +608 -0
- package/src/ultraplan/authoring-wizard.ts +587 -0
- package/src/ultraplan/batch/merge.ts +98 -0
- package/src/ultraplan/batch/planner.ts +150 -0
- package/src/ultraplan/batch/presenter.ts +97 -0
- package/src/ultraplan/batch/storage.ts +420 -0
- package/src/ultraplan/batch/supervisor.ts +317 -0
- package/src/ultraplan/batch/worker.ts +26 -0
- package/src/ultraplan/batch/worktree.ts +110 -0
- package/src/ultraplan/contracts.ts +1593 -0
- package/src/ultraplan/default-agents/authoring/discoverer.md +12 -0
- package/src/ultraplan/default-agents/authoring/intake.md +12 -0
- package/src/ultraplan/default-agents/authoring/planner.md +12 -0
- package/src/ultraplan/default-agents/authoring/researcher.md +12 -0
- package/src/ultraplan/default-agents/authoring/scope-checker.md +12 -0
- package/src/ultraplan/default-agents/authoring/scout.md +12 -0
- package/src/ultraplan/default-agents/authoring/structure-checker.md +12 -0
- package/src/ultraplan/default-agents/authoring/tdd-checker.md +12 -0
- package/src/ultraplan/default-agents/backend-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/backend-executor.md +10 -0
- package/src/ultraplan/default-agents/backend-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/backend-tester.md +10 -0
- package/src/ultraplan/default-agents/frontend-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/frontend-executor.md +10 -0
- package/src/ultraplan/default-agents/frontend-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/frontend-tester.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-domain-reviewer.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-executor.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-stack-reviewer.md +10 -0
- package/src/ultraplan/default-agents/infrastructure-tester.md +10 -0
- package/src/ultraplan/execution/contract.ts +71 -0
- package/src/ultraplan/execution/policy.ts +217 -0
- package/src/ultraplan/execution/runtime-tools.ts +107 -0
- package/src/ultraplan/execution/session-runner.ts +281 -0
- package/src/ultraplan/next-router.ts +85 -0
- package/src/ultraplan/presenter.ts +359 -0
- package/src/ultraplan/project-paths.ts +342 -0
- package/src/ultraplan/runtime/active-execution.ts +72 -0
- package/src/ultraplan/runtime/apply-mutation.ts +416 -0
- package/src/ultraplan/runtime/blockers.ts +243 -0
- package/src/ultraplan/runtime/hook-bridge.ts +486 -0
- package/src/ultraplan/runtime/launch-context.ts +207 -0
- package/src/ultraplan/runtime/migration.ts +524 -0
- package/src/ultraplan/runtime/normalize.ts +281 -0
- package/src/ultraplan/runtime/proof.ts +260 -0
- package/src/ultraplan/runtime/reducer.ts +416 -0
- package/src/ultraplan/runtime/repair.ts +251 -0
- package/src/ultraplan/runtime/tracker-storage.ts +368 -0
- package/src/ultraplan/session-selection.ts +291 -0
- package/src/ultraplan/storage.ts +374 -0
- package/src/utils/editor.ts +38 -0
- package/src/utils/executable.ts +80 -0
- package/src/utils/paths.ts +1 -20
- package/src/utils/shell.ts +31 -0
- package/src/visual/companion.ts +2 -1
- package/src/visual/scripts/frame-template.html +60 -0
- package/src/visual/scripts/index.js +59 -13
- package/src/visual/scripts/package.json +3 -0
- package/src/visual/start-server.ts +2 -1
- package/src/workspace/git-scope.ts +64 -0
- package/src/workspace/locks.ts +23 -0
- package/src/workspace/package-manager.ts +117 -0
- package/src/workspace/path-mapping.ts +75 -0
- package/src/workspace/project-slug.ts +92 -0
- package/src/workspace/repo-root.ts +137 -0
- package/src/workspace/selector.ts +115 -0
- package/src/workspace/state-paths.ts +118 -0
- package/src/workspace/targets.ts +313 -0
- package/src/fix-pr/scripts/diff-comments.sh +0 -33
- package/src/fix-pr/scripts/fetch-pr-comments.sh +0 -25
- package/src/fix-pr/scripts/trigger-review.sh +0 -36
- package/src/fix-pr/scripts/wait-and-check.sh +0 -37
- package/src/qa/scripts/detect-app-type.sh +0 -68
- package/src/qa/scripts/discover-routes.sh +0 -143
- package/src/qa/scripts/run-e2e-tests.sh +0 -131
- package/src/qa/scripts/start-dev-server.sh +0 -46
- package/src/qa/scripts/stop-dev-server.sh +0 -36
- package/src/review/prompts/fix-output-schema.md +0 -18
- package/src/review/prompts/review-output-schema.md +0 -38
- package/src/review/template.ts +0 -15
- /package/src/{review → ai}/prompts/invalid-output-retry.md +0 -0
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import type { Platform, PlatformContext, PlatformPaths } from "../platform/types.js";
|
|
2
|
+
import type {
|
|
3
|
+
ResolvedUltraPlanCatalog,
|
|
4
|
+
UltraPlanAgentSlotName,
|
|
5
|
+
UltraPlanApplicability,
|
|
6
|
+
UltraPlanCatalogError,
|
|
7
|
+
UltraPlanCatalogLoadResult,
|
|
8
|
+
UltraPlanReviewerSlotName,
|
|
9
|
+
UltraPlanStackId,
|
|
10
|
+
UltraPlanStorageError,
|
|
11
|
+
} from "../types.js";
|
|
12
|
+
import { loadUltraPlanAgentCatalog } from "./agent-catalog.js";
|
|
13
|
+
import {
|
|
14
|
+
addDomain,
|
|
15
|
+
addScenario,
|
|
16
|
+
buildInitialAuthoredDraft,
|
|
17
|
+
draftToAuthoredArtifact,
|
|
18
|
+
draftToManifest,
|
|
19
|
+
isDraftReadyToPersist,
|
|
20
|
+
removeDomain,
|
|
21
|
+
removeScenario,
|
|
22
|
+
renameDomain,
|
|
23
|
+
renameScenario,
|
|
24
|
+
setSessionId,
|
|
25
|
+
setSessionTitleAndGoal,
|
|
26
|
+
setStackApplicability,
|
|
27
|
+
slugifyUltraPlanId,
|
|
28
|
+
SESSION_GOAL_MAX,
|
|
29
|
+
SESSION_TITLE_MAX,
|
|
30
|
+
type UltraPlanAuthoredDraft,
|
|
31
|
+
} from "./authoring-draft.js";
|
|
32
|
+
import {
|
|
33
|
+
persistAuthoredUltraPlanSession,
|
|
34
|
+
type AuthoringPersistInput,
|
|
35
|
+
type AuthoringPersistResult,
|
|
36
|
+
} from "./authoring-persist.js";
|
|
37
|
+
import { renderUltraPlanAuthoredDraft } from "./presenter.js";
|
|
38
|
+
import { getUltraplanProjectName, getUltraplanSessionDir } from "./project-paths.js";
|
|
39
|
+
import { ULTRAPLAN_STACKS } from "./contracts.js";
|
|
40
|
+
|
|
41
|
+
export interface AuthoringDependencies {
|
|
42
|
+
now: () => Date;
|
|
43
|
+
newSessionId: () => string;
|
|
44
|
+
loadCatalog: (paths: PlatformPaths, cwd: string) => UltraPlanCatalogLoadResult;
|
|
45
|
+
persist: (input: AuthoringPersistInput) => AuthoringPersistResult;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type AuthoringFailure =
|
|
49
|
+
| { kind: "no-ui" }
|
|
50
|
+
| { kind: "catalog-error"; errors: UltraPlanCatalogError[] }
|
|
51
|
+
| { kind: "cancelled" }
|
|
52
|
+
| { kind: "discarded" }
|
|
53
|
+
| { kind: "empty-session" }
|
|
54
|
+
| { kind: "persist-failed"; error: UltraPlanStorageError; partial: string[] };
|
|
55
|
+
|
|
56
|
+
export type AuthoringResult =
|
|
57
|
+
| {
|
|
58
|
+
ok: true;
|
|
59
|
+
sessionId: string;
|
|
60
|
+
paths: { authored: string; manifest: string; indexEntry: string };
|
|
61
|
+
}
|
|
62
|
+
| { ok: false; failure: AuthoringFailure };
|
|
63
|
+
|
|
64
|
+
export function generateUltraPlanSessionId(now: Date): string {
|
|
65
|
+
const yyyymmdd = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
66
|
+
const random = Math.floor(Math.random() * 0xffff).toString(16).padStart(4, "0");
|
|
67
|
+
return `ultraplan-${yyyymmdd}-${random}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function defaultDependencies(_platform: Platform): AuthoringDependencies {
|
|
71
|
+
return {
|
|
72
|
+
now: () => new Date(),
|
|
73
|
+
newSessionId: () => generateUltraPlanSessionId(new Date()),
|
|
74
|
+
loadCatalog: loadUltraPlanAgentCatalog,
|
|
75
|
+
persist: persistAuthoredUltraPlanSession,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function mergeDependencies(
|
|
80
|
+
platform: Platform,
|
|
81
|
+
overrides: Partial<AuthoringDependencies> | undefined,
|
|
82
|
+
): AuthoringDependencies {
|
|
83
|
+
return { ...defaultDependencies(platform), ...overrides };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function collectMissingRequiredSlotErrors(catalog: ResolvedUltraPlanCatalog): UltraPlanCatalogError[] {
|
|
87
|
+
const errors: UltraPlanCatalogError[] = [];
|
|
88
|
+
for (const stack of ULTRAPLAN_STACKS) {
|
|
89
|
+
const executorSlot: UltraPlanAgentSlotName = `${stack}-executor`;
|
|
90
|
+
const testerSlot: UltraPlanAgentSlotName = `${stack}-tester`;
|
|
91
|
+
if (!catalog.slots[executorSlot]) {
|
|
92
|
+
errors.push({
|
|
93
|
+
slot: executorSlot,
|
|
94
|
+
code: "required-slot-unresolved",
|
|
95
|
+
message: `UltraPlan slot "${executorSlot}" is unresolved.`,
|
|
96
|
+
path: null,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (!catalog.slots[testerSlot]) {
|
|
100
|
+
errors.push({
|
|
101
|
+
slot: testerSlot,
|
|
102
|
+
code: "required-slot-unresolved",
|
|
103
|
+
message: `UltraPlan slot "${testerSlot}" is unresolved.`,
|
|
104
|
+
path: null,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
for (const suffix of ["domain-reviewer", "stack-reviewer"] as const) {
|
|
108
|
+
const reviewer = `${stack}-${suffix}` as UltraPlanReviewerSlotName;
|
|
109
|
+
const gateEnabled = catalog.reviewGates[reviewer]?.enabled ?? true;
|
|
110
|
+
if (gateEnabled && !catalog.slots[reviewer]) {
|
|
111
|
+
errors.push({
|
|
112
|
+
slot: reviewer,
|
|
113
|
+
code: "required-slot-unresolved",
|
|
114
|
+
message: `UltraPlan slot "${reviewer}" is unresolved while its review gate is enabled.`,
|
|
115
|
+
path: null,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return errors;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function promptBounded(
|
|
124
|
+
ctx: PlatformContext,
|
|
125
|
+
label: string,
|
|
126
|
+
max: number,
|
|
127
|
+
inputOpts: { placeholder?: string; helpText?: string } = {},
|
|
128
|
+
): Promise<string | null> {
|
|
129
|
+
while (true) {
|
|
130
|
+
const value = await ctx.ui.input(label, inputOpts);
|
|
131
|
+
if (value === null) return null;
|
|
132
|
+
if (value === "") {
|
|
133
|
+
ctx.ui.notify(`${label}: must not be empty`, "warning");
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const len = [...value].length;
|
|
137
|
+
if (len > max) {
|
|
138
|
+
ctx.ui.notify(`${label}: over length cap (${len}/${max})`, "warning");
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function promptStackApplicability(
|
|
146
|
+
ctx: PlatformContext,
|
|
147
|
+
stack: UltraPlanStackId,
|
|
148
|
+
): Promise<UltraPlanApplicability | null> {
|
|
149
|
+
const selected = await ctx.ui.select(`${stack} applicability`, ["applicable", "not-applicable"], {
|
|
150
|
+
helpText: `Does this ultraplan include ${stack} work?`,
|
|
151
|
+
});
|
|
152
|
+
if (selected === null) return null;
|
|
153
|
+
return selected === "not-applicable" ? "not-applicable" : "applicable";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function runUltraPlanAuthoringWizard(
|
|
157
|
+
platform: Platform,
|
|
158
|
+
ctx: PlatformContext,
|
|
159
|
+
overrides?: Partial<AuthoringDependencies>,
|
|
160
|
+
): Promise<AuthoringResult> {
|
|
161
|
+
if (!ctx.hasUI) {
|
|
162
|
+
return { ok: false, failure: { kind: "no-ui" } };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const deps = mergeDependencies(platform, overrides);
|
|
166
|
+
const catalogResult = deps.loadCatalog(platform.paths, ctx.cwd);
|
|
167
|
+
if (!catalogResult.ok) {
|
|
168
|
+
return { ok: false, failure: { kind: "catalog-error", errors: catalogResult.errors } };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const missing = collectMissingRequiredSlotErrors(catalogResult.value);
|
|
172
|
+
if (missing.length > 0) {
|
|
173
|
+
return { ok: false, failure: { kind: "catalog-error", errors: missing } };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Phases 1–2: title + goal
|
|
177
|
+
const title = await promptBounded(ctx, "Ultraplan title", SESSION_TITLE_MAX, {
|
|
178
|
+
placeholder: "e.g. checkout-redesign",
|
|
179
|
+
helpText: "Short name used in the session picker",
|
|
180
|
+
});
|
|
181
|
+
if (title === null) return { ok: false, failure: { kind: "cancelled" } };
|
|
182
|
+
|
|
183
|
+
const goal = await promptBounded(ctx, "One-line goal", SESSION_GOAL_MAX, {
|
|
184
|
+
placeholder: "e.g. Users can complete checkout on mobile",
|
|
185
|
+
});
|
|
186
|
+
if (goal === null) return { ok: false, failure: { kind: "cancelled" } };
|
|
187
|
+
|
|
188
|
+
const initialDraft: UltraPlanAuthoredDraft = buildInitialAuthoredDraft({
|
|
189
|
+
sessionId: deps.newSessionId(),
|
|
190
|
+
title,
|
|
191
|
+
goal,
|
|
192
|
+
createdAt: deps.now(),
|
|
193
|
+
catalog: catalogResult.value,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Phase 3: stack applicability loop. Require at least one applicable stack.
|
|
197
|
+
let draft = initialDraft;
|
|
198
|
+
while (true) {
|
|
199
|
+
let anyApplicable = false;
|
|
200
|
+
for (const stack of ULTRAPLAN_STACKS) {
|
|
201
|
+
const applicability = await promptStackApplicability(ctx, stack);
|
|
202
|
+
if (applicability === null) {
|
|
203
|
+
return { ok: false, failure: { kind: "cancelled" } };
|
|
204
|
+
}
|
|
205
|
+
const updated = setStackApplicability(draft, stack, applicability);
|
|
206
|
+
if (!updated.ok) {
|
|
207
|
+
// Shouldn't happen in practice — the set is total over valid stacks.
|
|
208
|
+
return { ok: false, failure: { kind: "cancelled" } };
|
|
209
|
+
}
|
|
210
|
+
draft = updated.draft;
|
|
211
|
+
if (applicability === "applicable") anyApplicable = true;
|
|
212
|
+
}
|
|
213
|
+
if (anyApplicable) break;
|
|
214
|
+
ctx.ui.notify("At least one stack must be applicable", "warning");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Phase 4 + 5: per-stack domain loop + per-domain scenario loop.
|
|
218
|
+
for (const stack of draft.stacks) {
|
|
219
|
+
if (stack.applicability !== "applicable") continue;
|
|
220
|
+
const afterPhase4 = await runDomainLoop(ctx, draft, stack.stack, deps);
|
|
221
|
+
if (afterPhase4 === null) return { ok: false, failure: { kind: "cancelled" } };
|
|
222
|
+
draft = afterPhase4;
|
|
223
|
+
|
|
224
|
+
const stackAfter = draft.stacks.find((s) => s.stack === stack.stack)!;
|
|
225
|
+
for (const domain of stackAfter.domains) {
|
|
226
|
+
const afterPhase5 = await runScenariosLoop(ctx, draft, stack.stack, domain.id, deps);
|
|
227
|
+
if (afterPhase5 === null) return { ok: false, failure: { kind: "cancelled" } };
|
|
228
|
+
draft = afterPhase5;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Phase 6: review loop
|
|
232
|
+
while (true) {
|
|
233
|
+
const reviewResult = await runReviewLoop(ctx, draft, deps);
|
|
234
|
+
if (reviewResult.kind === "approved") {
|
|
235
|
+
draft = reviewResult.draft;
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
if (reviewResult.kind === "cancelled") return { ok: false, failure: { kind: "cancelled" } };
|
|
239
|
+
if (reviewResult.kind === "discarded") return { ok: false, failure: { kind: "discarded" } };
|
|
240
|
+
draft = reviewResult.draft;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Phase 7: persist.
|
|
244
|
+
return runPersist(platform, ctx, draft, deps);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
const DOMAIN_NAME_MAX = 60;
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
async function runDomainLoop(
|
|
253
|
+
ctx: PlatformContext,
|
|
254
|
+
draft: UltraPlanAuthoredDraft,
|
|
255
|
+
stackId: UltraPlanStackId,
|
|
256
|
+
_deps: AuthoringDependencies,
|
|
257
|
+
): Promise<UltraPlanAuthoredDraft | null> {
|
|
258
|
+
let current = draft;
|
|
259
|
+
|
|
260
|
+
while (true) {
|
|
261
|
+
const stack = current.stacks.find((s) => s.stack === stackId)!;
|
|
262
|
+
const options = [
|
|
263
|
+
"+ Add domain",
|
|
264
|
+
...stack.domains.map((d) => `✎ Rename ${d.id}`),
|
|
265
|
+
...stack.domains.map((d) => `− Remove ${d.id}`),
|
|
266
|
+
`✓ Done with ${stackId} domains`,
|
|
267
|
+
];
|
|
268
|
+
const selected = await ctx.ui.select(`${stackId} domains`, options, {
|
|
269
|
+
helpText: "Domains group related scenarios. Each must carry ≥1 scenario.",
|
|
270
|
+
});
|
|
271
|
+
if (selected === null) return null;
|
|
272
|
+
|
|
273
|
+
if (selected === "+ Add domain") {
|
|
274
|
+
const name = await promptBounded(ctx, `Name for the new ${stackId} domain`, DOMAIN_NAME_MAX);
|
|
275
|
+
if (name === null) return null;
|
|
276
|
+
const id = slugifyUltraPlanId(name);
|
|
277
|
+
const result = addDomain(current, stackId, { id, name });
|
|
278
|
+
if (!result.ok) {
|
|
279
|
+
if (result.reason.code === "duplicate-id") {
|
|
280
|
+
ctx.ui.notify(`duplicate domain id: ${id}`, "warning");
|
|
281
|
+
} else {
|
|
282
|
+
ctx.ui.notify(`could not add domain: ${result.reason.code}`, "warning");
|
|
283
|
+
}
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
current = result.draft;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const renameMatch = /^✎ Rename (.+)$/.exec(selected);
|
|
291
|
+
if (renameMatch) {
|
|
292
|
+
const domainId = renameMatch[1];
|
|
293
|
+
const newName = await promptBounded(ctx, `New name for ${domainId}`, DOMAIN_NAME_MAX);
|
|
294
|
+
if (newName === null) return null;
|
|
295
|
+
const result = renameDomain(current, stackId, domainId, { name: newName });
|
|
296
|
+
if (!result.ok) {
|
|
297
|
+
ctx.ui.notify(`could not rename: ${result.reason.code}`, "warning");
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
current = result.draft;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const removeMatch = /^− Remove (.+)$/.exec(selected);
|
|
305
|
+
if (removeMatch) {
|
|
306
|
+
const domainId = removeMatch[1];
|
|
307
|
+
const confirmed = await confirmDestructive(ctx, "Remove domain?", `Remove ${domainId} and its scenarios?`);
|
|
308
|
+
if (!confirmed) continue;
|
|
309
|
+
const result = removeDomain(current, stackId, domainId);
|
|
310
|
+
if (!result.ok) {
|
|
311
|
+
ctx.ui.notify(`could not remove: ${result.reason.code}`, "warning");
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
current = result.draft;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (selected === `✓ Done with ${stackId} domains`) {
|
|
319
|
+
if (stack.domains.length === 0) {
|
|
320
|
+
ctx.ui.notify(`${stackId} must have at least one domain`, "warning");
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
return current;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function confirmDestructive(
|
|
329
|
+
ctx: PlatformContext,
|
|
330
|
+
title: string,
|
|
331
|
+
message: string,
|
|
332
|
+
opts: { keep?: string; yes?: string } = {},
|
|
333
|
+
): Promise<boolean> {
|
|
334
|
+
if (ctx.ui.confirm) {
|
|
335
|
+
return ctx.ui.confirm(title, message);
|
|
336
|
+
}
|
|
337
|
+
const yes = opts.yes ?? "Yes, remove";
|
|
338
|
+
const keep = opts.keep ?? "Keep";
|
|
339
|
+
const answer = await ctx.ui.select(title, [keep, yes]);
|
|
340
|
+
return answer === yes;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const SCENARIO_TITLE_MAX_LOCAL = 120;
|
|
344
|
+
const LEVELS_ORDER: readonly ("unit" | "integration" | "e2e")[] = ["unit", "integration", "e2e"];
|
|
345
|
+
|
|
346
|
+
async function runScenariosLoop(
|
|
347
|
+
ctx: PlatformContext,
|
|
348
|
+
draft: UltraPlanAuthoredDraft,
|
|
349
|
+
stackId: UltraPlanStackId,
|
|
350
|
+
domainId: string,
|
|
351
|
+
_deps: AuthoringDependencies,
|
|
352
|
+
): Promise<UltraPlanAuthoredDraft | null> {
|
|
353
|
+
let current = draft;
|
|
354
|
+
|
|
355
|
+
while (true) {
|
|
356
|
+
for (const level of LEVELS_ORDER) {
|
|
357
|
+
const next = await runScenarioLevelLoop(ctx, current, stackId, domainId, level);
|
|
358
|
+
if (next === null) return null;
|
|
359
|
+
current = next;
|
|
360
|
+
}
|
|
361
|
+
const domain = current.stacks.find((s) => s.stack === stackId)?.domains.find((d) => d.id === domainId);
|
|
362
|
+
if (!domain) return current;
|
|
363
|
+
const total = domain.unit.length + domain.integration.length + domain.e2e.length;
|
|
364
|
+
if (total > 0) return current;
|
|
365
|
+
ctx.ui.notify(`${stackId}.${domainId} must have at least one scenario`, "warning");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function runScenarioLevelLoop(
|
|
370
|
+
ctx: PlatformContext,
|
|
371
|
+
draft: UltraPlanAuthoredDraft,
|
|
372
|
+
stackId: UltraPlanStackId,
|
|
373
|
+
domainId: string,
|
|
374
|
+
level: "unit" | "integration" | "e2e",
|
|
375
|
+
): Promise<UltraPlanAuthoredDraft | null> {
|
|
376
|
+
let current = draft;
|
|
377
|
+
while (true) {
|
|
378
|
+
const domain = current.stacks.find((s) => s.stack === stackId)?.domains.find((d) => d.id === domainId);
|
|
379
|
+
if (!domain) return current;
|
|
380
|
+
const scenarios = domain[level];
|
|
381
|
+
const options = [
|
|
382
|
+
`+ Add ${level} scenario`,
|
|
383
|
+
...scenarios.map((s) => `✎ Rename ${s.id}`),
|
|
384
|
+
...scenarios.map((s) => `− Remove ${s.id}`),
|
|
385
|
+
`✓ Done with ${level}`,
|
|
386
|
+
];
|
|
387
|
+
const selected = await ctx.ui.select(
|
|
388
|
+
`${stackId} / ${domainId} / ${level}`,
|
|
389
|
+
options,
|
|
390
|
+
);
|
|
391
|
+
if (selected === null) return null;
|
|
392
|
+
|
|
393
|
+
if (selected === `+ Add ${level} scenario`) {
|
|
394
|
+
const title = await promptBounded(ctx, `Title for the new ${level} scenario`, SCENARIO_TITLE_MAX_LOCAL);
|
|
395
|
+
if (title === null) return null;
|
|
396
|
+
const id = slugifyUltraPlanId(title);
|
|
397
|
+
const result = addScenario(current, { stack: stackId, domainId, level }, { id, title });
|
|
398
|
+
if (!result.ok) {
|
|
399
|
+
if (result.reason.code === "duplicate-id") {
|
|
400
|
+
ctx.ui.notify(`duplicate scenario id: ${id}`, "warning");
|
|
401
|
+
} else {
|
|
402
|
+
ctx.ui.notify(`could not add scenario: ${result.reason.code}`, "warning");
|
|
403
|
+
}
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
current = result.draft;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const renameMatch = /^✎ Rename (.+)$/.exec(selected);
|
|
411
|
+
if (renameMatch) {
|
|
412
|
+
const scenarioId = renameMatch[1];
|
|
413
|
+
const newTitle = await promptBounded(ctx, `New title for ${scenarioId}`, SCENARIO_TITLE_MAX_LOCAL);
|
|
414
|
+
if (newTitle === null) return null;
|
|
415
|
+
const result = renameScenario(current, { stack: stackId, domainId, level, scenarioId }, { title: newTitle });
|
|
416
|
+
if (!result.ok) {
|
|
417
|
+
ctx.ui.notify(`could not rename: ${result.reason.code}`, "warning");
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
current = result.draft;
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const removeMatch = /^− Remove (.+)$/.exec(selected);
|
|
425
|
+
if (removeMatch) {
|
|
426
|
+
const scenarioId = removeMatch[1];
|
|
427
|
+
const confirmed = await confirmDestructive(ctx, "Remove scenario?", `Remove ${scenarioId}?`);
|
|
428
|
+
if (!confirmed) continue;
|
|
429
|
+
const result = removeScenario(current, { stack: stackId, domainId, level, scenarioId });
|
|
430
|
+
if (!result.ok) {
|
|
431
|
+
ctx.ui.notify(`could not remove: ${result.reason.code}`, "warning");
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
current = result.draft;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (selected === `✓ Done with ${level}`) {
|
|
439
|
+
return current;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
type ReviewOutcome =
|
|
445
|
+
| { kind: "approved"; draft: UltraPlanAuthoredDraft }
|
|
446
|
+
| { kind: "cancelled" }
|
|
447
|
+
| { kind: "discarded" }
|
|
448
|
+
| { kind: "edited"; draft: UltraPlanAuthoredDraft };
|
|
449
|
+
|
|
450
|
+
async function runReviewLoop(
|
|
451
|
+
ctx: PlatformContext,
|
|
452
|
+
draft: UltraPlanAuthoredDraft,
|
|
453
|
+
deps: AuthoringDependencies,
|
|
454
|
+
): Promise<ReviewOutcome> {
|
|
455
|
+
const readiness = isDraftReadyToPersist(draft);
|
|
456
|
+
const reviewLines = renderUltraPlanAuthoredDraft(draft);
|
|
457
|
+
const options: string[] = [];
|
|
458
|
+
if (readiness.ok) options.push("✓ Approve & save");
|
|
459
|
+
options.push("✎ Edit title & goal");
|
|
460
|
+
for (const stack of draft.stacks) {
|
|
461
|
+
options.push(`✎ Edit ${stack.stack}.applicability`);
|
|
462
|
+
for (const domain of stack.domains) {
|
|
463
|
+
options.push(`✎ Edit ${stack.stack}.${domain.id}`);
|
|
464
|
+
options.push(`✎ Edit ${stack.stack}.${domain.id}.scenarios`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
options.push("✗ Discard");
|
|
468
|
+
|
|
469
|
+
const selected = await ctx.ui.select(reviewLines.join("\n"), options);
|
|
470
|
+
if (selected === null) return { kind: "cancelled" };
|
|
471
|
+
|
|
472
|
+
if (selected === "✓ Approve & save") {
|
|
473
|
+
return { kind: "approved", draft };
|
|
474
|
+
}
|
|
475
|
+
if (selected === "✎ Edit title & goal") {
|
|
476
|
+
const title = await promptBounded(ctx, "Ultraplan title", SESSION_TITLE_MAX, {
|
|
477
|
+
placeholder: draft.title,
|
|
478
|
+
});
|
|
479
|
+
if (title === null) return { kind: "edited", draft };
|
|
480
|
+
const goal = await promptBounded(ctx, "One-line goal", SESSION_GOAL_MAX, {
|
|
481
|
+
placeholder: draft.goal,
|
|
482
|
+
});
|
|
483
|
+
if (goal === null) return { kind: "edited", draft };
|
|
484
|
+
const updated = setSessionTitleAndGoal(draft, { title, goal });
|
|
485
|
+
return { kind: "edited", draft: updated.ok ? updated.draft : draft };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const applicabilityMatch = /^✎ Edit (.+)\.applicability$/.exec(selected);
|
|
489
|
+
if (applicabilityMatch) {
|
|
490
|
+
const stackId = applicabilityMatch[1] as UltraPlanStackId;
|
|
491
|
+
const applicability = await promptStackApplicability(ctx, stackId);
|
|
492
|
+
if (applicability === null) return { kind: "edited", draft };
|
|
493
|
+
const current = draft.stacks.find((s) => s.stack === stackId)!;
|
|
494
|
+
const isDestructive = current.applicability === "applicable" && applicability === "not-applicable" && current.domains.length > 0;
|
|
495
|
+
if (isDestructive) {
|
|
496
|
+
const scenarioCount = current.domains.reduce((acc, d) => acc + d.unit.length + d.integration.length + d.e2e.length, 0);
|
|
497
|
+
const confirmed = await confirmDestructive(
|
|
498
|
+
ctx,
|
|
499
|
+
`${stackId} will lose ${current.domains.length} domain(s) and ${scenarioCount} scenario(s). Continue?`,
|
|
500
|
+
`Change ${stackId} applicability`,
|
|
501
|
+
{ keep: "Keep", yes: "Yes, change" },
|
|
502
|
+
);
|
|
503
|
+
if (!confirmed) return { kind: "edited", draft };
|
|
504
|
+
}
|
|
505
|
+
const updated = setStackApplicability(draft, stackId, applicability);
|
|
506
|
+
return { kind: "edited", draft: updated.ok ? updated.draft : draft };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const scenariosMatch = /^✎ Edit (.+)\.(.+)\.scenarios$/.exec(selected);
|
|
510
|
+
if (scenariosMatch) {
|
|
511
|
+
const stackId = scenariosMatch[1] as UltraPlanStackId;
|
|
512
|
+
const domainId = scenariosMatch[2];
|
|
513
|
+
const next = await runScenariosLoop(ctx, draft, stackId, domainId, deps);
|
|
514
|
+
return next === null ? { kind: "cancelled" } : { kind: "edited", draft: next };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const domainMatch = /^✎ Edit (.+)\.(.+)$/.exec(selected);
|
|
518
|
+
if (domainMatch) {
|
|
519
|
+
const stackId = domainMatch[1] as UltraPlanStackId;
|
|
520
|
+
const next = await runDomainLoop(ctx, draft, stackId, deps);
|
|
521
|
+
return next === null ? { kind: "cancelled" } : { kind: "edited", draft: next };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (selected === "✗ Discard") {
|
|
525
|
+
const confirmed = await confirmDestructive(ctx, "Discard?", "No files have been written yet. Throw away this draft?", { keep: "Keep editing", yes: "Yes, discard" });
|
|
526
|
+
return confirmed ? { kind: "discarded" } : { kind: "edited", draft };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return { kind: "edited", draft };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function runPersist(
|
|
533
|
+
platform: Platform,
|
|
534
|
+
ctx: PlatformContext,
|
|
535
|
+
initialDraft: UltraPlanAuthoredDraft,
|
|
536
|
+
deps: AuthoringDependencies,
|
|
537
|
+
): Promise<AuthoringResult> {
|
|
538
|
+
let draft = initialDraft;
|
|
539
|
+
let attempts = 0;
|
|
540
|
+
while (true) {
|
|
541
|
+
attempts += 1;
|
|
542
|
+
const authored = draftToAuthoredArtifact(draft, deps.now());
|
|
543
|
+
const projectName = getUltraplanProjectName(ctx.cwd);
|
|
544
|
+
const manifest = draftToManifest(draft, projectName, deps.now());
|
|
545
|
+
const persistResult = deps.persist({ paths: platform.paths, cwd: ctx.cwd, authored, manifest });
|
|
546
|
+
if (persistResult.ok) {
|
|
547
|
+
if (persistResult.reclaimed) {
|
|
548
|
+
ctx.ui.notify("Cleaning up prior aborted session id", "info");
|
|
549
|
+
}
|
|
550
|
+
ctx.ui.notify(`Ultraplan session '${draft.title}' saved (${draft.sessionId})`, "info");
|
|
551
|
+
return {
|
|
552
|
+
ok: true,
|
|
553
|
+
sessionId: draft.sessionId,
|
|
554
|
+
paths: {
|
|
555
|
+
authored: persistResult.authoredPath,
|
|
556
|
+
manifest: persistResult.manifestPath,
|
|
557
|
+
indexEntry: persistResult.indexPath,
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
if (persistResult.error.kind === "session-id-exists") {
|
|
562
|
+
if (attempts >= 2) {
|
|
563
|
+
const synthesized: UltraPlanStorageError = {
|
|
564
|
+
kind: "io",
|
|
565
|
+
path: getUltraplanSessionDir(platform.paths, ctx.cwd, draft.sessionId),
|
|
566
|
+
message: "session id collision after retry",
|
|
567
|
+
};
|
|
568
|
+
return { ok: false, failure: { kind: "persist-failed", error: synthesized, partial: [] } };
|
|
569
|
+
}
|
|
570
|
+
const reroll = setSessionId(draft, deps.newSessionId());
|
|
571
|
+
if (!reroll.ok) {
|
|
572
|
+
return { ok: false, failure: { kind: "cancelled" } };
|
|
573
|
+
}
|
|
574
|
+
draft = reroll.draft;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (persistResult.error.kind === "index-invalid") {
|
|
578
|
+
ctx.ui.notify(`Persist failed: existing ultraplan index.json is invalid (${persistResult.error.error.message})`, "error");
|
|
579
|
+
return { ok: false, failure: { kind: "persist-failed", error: persistResult.error.error, partial: [] } };
|
|
580
|
+
}
|
|
581
|
+
if (persistResult.error.kind === "storage-error") {
|
|
582
|
+
ctx.ui.notify(`Persist failed: ${persistResult.error.error.message}`, "error");
|
|
583
|
+
return { ok: false, failure: { kind: "persist-failed", error: persistResult.error.error, partial: persistResult.error.written } };
|
|
584
|
+
}
|
|
585
|
+
return { ok: false, failure: { kind: "cancelled" } };
|
|
586
|
+
}
|
|
587
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export interface UltraPlanBatchSupervisorWorktreeState {
|
|
2
|
+
headAttached: boolean;
|
|
3
|
+
branchName: string | null;
|
|
4
|
+
dirtyTracked: boolean;
|
|
5
|
+
inProgressOperation: boolean;
|
|
6
|
+
headSha: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface UltraPlanBatchMergeDeps {
|
|
10
|
+
inspectSupervisorWorktree(): UltraPlanBatchSupervisorWorktreeState;
|
|
11
|
+
mergeBranch(branchName: string):
|
|
12
|
+
| { ok: true; newBaseHead: string }
|
|
13
|
+
| { ok: false; summary: string };
|
|
14
|
+
cleanupWorktree(worktreePath: string):
|
|
15
|
+
| { ok: true }
|
|
16
|
+
| { ok: false; summary: string };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UltraPlanBatchMergeInput {
|
|
20
|
+
supervisorBranch: string;
|
|
21
|
+
currentBaseHead: string;
|
|
22
|
+
branchName: string;
|
|
23
|
+
worktreePath: string;
|
|
24
|
+
deps: UltraPlanBatchMergeDeps;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type UltraPlanBatchMergeResult =
|
|
28
|
+
| {
|
|
29
|
+
kind: "merged";
|
|
30
|
+
currentBaseHead: string;
|
|
31
|
+
worktreePath: string | null;
|
|
32
|
+
cleanupWarning: string | null;
|
|
33
|
+
countsAgainstParallelism: false;
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
kind: "blocked";
|
|
37
|
+
code: "base-drift" | "project-identity-failed" | "supervisor-worktree-invalid" | "merge-blocked";
|
|
38
|
+
currentBaseHead: string;
|
|
39
|
+
worktreePath: string;
|
|
40
|
+
summary: string;
|
|
41
|
+
countsAgainstParallelism: false;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function block(
|
|
45
|
+
code: Extract<UltraPlanBatchMergeResult, { kind: "blocked" }>["code"],
|
|
46
|
+
input: UltraPlanBatchMergeInput,
|
|
47
|
+
summary: string,
|
|
48
|
+
): UltraPlanBatchMergeResult {
|
|
49
|
+
return {
|
|
50
|
+
kind: "blocked",
|
|
51
|
+
code,
|
|
52
|
+
currentBaseHead: input.currentBaseHead,
|
|
53
|
+
worktreePath: input.worktreePath,
|
|
54
|
+
summary,
|
|
55
|
+
countsAgainstParallelism: false,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function mergeUltraPlanBatchWorktree(input: UltraPlanBatchMergeInput): UltraPlanBatchMergeResult {
|
|
60
|
+
const supervisor = input.deps.inspectSupervisorWorktree();
|
|
61
|
+
if (!supervisor.headAttached) {
|
|
62
|
+
return block("supervisor-worktree-invalid", input, "Supervisor worktree HEAD is detached.");
|
|
63
|
+
}
|
|
64
|
+
if (supervisor.branchName !== input.supervisorBranch) {
|
|
65
|
+
return block(
|
|
66
|
+
"supervisor-worktree-invalid",
|
|
67
|
+
input,
|
|
68
|
+
`Supervisor worktree is on ${supervisor.branchName ?? "<unknown>"}, expected ${input.supervisorBranch}.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (supervisor.dirtyTracked) {
|
|
72
|
+
return block("supervisor-worktree-invalid", input, "Supervisor worktree has tracked changes.");
|
|
73
|
+
}
|
|
74
|
+
if (supervisor.inProgressOperation) {
|
|
75
|
+
return block("supervisor-worktree-invalid", input, "Supervisor worktree has an in-progress git operation.");
|
|
76
|
+
}
|
|
77
|
+
if (supervisor.headSha !== input.currentBaseHead) {
|
|
78
|
+
return block(
|
|
79
|
+
"base-drift",
|
|
80
|
+
input,
|
|
81
|
+
`Supervisor branch advanced from ${input.currentBaseHead} to ${supervisor.headSha} before merge.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const merge = input.deps.mergeBranch(input.branchName);
|
|
86
|
+
if (!merge.ok) {
|
|
87
|
+
return block("merge-blocked", input, merge.summary);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const cleanup = input.deps.cleanupWorktree(input.worktreePath);
|
|
91
|
+
return {
|
|
92
|
+
kind: "merged",
|
|
93
|
+
currentBaseHead: merge.newBaseHead,
|
|
94
|
+
worktreePath: cleanup.ok ? null : input.worktreePath,
|
|
95
|
+
cleanupWarning: cleanup.ok ? null : cleanup.summary,
|
|
96
|
+
countsAgainstParallelism: false,
|
|
97
|
+
};
|
|
98
|
+
}
|