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,368 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { PlatformPaths } from "../../platform/types.js";
|
|
4
|
+
import type {
|
|
5
|
+
UltraPlanHookObservation,
|
|
6
|
+
UltraPlanPendingMutation,
|
|
7
|
+
UltraPlanRuntimeTracker,
|
|
8
|
+
UltraPlanSessionMigrationRecord,
|
|
9
|
+
UltraPlanStorageError,
|
|
10
|
+
UltraPlanStorageResult,
|
|
11
|
+
} from "../../types.js";
|
|
12
|
+
import {
|
|
13
|
+
validateUltraPlanRuntimeTracker,
|
|
14
|
+
validateUltraPlanSessionMigrationRecord,
|
|
15
|
+
} from "../contracts.js";
|
|
16
|
+
import {
|
|
17
|
+
getUltraplanExecutionLogPath,
|
|
18
|
+
getUltraplanHooksLogPath,
|
|
19
|
+
getUltraplanMigrationRecordPath,
|
|
20
|
+
getUltraplanRuntimeTrackerPath,
|
|
21
|
+
} from "../project-paths.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Slice-2 runtime storage seam.
|
|
25
|
+
*
|
|
26
|
+
* This module owns the durable read/write path for `runtime-tracker.json` and `migration.json`.
|
|
27
|
+
* Task 2.3 will grow this module to additionally own `hooks-log.jsonl`, pendingMutation staging,
|
|
28
|
+
* and reconciliation of partial writes against the manifest. For Slice 2/1.4 it provides the
|
|
29
|
+
* round-trip primitives that the storage wrappers and the migration engine depend on.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
function success<T>(value: T): UltraPlanStorageResult<T> {
|
|
33
|
+
return { ok: true, value };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function failure(
|
|
37
|
+
pathname: string,
|
|
38
|
+
kind: UltraPlanStorageError["kind"],
|
|
39
|
+
message: string,
|
|
40
|
+
details?: string[],
|
|
41
|
+
): UltraPlanStorageResult<never> {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
error: {
|
|
45
|
+
kind,
|
|
46
|
+
path: pathname,
|
|
47
|
+
message,
|
|
48
|
+
...(details ? { details } : {}),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ensureDir(filePath: string): void {
|
|
54
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readJsonFile(filePath: string): UltraPlanStorageResult<unknown> {
|
|
58
|
+
if (!fs.existsSync(filePath)) {
|
|
59
|
+
return failure(filePath, "missing", `Artifact not found: ${filePath}`);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return success(JSON.parse(fs.readFileSync(filePath, "utf8")));
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return failure(
|
|
65
|
+
filePath,
|
|
66
|
+
"invalid-json",
|
|
67
|
+
error instanceof Error ? error.message : `Invalid JSON in ${filePath}`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Atomic write: write to a sibling `*.tmp` file, then rename onto the destination. Prevents a
|
|
74
|
+
* half-written tracker from surviving a crash and being observed by the loader.
|
|
75
|
+
*/
|
|
76
|
+
function writeJsonAtomic(filePath: string, payload: unknown): UltraPlanStorageResult<string> {
|
|
77
|
+
try {
|
|
78
|
+
ensureDir(filePath);
|
|
79
|
+
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
80
|
+
fs.writeFileSync(tmpPath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
81
|
+
fs.renameSync(tmpPath, filePath);
|
|
82
|
+
return success(filePath);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return failure(
|
|
85
|
+
filePath,
|
|
86
|
+
"io",
|
|
87
|
+
error instanceof Error ? error.message : `Unable to write ${filePath}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Load the runtime tracker for a given session. Missing tracker is a first-class, non-error
|
|
94
|
+
* state — callers treat it as "no prior runtime state."
|
|
95
|
+
*/
|
|
96
|
+
export function loadTracker(
|
|
97
|
+
paths: PlatformPaths,
|
|
98
|
+
cwd: string,
|
|
99
|
+
sessionId: string,
|
|
100
|
+
): UltraPlanStorageResult<UltraPlanRuntimeTracker> {
|
|
101
|
+
const filePath = getUltraplanRuntimeTrackerPath(paths, cwd, sessionId);
|
|
102
|
+
const parsed = readJsonFile(filePath);
|
|
103
|
+
if (!parsed.ok) return parsed;
|
|
104
|
+
|
|
105
|
+
const validation = validateUltraPlanRuntimeTracker(parsed.value);
|
|
106
|
+
if (!validation.ok) {
|
|
107
|
+
return failure(
|
|
108
|
+
filePath,
|
|
109
|
+
"validation-error",
|
|
110
|
+
`Runtime tracker failed schema validation: ${filePath}`,
|
|
111
|
+
validation.errors,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return success(validation.value);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Save the runtime tracker atomically after semantic and schema validation. Writes never observe
|
|
119
|
+
* a half-finalized tracker because the destination rename happens in one filesystem step.
|
|
120
|
+
*/
|
|
121
|
+
export function saveTrackerAtomic(
|
|
122
|
+
paths: PlatformPaths,
|
|
123
|
+
cwd: string,
|
|
124
|
+
sessionId: string,
|
|
125
|
+
tracker: UltraPlanRuntimeTracker,
|
|
126
|
+
): UltraPlanStorageResult<string> {
|
|
127
|
+
const filePath = getUltraplanRuntimeTrackerPath(paths, cwd, sessionId);
|
|
128
|
+
// Normalize the applied-fingerprint ledger before validation: the invariant enforced on
|
|
129
|
+
// disk is that it contains no duplicates. Callers may hand in repeated fingerprints from
|
|
130
|
+
// replay flows; we dedupe once here so the persisted tracker stays coherent.
|
|
131
|
+
const normalized: UltraPlanRuntimeTracker = {
|
|
132
|
+
...tracker,
|
|
133
|
+
appliedFingerprints: dedupeInOrder(tracker.appliedFingerprints),
|
|
134
|
+
};
|
|
135
|
+
const validation = validateUltraPlanRuntimeTracker(normalized);
|
|
136
|
+
if (!validation.ok) {
|
|
137
|
+
return failure(
|
|
138
|
+
filePath,
|
|
139
|
+
"validation-error",
|
|
140
|
+
`Runtime tracker failed schema validation: ${filePath}`,
|
|
141
|
+
validation.errors,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return writeJsonAtomic(filePath, validation.value);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function dedupeInOrder(values: readonly string[]): string[] {
|
|
148
|
+
const seen = new Set<string>();
|
|
149
|
+
const out: string[] = [];
|
|
150
|
+
for (const v of values) {
|
|
151
|
+
if (!seen.has(v)) {
|
|
152
|
+
seen.add(v);
|
|
153
|
+
out.push(v);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function loadMigrationRecord(
|
|
160
|
+
paths: PlatformPaths,
|
|
161
|
+
cwd: string,
|
|
162
|
+
sessionId: string,
|
|
163
|
+
): UltraPlanStorageResult<UltraPlanSessionMigrationRecord> {
|
|
164
|
+
const filePath = getUltraplanMigrationRecordPath(paths, cwd, sessionId);
|
|
165
|
+
const parsed = readJsonFile(filePath);
|
|
166
|
+
if (!parsed.ok) return parsed;
|
|
167
|
+
|
|
168
|
+
const validation = validateUltraPlanSessionMigrationRecord(parsed.value);
|
|
169
|
+
if (!validation.ok) {
|
|
170
|
+
return failure(
|
|
171
|
+
filePath,
|
|
172
|
+
"validation-error",
|
|
173
|
+
`Migration record failed schema validation: ${filePath}`,
|
|
174
|
+
validation.errors,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return success(validation.value);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function saveMigrationRecord(
|
|
181
|
+
paths: PlatformPaths,
|
|
182
|
+
cwd: string,
|
|
183
|
+
sessionId: string,
|
|
184
|
+
record: UltraPlanSessionMigrationRecord,
|
|
185
|
+
): UltraPlanStorageResult<string> {
|
|
186
|
+
const filePath = getUltraplanMigrationRecordPath(paths, cwd, sessionId);
|
|
187
|
+
const validation = validateUltraPlanSessionMigrationRecord(record);
|
|
188
|
+
if (!validation.ok) {
|
|
189
|
+
return failure(
|
|
190
|
+
filePath,
|
|
191
|
+
"validation-error",
|
|
192
|
+
`Migration record failed schema validation: ${filePath}`,
|
|
193
|
+
validation.errors,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return writeJsonAtomic(filePath, validation.value);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Hooks log (append-only JSONL)
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Append a normalized hook observation to `hooks-log.jsonl`. This is the audit trail the
|
|
206
|
+
* reducer reads on replay. The append is suppressed for observations whose `fingerprint` has
|
|
207
|
+
* already been persisted into the tracker's `appliedFingerprints` set — that is what makes
|
|
208
|
+
* replay a persisted no-op on both the tracker AND the hooks log.
|
|
209
|
+
*/
|
|
210
|
+
export function appendHookLog(
|
|
211
|
+
paths: PlatformPaths,
|
|
212
|
+
cwd: string,
|
|
213
|
+
sessionId: string,
|
|
214
|
+
observation: UltraPlanHookObservation,
|
|
215
|
+
): UltraPlanStorageResult<string> {
|
|
216
|
+
const trackerResult = loadTracker(paths, cwd, sessionId);
|
|
217
|
+
if (trackerResult.ok) {
|
|
218
|
+
if (trackerResult.value.appliedFingerprints.includes(observation.fingerprint)) {
|
|
219
|
+
// Observation already applied; replay is a persisted no-op.
|
|
220
|
+
return success(getUltraplanHooksLogPath(paths, cwd, sessionId));
|
|
221
|
+
}
|
|
222
|
+
} else if (trackerResult.error.kind !== "missing") {
|
|
223
|
+
// Tracker exists but is unreadable; fail closed.
|
|
224
|
+
return trackerResult;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const logPath = getUltraplanHooksLogPath(paths, cwd, sessionId);
|
|
228
|
+
try {
|
|
229
|
+
ensureDir(logPath);
|
|
230
|
+
fs.appendFileSync(logPath, `${JSON.stringify(observation)}\n`);
|
|
231
|
+
return success(logPath);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
return failure(
|
|
234
|
+
logPath,
|
|
235
|
+
"io",
|
|
236
|
+
error instanceof Error ? error.message : `Unable to append ${logPath}`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function appendExecutionLog(
|
|
242
|
+
paths: PlatformPaths,
|
|
243
|
+
cwd: string,
|
|
244
|
+
sessionId: string,
|
|
245
|
+
entry: Record<string, unknown>,
|
|
246
|
+
): UltraPlanStorageResult<string> {
|
|
247
|
+
const logPath = getUltraplanExecutionLogPath(paths, cwd, sessionId);
|
|
248
|
+
const observationFingerprint = typeof entry.observationFingerprint === "string"
|
|
249
|
+
? entry.observationFingerprint
|
|
250
|
+
: null;
|
|
251
|
+
try {
|
|
252
|
+
ensureDir(logPath);
|
|
253
|
+
if (observationFingerprint && executionLogContainsObservation(logPath, observationFingerprint)) {
|
|
254
|
+
return success(logPath);
|
|
255
|
+
}
|
|
256
|
+
fs.appendFileSync(logPath, `${JSON.stringify(entry)}\n`);
|
|
257
|
+
return success(logPath);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
return failure(
|
|
260
|
+
logPath,
|
|
261
|
+
"io",
|
|
262
|
+
error instanceof Error ? error.message : `Unable to append ${logPath}`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function executionLogContainsObservation(logPath: string, observationFingerprint: string): boolean {
|
|
268
|
+
if (!fs.existsSync(logPath)) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const line of fs.readFileSync(logPath, "utf8").split("\n")) {
|
|
273
|
+
if (!line) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const parsed = JSON.parse(line) as Record<string, unknown>;
|
|
279
|
+
if (parsed.observationFingerprint === observationFingerprint) {
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
} catch {
|
|
283
|
+
// Ignore malformed historical lines and keep scanning for a matching durable entry.
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// Pending-mutation staging and reconciliation
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Stage a pending mutation atomically: load the tracker, set `pendingMutation`, and rewrite.
|
|
296
|
+
* Caller provides the exact `UltraPlanPendingMutation` record (attemptId, plan, expected
|
|
297
|
+
* manifest fingerprint, stagedAt) per spec §durability order item 3.
|
|
298
|
+
*/
|
|
299
|
+
export function stagePendingMutation(
|
|
300
|
+
paths: PlatformPaths,
|
|
301
|
+
cwd: string,
|
|
302
|
+
sessionId: string,
|
|
303
|
+
pending: UltraPlanPendingMutation,
|
|
304
|
+
): UltraPlanStorageResult<string> {
|
|
305
|
+
const loaded = loadTracker(paths, cwd, sessionId);
|
|
306
|
+
if (!loaded.ok) return loaded;
|
|
307
|
+
const next: UltraPlanRuntimeTracker = {
|
|
308
|
+
...loaded.value,
|
|
309
|
+
pendingMutation: pending,
|
|
310
|
+
updatedAt: pending.stagedAt,
|
|
311
|
+
};
|
|
312
|
+
return saveTrackerAtomic(paths, cwd, sessionId, next);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Clear `pendingMutation` atomically. Used at the end of the durability order (item 5) when
|
|
317
|
+
* the manifest write has already landed and the attempt is committed to the finalized ledger.
|
|
318
|
+
*/
|
|
319
|
+
export function clearPendingMutation(
|
|
320
|
+
paths: PlatformPaths,
|
|
321
|
+
cwd: string,
|
|
322
|
+
sessionId: string,
|
|
323
|
+
): UltraPlanStorageResult<string> {
|
|
324
|
+
const loaded = loadTracker(paths, cwd, sessionId);
|
|
325
|
+
if (!loaded.ok) return loaded;
|
|
326
|
+
if (loaded.value.pendingMutation === null) {
|
|
327
|
+
return success(getUltraplanRuntimeTrackerPath(paths, cwd, sessionId));
|
|
328
|
+
}
|
|
329
|
+
const next: UltraPlanRuntimeTracker = {
|
|
330
|
+
...loaded.value,
|
|
331
|
+
pendingMutation: null,
|
|
332
|
+
};
|
|
333
|
+
return saveTrackerAtomic(paths, cwd, sessionId, next);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export type UltraPlanReconciliationOutcome =
|
|
337
|
+
| { kind: "no-pending" }
|
|
338
|
+
| { kind: "committed" }
|
|
339
|
+
| { kind: "replay-needed"; pending: UltraPlanPendingMutation };
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Reconcile a staged `pendingMutation` against the actual manifest contents on resume.
|
|
343
|
+
*
|
|
344
|
+
* - When no pending mutation is present, returns `no-pending`.
|
|
345
|
+
* - When the manifest already matches the staged `expectedManifestFingerprint`, the pending
|
|
346
|
+
* mutation is considered committed: clear it and return `committed`.
|
|
347
|
+
* - Otherwise return `replay-needed` and leave the pending mutation in place so the caller can
|
|
348
|
+
* replay the mutation plan idempotently.
|
|
349
|
+
*/
|
|
350
|
+
export function reconcilePendingMutationAgainstManifest(
|
|
351
|
+
paths: PlatformPaths,
|
|
352
|
+
cwd: string,
|
|
353
|
+
sessionId: string,
|
|
354
|
+
actualManifestFingerprint: string,
|
|
355
|
+
): UltraPlanStorageResult<UltraPlanReconciliationOutcome> {
|
|
356
|
+
const loaded = loadTracker(paths, cwd, sessionId);
|
|
357
|
+
if (!loaded.ok) return loaded;
|
|
358
|
+
const pending = loaded.value.pendingMutation;
|
|
359
|
+
if (pending === null) {
|
|
360
|
+
return success({ kind: "no-pending" });
|
|
361
|
+
}
|
|
362
|
+
if (pending.expectedManifestFingerprint === actualManifestFingerprint) {
|
|
363
|
+
const cleared = clearPendingMutation(paths, cwd, sessionId);
|
|
364
|
+
if (!cleared.ok) return cleared;
|
|
365
|
+
return success({ kind: "committed" });
|
|
366
|
+
}
|
|
367
|
+
return success({ kind: "replay-needed", pending });
|
|
368
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
UltraPlanAuthoredArtifact,
|
|
3
|
+
UltraPlanCursor,
|
|
4
|
+
UltraPlanManifest,
|
|
5
|
+
UltraPlanReviewStatus,
|
|
6
|
+
UltraPlanScenario,
|
|
7
|
+
UltraPlanScenarioStatus,
|
|
8
|
+
UltraPlanSessionBucket,
|
|
9
|
+
UltraPlanSessionState,
|
|
10
|
+
UltraPlanSessionSummary,
|
|
11
|
+
UltraPlanStack,
|
|
12
|
+
} from "../types.js";
|
|
13
|
+
import { hasRequiredUltraPlanScenarioProof } from "./contracts.js";
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
export interface UltraPlanVisibleSession extends UltraPlanSessionSummary {
|
|
17
|
+
bucket: UltraPlanSessionBucket;
|
|
18
|
+
idleReasonLabel: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface UltraPlanResolvedCursor {
|
|
22
|
+
cursor: UltraPlanCursor;
|
|
23
|
+
source: "persisted" | "recomputed";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const TERMINAL_SCENARIO_STATUSES = new Set<UltraPlanScenarioStatus>([
|
|
27
|
+
"green-proved",
|
|
28
|
+
"review-passed",
|
|
29
|
+
"done",
|
|
30
|
+
]);
|
|
31
|
+
const ONGOING_CURSOR_STATUSES = new Set([
|
|
32
|
+
"red-running",
|
|
33
|
+
"green-running",
|
|
34
|
+
"in-review",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
export function mapUltraPlanStateToBucket(state: UltraPlanSessionState): UltraPlanSessionBucket {
|
|
39
|
+
switch (state) {
|
|
40
|
+
case "ready":
|
|
41
|
+
return "pending";
|
|
42
|
+
case "running":
|
|
43
|
+
return "ongoing";
|
|
44
|
+
case "blocked":
|
|
45
|
+
case "awaiting-user":
|
|
46
|
+
return "idle";
|
|
47
|
+
case "complete":
|
|
48
|
+
case "discarded":
|
|
49
|
+
return "done";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getUltraPlanIdleReasonLabel(session: UltraPlanSessionSummary): string | null {
|
|
54
|
+
if (session.state === "awaiting-user") {
|
|
55
|
+
return session.blocker?.message ? `Awaiting user: ${session.blocker.message}` : "Awaiting user input";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (session.state === "blocked") {
|
|
59
|
+
return session.blocker?.message ?? "Blocked";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resolveUltraPlanSessionBucket(
|
|
66
|
+
session: UltraPlanSessionSummary,
|
|
67
|
+
resolved?: UltraPlanResolvedCursor,
|
|
68
|
+
): UltraPlanSessionBucket {
|
|
69
|
+
const cursor = resolved?.cursor ?? session.cursor;
|
|
70
|
+
|
|
71
|
+
if (session.state === "discarded") {
|
|
72
|
+
return "done";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!resolved && session.state === "complete") {
|
|
76
|
+
return "done";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (cursor?.targetType === "session" && cursor.status === "complete") {
|
|
80
|
+
return "done";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (session.state === "awaiting-user") {
|
|
84
|
+
return "idle";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (session.state === "blocked" || cursor?.status === "blocked") {
|
|
88
|
+
return "idle";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (session.state === "running" || (cursor && ONGOING_CURSOR_STATUSES.has(cursor.status))) {
|
|
92
|
+
return "ongoing";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return "pending";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getVisibleUltraPlanSessions(
|
|
99
|
+
sessions: UltraPlanSessionSummary[],
|
|
100
|
+
options?: { includeDone?: boolean },
|
|
101
|
+
): UltraPlanVisibleSession[] {
|
|
102
|
+
const includeDone = options?.includeDone ?? false;
|
|
103
|
+
|
|
104
|
+
return sessions
|
|
105
|
+
.map((session) => ({
|
|
106
|
+
...session,
|
|
107
|
+
bucket: resolveUltraPlanSessionBucket(session),
|
|
108
|
+
idleReasonLabel: getUltraPlanIdleReasonLabel(session),
|
|
109
|
+
}))
|
|
110
|
+
.filter((session) => includeDone || session.bucket !== "done");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function isUltraPlanCursorSummaryValid(
|
|
114
|
+
manifest: UltraPlanManifest,
|
|
115
|
+
authored: UltraPlanAuthoredArtifact,
|
|
116
|
+
): boolean {
|
|
117
|
+
if (!manifest.cursor) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return sameCursor(manifest.cursor, recomputeUltraPlanCursor(manifest, authored));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function resolveUltraPlanCurrentCursor(
|
|
125
|
+
manifest: UltraPlanManifest,
|
|
126
|
+
authored: UltraPlanAuthoredArtifact,
|
|
127
|
+
): UltraPlanResolvedCursor {
|
|
128
|
+
const recomputed = recomputeUltraPlanCursor(manifest, authored);
|
|
129
|
+
if (manifest.cursor && sameCursor(manifest.cursor, recomputed)) {
|
|
130
|
+
return { cursor: recomputed, source: "persisted" };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { cursor: recomputed, source: "recomputed" };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function recomputeUltraPlanCursor(manifest: UltraPlanManifest, authored: UltraPlanAuthoredArtifact): UltraPlanCursor {
|
|
137
|
+
for (const stack of authored.stacks) {
|
|
138
|
+
if (stack.applicability === "not-applicable") {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const stackCursor = recomputeStackCursor(manifest, stack);
|
|
143
|
+
if (stackCursor) {
|
|
144
|
+
return stackCursor;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
targetType: "session",
|
|
150
|
+
stack: null,
|
|
151
|
+
domainId: null,
|
|
152
|
+
level: null,
|
|
153
|
+
scenarioId: null,
|
|
154
|
+
phase: "complete",
|
|
155
|
+
status: "complete",
|
|
156
|
+
summary: "Session complete",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function recomputeStackCursor(manifest: UltraPlanManifest, stack: UltraPlanStack): UltraPlanCursor | null {
|
|
161
|
+
for (const domain of stack.domains) {
|
|
162
|
+
const scenarioCursor = findFirstScenarioCursor(domain.unit)
|
|
163
|
+
?? findFirstScenarioCursor(domain.integration)
|
|
164
|
+
?? findFirstScenarioCursor(domain.e2e);
|
|
165
|
+
|
|
166
|
+
if (scenarioCursor) {
|
|
167
|
+
return scenarioCursor;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (stack.agentSlots.domainReviewEnabled && domain.review.enabled) {
|
|
171
|
+
if (!stack.agentSlots.domainReviewer) {
|
|
172
|
+
return {
|
|
173
|
+
targetType: "domain-review",
|
|
174
|
+
stack: stack.stack,
|
|
175
|
+
domainId: domain.id,
|
|
176
|
+
level: null,
|
|
177
|
+
scenarioId: null,
|
|
178
|
+
phase: "waiting",
|
|
179
|
+
status: "blocked",
|
|
180
|
+
summary: `${stack.stack} / ${domain.id} / domain review blocked — missing reviewer slot`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const reviewStatus = getDomainReviewStatus(manifest, stack.stack, domain.id) ?? "pending";
|
|
185
|
+
if (reviewStatus !== "passed") {
|
|
186
|
+
return {
|
|
187
|
+
targetType: "domain-review",
|
|
188
|
+
stack: stack.stack,
|
|
189
|
+
domainId: domain.id,
|
|
190
|
+
level: null,
|
|
191
|
+
scenarioId: null,
|
|
192
|
+
phase: reviewStatus === "blocked" ? "waiting" : "review",
|
|
193
|
+
status: reviewStatus,
|
|
194
|
+
summary: `${stack.stack} / ${domain.id} / domain review`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (stack.agentSlots.stackReviewEnabled) {
|
|
201
|
+
if (!stack.agentSlots.stackReviewer) {
|
|
202
|
+
return {
|
|
203
|
+
targetType: "stack-review",
|
|
204
|
+
stack: stack.stack,
|
|
205
|
+
domainId: null,
|
|
206
|
+
level: null,
|
|
207
|
+
scenarioId: null,
|
|
208
|
+
phase: "waiting",
|
|
209
|
+
status: "blocked",
|
|
210
|
+
summary: `${stack.stack} / stack review blocked — missing reviewer slot`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const reviewStatus = getStackReviewStatus(manifest, stack.stack) ?? "pending";
|
|
215
|
+
if (reviewStatus !== "passed") {
|
|
216
|
+
return {
|
|
217
|
+
targetType: "stack-review",
|
|
218
|
+
stack: stack.stack,
|
|
219
|
+
domainId: null,
|
|
220
|
+
level: null,
|
|
221
|
+
scenarioId: null,
|
|
222
|
+
phase: reviewStatus === "blocked" ? "waiting" : "review",
|
|
223
|
+
status: reviewStatus,
|
|
224
|
+
summary: `${stack.stack} / stack review`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function findFirstScenarioCursor(scenarios: UltraPlanScenario[]): UltraPlanCursor | null {
|
|
233
|
+
for (const scenario of scenarios) {
|
|
234
|
+
if (!TERMINAL_SCENARIO_STATUSES.has(scenario.status) || !hasRequiredUltraPlanScenarioProof(scenario)) {
|
|
235
|
+
return {
|
|
236
|
+
targetType: "scenario",
|
|
237
|
+
stack: scenario.stack,
|
|
238
|
+
domainId: scenario.domainId,
|
|
239
|
+
level: scenario.level,
|
|
240
|
+
scenarioId: scenario.id,
|
|
241
|
+
phase: getScenarioPhase(scenario.status),
|
|
242
|
+
status: scenario.status,
|
|
243
|
+
summary: `${scenario.stack} / ${scenario.domainId} / ${scenario.level} / ${scenario.title}`,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function getDomainReviewStatus(
|
|
252
|
+
manifest: UltraPlanManifest,
|
|
253
|
+
stack: UltraPlanStack["stack"],
|
|
254
|
+
domainId: string,
|
|
255
|
+
): UltraPlanReviewStatus | null {
|
|
256
|
+
return manifest.reviews.find((review) => review.type === "domain" && review.stack === stack && review.domainId === domainId)?.status ?? null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function getStackReviewStatus(manifest: UltraPlanManifest, stack: UltraPlanStack["stack"]): UltraPlanReviewStatus | null {
|
|
260
|
+
return manifest.reviews.find((review) => review.type === "stack" && review.stack === stack)?.status ?? null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getScenarioPhase(status: UltraPlanScenarioStatus): UltraPlanCursor["phase"] {
|
|
264
|
+
switch (status) {
|
|
265
|
+
case "planned":
|
|
266
|
+
case "red-running":
|
|
267
|
+
return "red";
|
|
268
|
+
case "red-proved":
|
|
269
|
+
case "green-running":
|
|
270
|
+
return "green";
|
|
271
|
+
case "in-review":
|
|
272
|
+
return "review";
|
|
273
|
+
case "blocked":
|
|
274
|
+
return "waiting";
|
|
275
|
+
case "green-proved":
|
|
276
|
+
case "review-passed":
|
|
277
|
+
case "done":
|
|
278
|
+
return "complete";
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function sameCursor(left: UltraPlanCursor, right: UltraPlanCursor): boolean {
|
|
283
|
+
return left.targetType === right.targetType
|
|
284
|
+
&& left.stack === right.stack
|
|
285
|
+
&& left.domainId === right.domainId
|
|
286
|
+
&& left.level === right.level
|
|
287
|
+
&& left.scenarioId === right.scenarioId
|
|
288
|
+
&& left.phase === right.phase
|
|
289
|
+
&& left.status === right.status
|
|
290
|
+
&& left.summary === right.summary;
|
|
291
|
+
}
|