supipowers 1.5.3 → 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 +131 -42
- 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 +18 -8
- 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 +221 -109
- package/src/review/fixer.ts +10 -6
- package/src/review/multi-agent-runner.ts +114 -13
- 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 +1 -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 +1399 -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,416 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
UltraPlanAttemptRecord,
|
|
3
|
+
UltraPlanBlockerCandidate,
|
|
4
|
+
UltraPlanCursor,
|
|
5
|
+
UltraPlanHookObservation,
|
|
6
|
+
UltraPlanMutationPlan,
|
|
7
|
+
UltraPlanProofCandidate,
|
|
8
|
+
UltraPlanReducerAction,
|
|
9
|
+
UltraPlanRuntimeTracker,
|
|
10
|
+
UltraPlanScenarioStatus,
|
|
11
|
+
} from "../../types.js";
|
|
12
|
+
import {
|
|
13
|
+
buildConflictingEvidenceBlocker,
|
|
14
|
+
buildInterruptedAttemptBlocker,
|
|
15
|
+
buildProofMissingBlocker,
|
|
16
|
+
buildUnsafeRepairRequiredBlocker,
|
|
17
|
+
} from "./blockers.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Slice-2 pure reducer.
|
|
21
|
+
*
|
|
22
|
+
* Decides, given the current state and a validated action, what the persisted mutation should
|
|
23
|
+
* be. The reducer performs no I/O; its output is consumed by the hook bridge's
|
|
24
|
+
* `applyMutationPlan` seam which runs the durability order. See approved spec §reducer outcome
|
|
25
|
+
* precedence (lines 562–572) and §transition rules.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export interface ReducerState {
|
|
29
|
+
tracker: UltraPlanRuntimeTracker;
|
|
30
|
+
cursor: UltraPlanCursor | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function reduce(state: ReducerState, action: UltraPlanReducerAction): UltraPlanMutationPlan {
|
|
34
|
+
if (state.cursor?.targetType === "session" && state.cursor.status === "complete") {
|
|
35
|
+
return buildPlan({
|
|
36
|
+
kind: "complete",
|
|
37
|
+
rationale: "cursor already resolved to session complete",
|
|
38
|
+
cursorUpdate: state.cursor,
|
|
39
|
+
sessionStateUpdate: "complete",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
switch (action.kind) {
|
|
44
|
+
case "session_started":
|
|
45
|
+
return buildPlan({ kind: "noop", rationale: "session_started observed; repair runs separately" });
|
|
46
|
+
case "attempt_started":
|
|
47
|
+
return reduceAttemptStarted(state, action);
|
|
48
|
+
case "observation_staged":
|
|
49
|
+
return reduceObservationStaged(state, action);
|
|
50
|
+
case "attempt_finalized":
|
|
51
|
+
return reduceAttemptFinalized(state, action);
|
|
52
|
+
case "session_shutdown":
|
|
53
|
+
return reduceSessionShutdown(state, action);
|
|
54
|
+
case "repair_applied":
|
|
55
|
+
return buildPlan({
|
|
56
|
+
kind: "repair",
|
|
57
|
+
rationale: action.details.reason,
|
|
58
|
+
repairActions: action.details.actions,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// attempt_started
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
function reduceAttemptStarted(
|
|
68
|
+
state: ReducerState,
|
|
69
|
+
action: Extract<UltraPlanReducerAction, { kind: "attempt_started" }>,
|
|
70
|
+
): UltraPlanMutationPlan {
|
|
71
|
+
const { observation } = action;
|
|
72
|
+
|
|
73
|
+
if (alreadyApplied(state.tracker, observation.fingerprint)) {
|
|
74
|
+
return buildPlan({
|
|
75
|
+
kind: "noop",
|
|
76
|
+
rationale: `attempt_started observation ${observation.fingerprint} already applied`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (state.tracker.activeAttempt) {
|
|
81
|
+
return buildPlan({
|
|
82
|
+
kind: "block",
|
|
83
|
+
rationale: `nested before_agent_start observed while attempt ${state.tracker.activeAttempt.attemptId} is still active`,
|
|
84
|
+
blockerUpdate: {
|
|
85
|
+
scope: "session",
|
|
86
|
+
nextValue: buildUnsafeRepairRequiredBlocker({
|
|
87
|
+
detectedAt: observation.occurredAt,
|
|
88
|
+
scope: "session",
|
|
89
|
+
reason: `active attempt ${state.tracker.activeAttempt.attemptId} must finalize before a new attempt starts`,
|
|
90
|
+
}),
|
|
91
|
+
clearedByObservationFingerprint: null,
|
|
92
|
+
},
|
|
93
|
+
sessionStateUpdate: "blocked",
|
|
94
|
+
appendObservationFingerprint: observation.fingerprint,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const cursor = state.cursor;
|
|
99
|
+
if (!cursor || !isLegalStartFromCursor(cursor)) {
|
|
100
|
+
return buildPlan({
|
|
101
|
+
kind: "block",
|
|
102
|
+
rationale: "attempt_started from a cursor that has no legal-start transition",
|
|
103
|
+
blockerUpdate: {
|
|
104
|
+
scope: "session",
|
|
105
|
+
nextValue: buildUnsafeRepairRequiredBlocker({
|
|
106
|
+
detectedAt: observation.occurredAt,
|
|
107
|
+
scope: "session",
|
|
108
|
+
reason: `cursor status ${cursor?.status ?? "null"} has no legal-start transition`,
|
|
109
|
+
}),
|
|
110
|
+
clearedByObservationFingerprint: null,
|
|
111
|
+
},
|
|
112
|
+
sessionStateUpdate: "blocked",
|
|
113
|
+
appendObservationFingerprint: observation.fingerprint,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const nextStatus = legalStartNextStatus(cursor);
|
|
118
|
+
return buildPlan({
|
|
119
|
+
kind: "start-attempt",
|
|
120
|
+
rationale: `legal start: ${cursor.status} -> ${nextStatus}`,
|
|
121
|
+
cursorUpdate: { ...cursor, status: nextStatus, phase: phaseForStatus(nextStatus, cursor.phase) },
|
|
122
|
+
sessionStateUpdate: "running",
|
|
123
|
+
appendObservationFingerprint: observation.fingerprint,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// observation_staged
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
function reduceObservationStaged(
|
|
132
|
+
state: ReducerState,
|
|
133
|
+
action: Extract<UltraPlanReducerAction, { kind: "observation_staged" }>,
|
|
134
|
+
): UltraPlanMutationPlan {
|
|
135
|
+
const { observation } = action;
|
|
136
|
+
if (alreadyApplied(state.tracker, observation.fingerprint)) {
|
|
137
|
+
return buildPlan({ kind: "noop", rationale: "observation already applied" });
|
|
138
|
+
}
|
|
139
|
+
return buildPlan({
|
|
140
|
+
kind: "stage-observation",
|
|
141
|
+
rationale: `stage observation ${observation.fingerprint}`,
|
|
142
|
+
appendObservationFingerprint: observation.fingerprint,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// attempt_finalized (precedence: conflicting -> proof -> blocker -> interrupted -> noop)
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
function reduceAttemptFinalized(
|
|
151
|
+
state: ReducerState,
|
|
152
|
+
action: Extract<UltraPlanReducerAction, { kind: "attempt_finalized" }>,
|
|
153
|
+
): UltraPlanMutationPlan {
|
|
154
|
+
const { observation, nowIso } = action;
|
|
155
|
+
if (alreadyApplied(state.tracker, observation.fingerprint)) {
|
|
156
|
+
return buildPlan({ kind: "noop", rationale: "attempt_finalized already applied" });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const active = state.tracker.activeAttempt;
|
|
160
|
+
if (!active) {
|
|
161
|
+
return buildPlan({
|
|
162
|
+
kind: "noop",
|
|
163
|
+
rationale: "attempt_finalized observed without an active attempt; treated as replay",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const proofs = active.proofCandidates;
|
|
168
|
+
const blockers = active.blockerCandidates;
|
|
169
|
+
const cursor = state.cursor;
|
|
170
|
+
|
|
171
|
+
// Rule 1 — conflicting evidence: fail closed.
|
|
172
|
+
if (proofs.length > 0 && blockers.length > 0) {
|
|
173
|
+
return buildPlan({
|
|
174
|
+
kind: "block",
|
|
175
|
+
rationale: "conflicting evidence: proof and blocker in the same attempt finalization",
|
|
176
|
+
blockerUpdate: {
|
|
177
|
+
scope: "scenario",
|
|
178
|
+
nextValue: buildConflictingEvidenceBlocker({
|
|
179
|
+
detectedAt: nowIso,
|
|
180
|
+
scope: "scenario",
|
|
181
|
+
affected: cursor ? toAffected(cursor) : undefined,
|
|
182
|
+
reason: "valid proof and blocker candidate observed in same attempt",
|
|
183
|
+
}),
|
|
184
|
+
clearedByObservationFingerprint: null,
|
|
185
|
+
},
|
|
186
|
+
trackerAttemptFinalization: { attemptId: active.attemptId, outcome: "blocked", finalizedAt: nowIso },
|
|
187
|
+
sessionStateUpdate: "blocked",
|
|
188
|
+
appendObservationFingerprint: observation.fingerprint,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Rule 2 — valid proof for the current target/phase advances scenario status.
|
|
193
|
+
if (proofs.length > 0 && cursor) {
|
|
194
|
+
const proof = pickProofForCursor(proofs, cursor);
|
|
195
|
+
if (proof) {
|
|
196
|
+
const nextStatus = provedStatusForPhase(proof.phase);
|
|
197
|
+
if (nextStatus) {
|
|
198
|
+
return buildPlan({
|
|
199
|
+
kind: "advance",
|
|
200
|
+
rationale: `${proof.phase}-phase proof matched cursor; advancing to ${nextStatus}`,
|
|
201
|
+
scenarioStatusUpdate: {
|
|
202
|
+
stack: cursor.stack!,
|
|
203
|
+
domainId: cursor.domainId!,
|
|
204
|
+
level: cursor.level!,
|
|
205
|
+
scenarioId: cursor.scenarioId!,
|
|
206
|
+
nextStatus,
|
|
207
|
+
appendProof: {
|
|
208
|
+
type: proof.type,
|
|
209
|
+
phase: proof.phase,
|
|
210
|
+
recordedAt: observation.occurredAt,
|
|
211
|
+
actor: observation.target?.resolvedSlot ?? "frontend-executor",
|
|
212
|
+
evidence: proof.evidence,
|
|
213
|
+
artifactRef: proof.artifactRef ?? `artifact://${proof.phase}-${active.attemptId}`,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
cursorUpdate: advancedCursor(cursor, nextStatus),
|
|
217
|
+
trackerAttemptFinalization: { attemptId: active.attemptId, outcome: "advanced", finalizedAt: nowIso },
|
|
218
|
+
recomputeProgress: true,
|
|
219
|
+
appendObservationFingerprint: observation.fingerprint,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Rule 3 — explicit blocker candidate.
|
|
226
|
+
if (blockers.length > 0) {
|
|
227
|
+
const candidate = blockers[0];
|
|
228
|
+
return buildPlan({
|
|
229
|
+
kind: "block",
|
|
230
|
+
rationale: `explicit blocker candidate ${candidate.blocker.code}`,
|
|
231
|
+
blockerUpdate: {
|
|
232
|
+
scope: candidate.blocker.scope,
|
|
233
|
+
nextValue: candidate.blocker,
|
|
234
|
+
clearedByObservationFingerprint: null,
|
|
235
|
+
},
|
|
236
|
+
trackerAttemptFinalization: { attemptId: active.attemptId, outcome: "blocked", finalizedAt: nowIso },
|
|
237
|
+
sessionStateUpdate: candidate.blocker.recoveryMode === "await-user" ? "awaiting-user" : "blocked",
|
|
238
|
+
appendObservationFingerprint: observation.fingerprint,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Rule 4 — no proof, no blocker: interrupted outcome. Do NOT silently advance.
|
|
243
|
+
const interruptedBlocker = buildInterruptedAttemptBlocker({
|
|
244
|
+
detectedAt: nowIso,
|
|
245
|
+
scope: "scenario",
|
|
246
|
+
affected: cursor ? toAffected(cursor) : undefined,
|
|
247
|
+
attemptId: active.attemptId,
|
|
248
|
+
});
|
|
249
|
+
return buildPlan({
|
|
250
|
+
kind: "interrupt",
|
|
251
|
+
rationale: "no proof and no blocker observed; attempt interrupted",
|
|
252
|
+
blockerUpdate: {
|
|
253
|
+
scope: "scenario",
|
|
254
|
+
nextValue: interruptedBlocker,
|
|
255
|
+
clearedByObservationFingerprint: null,
|
|
256
|
+
},
|
|
257
|
+
trackerAttemptFinalization: { attemptId: active.attemptId, outcome: "interrupted", finalizedAt: nowIso },
|
|
258
|
+
sessionStateUpdate: "blocked",
|
|
259
|
+
appendObservationFingerprint: observation.fingerprint,
|
|
260
|
+
notes: [`missing-proof: ${buildProofMissingBlocker({ detectedAt: nowIso, expectedPhase: inferPhase(cursor) }).message}`],
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// session_shutdown
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
function reduceSessionShutdown(
|
|
269
|
+
state: ReducerState,
|
|
270
|
+
action: Extract<UltraPlanReducerAction, { kind: "session_shutdown" }>,
|
|
271
|
+
): UltraPlanMutationPlan {
|
|
272
|
+
const active = state.tracker.activeAttempt;
|
|
273
|
+
if (!active) {
|
|
274
|
+
return buildPlan({ kind: "noop", rationale: "session_shutdown with no active attempt" });
|
|
275
|
+
}
|
|
276
|
+
const interruptedBlocker = buildInterruptedAttemptBlocker({
|
|
277
|
+
detectedAt: action.nowIso,
|
|
278
|
+
scope: "scenario",
|
|
279
|
+
affected: state.cursor ? toAffected(state.cursor) : undefined,
|
|
280
|
+
attemptId: active.attemptId,
|
|
281
|
+
reason: "session shut down with in-flight active attempt",
|
|
282
|
+
});
|
|
283
|
+
return buildPlan({
|
|
284
|
+
kind: "interrupt",
|
|
285
|
+
rationale: `session_shutdown interrupted attempt ${active.attemptId}`,
|
|
286
|
+
blockerUpdate: {
|
|
287
|
+
scope: "scenario",
|
|
288
|
+
nextValue: interruptedBlocker,
|
|
289
|
+
clearedByObservationFingerprint: null,
|
|
290
|
+
},
|
|
291
|
+
trackerAttemptFinalization: { attemptId: active.attemptId, outcome: "interrupted", finalizedAt: action.nowIso },
|
|
292
|
+
sessionStateUpdate: "blocked",
|
|
293
|
+
appendObservationFingerprint: action.observation.fingerprint,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// helpers
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
interface PlanBuilderInput {
|
|
302
|
+
kind: UltraPlanMutationPlan["kind"];
|
|
303
|
+
rationale: string;
|
|
304
|
+
appendObservationFingerprint?: string | null;
|
|
305
|
+
scenarioStatusUpdate?: UltraPlanMutationPlan["scenarioStatusUpdate"];
|
|
306
|
+
reviewStatusUpdate?: UltraPlanMutationPlan["reviewStatusUpdate"];
|
|
307
|
+
blockerUpdate?: UltraPlanMutationPlan["blockerUpdate"];
|
|
308
|
+
cursorUpdate?: UltraPlanMutationPlan["cursorUpdate"];
|
|
309
|
+
sessionStateUpdate?: UltraPlanMutationPlan["sessionStateUpdate"];
|
|
310
|
+
trackerAttemptFinalization?: UltraPlanMutationPlan["trackerAttemptFinalization"];
|
|
311
|
+
recomputeProgress?: boolean;
|
|
312
|
+
repairActions?: UltraPlanMutationPlan["repairActions"];
|
|
313
|
+
notes?: string[];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function buildPlan(input: PlanBuilderInput): UltraPlanMutationPlan {
|
|
317
|
+
return {
|
|
318
|
+
kind: input.kind,
|
|
319
|
+
rationale: input.rationale,
|
|
320
|
+
appendObservationFingerprint: input.appendObservationFingerprint ?? null,
|
|
321
|
+
scenarioStatusUpdate: input.scenarioStatusUpdate ?? null,
|
|
322
|
+
reviewStatusUpdate: input.reviewStatusUpdate ?? null,
|
|
323
|
+
blockerUpdate: input.blockerUpdate ?? null,
|
|
324
|
+
cursorUpdate: input.cursorUpdate ?? null,
|
|
325
|
+
sessionStateUpdate: input.sessionStateUpdate ?? null,
|
|
326
|
+
trackerAttemptFinalization: input.trackerAttemptFinalization ?? null,
|
|
327
|
+
recomputeProgress: input.recomputeProgress ?? false,
|
|
328
|
+
repairActions: input.repairActions ?? [],
|
|
329
|
+
notes: input.notes ?? [],
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function alreadyApplied(tracker: UltraPlanRuntimeTracker, fingerprint: string): boolean {
|
|
334
|
+
return tracker.appliedFingerprints.includes(fingerprint);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function isLegalStartFromCursor(cursor: UltraPlanCursor): boolean {
|
|
338
|
+
// Legal-start transitions per spec §transition classes:
|
|
339
|
+
// planned -> red-running
|
|
340
|
+
// red-proved -> green-running
|
|
341
|
+
// review pending -> running
|
|
342
|
+
if (cursor.targetType === "scenario") {
|
|
343
|
+
return cursor.status === "planned" || cursor.status === "red-proved";
|
|
344
|
+
}
|
|
345
|
+
if (cursor.targetType === "domain-review" || cursor.targetType === "stack-review") {
|
|
346
|
+
return cursor.status === "pending";
|
|
347
|
+
}
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function legalStartNextStatus(cursor: UltraPlanCursor): UltraPlanCursor["status"] {
|
|
352
|
+
if (cursor.targetType === "scenario") {
|
|
353
|
+
if (cursor.status === "planned") return "red-running";
|
|
354
|
+
if (cursor.status === "red-proved") return "green-running";
|
|
355
|
+
}
|
|
356
|
+
if (cursor.targetType === "domain-review" || cursor.targetType === "stack-review") {
|
|
357
|
+
return "running";
|
|
358
|
+
}
|
|
359
|
+
throw new Error(`legalStartNextStatus called with unsupported cursor status ${cursor.status}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function phaseForStatus(status: UltraPlanCursor["status"], fallback: UltraPlanCursor["phase"]): UltraPlanCursor["phase"] {
|
|
363
|
+
switch (status) {
|
|
364
|
+
case "red-running":
|
|
365
|
+
return "red";
|
|
366
|
+
case "green-running":
|
|
367
|
+
return "green";
|
|
368
|
+
case "red-proved":
|
|
369
|
+
return "green";
|
|
370
|
+
case "green-proved":
|
|
371
|
+
return "complete";
|
|
372
|
+
case "running":
|
|
373
|
+
return "review";
|
|
374
|
+
default:
|
|
375
|
+
return fallback;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function provedStatusForPhase(phase: UltraPlanProofCandidate["phase"]): UltraPlanScenarioStatus | null {
|
|
380
|
+
if (phase === "red") return "red-proved";
|
|
381
|
+
if (phase === "green") return "green-proved";
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function advancedCursor(cursor: UltraPlanCursor, nextStatus: UltraPlanScenarioStatus): UltraPlanCursor {
|
|
386
|
+
return { ...cursor, status: nextStatus, phase: phaseForStatus(nextStatus, cursor.phase) };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function pickProofForCursor(proofs: UltraPlanProofCandidate[], cursor: UltraPlanCursor): UltraPlanProofCandidate | null {
|
|
390
|
+
const expectedPhase = inferPhase(cursor);
|
|
391
|
+
for (const proof of proofs) {
|
|
392
|
+
if (proof.phase !== expectedPhase) continue;
|
|
393
|
+
if (proof.target.stack !== cursor.stack) continue;
|
|
394
|
+
if (proof.target.domainId !== cursor.domainId) continue;
|
|
395
|
+
if (proof.target.level !== cursor.level) continue;
|
|
396
|
+
if (proof.target.scenarioId !== cursor.scenarioId) continue;
|
|
397
|
+
return proof;
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function inferPhase(cursor: UltraPlanCursor | null): UltraPlanCursor["phase"] {
|
|
403
|
+
if (!cursor) return "red";
|
|
404
|
+
if (cursor.status === "red-running") return "red";
|
|
405
|
+
if (cursor.status === "green-running") return "green";
|
|
406
|
+
return cursor.phase;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function toAffected(cursor: UltraPlanCursor) {
|
|
410
|
+
return {
|
|
411
|
+
stack: cursor.stack,
|
|
412
|
+
domainId: cursor.domainId,
|
|
413
|
+
level: cursor.level,
|
|
414
|
+
scenarioId: cursor.scenarioId,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
UltraPlanAttemptRecord,
|
|
3
|
+
UltraPlanBlocker,
|
|
4
|
+
UltraPlanManifest,
|
|
5
|
+
UltraPlanPendingMutation,
|
|
6
|
+
UltraPlanRepairAction,
|
|
7
|
+
UltraPlanRuntimeTracker,
|
|
8
|
+
} from "../../types.js";
|
|
9
|
+
import { buildUnsafeRepairRequiredBlocker } from "./blockers.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Slice-2 deterministic repair engine. Pure.
|
|
13
|
+
*
|
|
14
|
+
* Safe auto-repair categories (spec §safe auto-repair boundaries, lines 707–723):
|
|
15
|
+
* - recompute cursor
|
|
16
|
+
* - recompute progress summaries
|
|
17
|
+
* - clear impossible active-attempt references
|
|
18
|
+
* - convert orphaned in-flight attempts into interrupted state
|
|
19
|
+
* - clear blockers directly invalidated by later proof on the same target
|
|
20
|
+
*
|
|
21
|
+
* Forbidden: inventing proof, promoting a target to terminal without evidence, discarding
|
|
22
|
+
* conflicting evidence, skipping ahead, or rewriting authored intent. When repair would require
|
|
23
|
+
* any of those, the engine emits an `unsafe-repair-required` blocker instead of a mutation.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export interface RepairState {
|
|
27
|
+
tracker: UltraPlanRuntimeTracker;
|
|
28
|
+
manifest: UltraPlanManifest | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RepairPlan {
|
|
32
|
+
/** Deterministic actions the caller applies to the tracker / derived state. */
|
|
33
|
+
actions: UltraPlanRepairAction[];
|
|
34
|
+
/** Blockers emitted by the repair engine when deterministic recovery is unsafe. */
|
|
35
|
+
emittedBlockers: UltraPlanBlocker[];
|
|
36
|
+
/** What the caller should do with the tracker's `activeAttempt`. */
|
|
37
|
+
activeAttemptAction: "leave" | "clear" | "finalize-as-interrupted";
|
|
38
|
+
/** Resume-time handling for a staged pending mutation. */
|
|
39
|
+
pendingMutationAction: "leave" | "clear" | "apply-and-clear";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// session_start
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
export function repairOnSessionStart(state: RepairState, nowIso: string): RepairPlan {
|
|
47
|
+
const actions: UltraPlanRepairAction[] = [];
|
|
48
|
+
const emittedBlockers: UltraPlanBlocker[] = [];
|
|
49
|
+
let activeAttemptAction: RepairPlan["activeAttemptAction"] = "leave";
|
|
50
|
+
let pendingMutationAction: RepairPlan["pendingMutationAction"] = "leave";
|
|
51
|
+
|
|
52
|
+
const active = state.tracker.activeAttempt;
|
|
53
|
+
if (active) {
|
|
54
|
+
const hasProof = active.proofCandidates.length > 0;
|
|
55
|
+
const hasBlocker = active.blockerCandidates.length > 0;
|
|
56
|
+
|
|
57
|
+
if (hasProof && hasBlocker) {
|
|
58
|
+
emittedBlockers.push(buildUnsafeRepairRequiredBlocker({
|
|
59
|
+
detectedAt: nowIso,
|
|
60
|
+
scope: "scenario",
|
|
61
|
+
affected: {
|
|
62
|
+
stack: active.cursorSnapshot?.stack ?? null,
|
|
63
|
+
domainId: active.cursorSnapshot?.domainId ?? null,
|
|
64
|
+
level: active.cursorSnapshot?.level ?? null,
|
|
65
|
+
scenarioId: active.cursorSnapshot?.scenarioId ?? null,
|
|
66
|
+
},
|
|
67
|
+
reason: `attempt ${active.attemptId} carries both proof and blocker candidates; cannot auto-finalize`,
|
|
68
|
+
}));
|
|
69
|
+
actions.push({
|
|
70
|
+
op: "convert-active-to-interrupted",
|
|
71
|
+
attemptId: active.attemptId,
|
|
72
|
+
reason: "conflicting proof+blocker on resume",
|
|
73
|
+
});
|
|
74
|
+
activeAttemptAction = "finalize-as-interrupted";
|
|
75
|
+
} else {
|
|
76
|
+
actions.push({
|
|
77
|
+
op: "convert-active-to-interrupted",
|
|
78
|
+
attemptId: active.attemptId,
|
|
79
|
+
reason: "orphaned in-flight attempt recovered on session_start",
|
|
80
|
+
});
|
|
81
|
+
activeAttemptAction = "finalize-as-interrupted";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const pending = reconcilePendingMutation(state, nowIso);
|
|
86
|
+
pendingMutationAction = pending.pendingMutationAction;
|
|
87
|
+
emittedBlockers.push(...pending.emittedBlockers);
|
|
88
|
+
|
|
89
|
+
const manifestBlocker = state.manifest?.blocker ?? null;
|
|
90
|
+
if (manifestBlocker) {
|
|
91
|
+
const clearedBy = findLaterProofForBlocker(state.tracker, manifestBlocker);
|
|
92
|
+
if (clearedBy) {
|
|
93
|
+
actions.push({
|
|
94
|
+
op: "clear-blocker",
|
|
95
|
+
scope: manifestBlocker.scope,
|
|
96
|
+
clearedByObservationFingerprint: clearedBy,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { actions, emittedBlockers, activeAttemptAction, pendingMutationAction };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// session_shutdown
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
export function repairOnSessionShutdown(state: RepairState, nowIso: string): RepairPlan {
|
|
109
|
+
const actions: UltraPlanRepairAction[] = [];
|
|
110
|
+
let activeAttemptAction: RepairPlan["activeAttemptAction"] = "leave";
|
|
111
|
+
|
|
112
|
+
const active = state.tracker.activeAttempt;
|
|
113
|
+
if (active) {
|
|
114
|
+
actions.push({
|
|
115
|
+
op: "convert-active-to-interrupted",
|
|
116
|
+
attemptId: active.attemptId,
|
|
117
|
+
reason: `session shutdown with in-flight attempt (recorded at ${nowIso})`,
|
|
118
|
+
});
|
|
119
|
+
activeAttemptAction = "finalize-as-interrupted";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { actions, emittedBlockers: [], activeAttemptAction, pendingMutationAction: "leave" };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// helpers
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
function findLaterProofForBlocker(tracker: UltraPlanRuntimeTracker, blocker: UltraPlanBlocker): string | null {
|
|
130
|
+
const affected = blocker.affected;
|
|
131
|
+
for (const attempt of tracker.finalizedAttempts) {
|
|
132
|
+
if (attempt.outcome !== "advanced") continue;
|
|
133
|
+
const proof = attempt.proofCandidates.find((p) =>
|
|
134
|
+
p.target.stack === affected.stack
|
|
135
|
+
&& p.target.domainId === affected.domainId
|
|
136
|
+
&& p.target.level === affected.level
|
|
137
|
+
&& p.target.scenarioId === affected.scenarioId,
|
|
138
|
+
);
|
|
139
|
+
if (proof && isLaterThan(attempt.finalizedAt, blocker.detectedAt)) {
|
|
140
|
+
return proof.observationFingerprint;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isLaterThan(candidate: string | null, reference: string): boolean {
|
|
147
|
+
if (!candidate) return false;
|
|
148
|
+
const a = Date.parse(candidate);
|
|
149
|
+
const b = Date.parse(reference);
|
|
150
|
+
if (Number.isNaN(a) || Number.isNaN(b)) return false;
|
|
151
|
+
return a > b;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function reconcilePendingMutation(
|
|
155
|
+
state: RepairState,
|
|
156
|
+
nowIso: string,
|
|
157
|
+
): Pick<RepairPlan, "pendingMutationAction" | "emittedBlockers"> {
|
|
158
|
+
const pending = state.tracker.pendingMutation;
|
|
159
|
+
if (!pending) {
|
|
160
|
+
return { pendingMutationAction: "leave", emittedBlockers: [] };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!state.manifest) {
|
|
164
|
+
return {
|
|
165
|
+
pendingMutationAction: "leave",
|
|
166
|
+
emittedBlockers: [buildUnsafeRepairRequiredBlocker({
|
|
167
|
+
detectedAt: nowIso,
|
|
168
|
+
scope: "session",
|
|
169
|
+
reason: `pending mutation ${pending.attemptId} cannot be reconciled without a manifest snapshot`,
|
|
170
|
+
})],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (isPendingMutationReflected(state.manifest, pending)) {
|
|
175
|
+
return { pendingMutationAction: "clear", emittedBlockers: [] };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (canSafelyApplyPendingMutation(pending)) {
|
|
179
|
+
return { pendingMutationAction: "apply-and-clear", emittedBlockers: [] };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
pendingMutationAction: "leave",
|
|
184
|
+
emittedBlockers: [buildUnsafeRepairRequiredBlocker({
|
|
185
|
+
detectedAt: nowIso,
|
|
186
|
+
scope: "session",
|
|
187
|
+
reason: `pending mutation ${pending.attemptId} still requires canonical authored/review reconciliation`,
|
|
188
|
+
})],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isPendingMutationReflected(manifest: UltraPlanManifest, pending: UltraPlanPendingMutation): boolean {
|
|
193
|
+
const mutationPlan = pending.mutationPlan;
|
|
194
|
+
|
|
195
|
+
if (mutationPlan.scenarioStatusUpdate || mutationPlan.recomputeProgress) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const checks: boolean[] = [];
|
|
200
|
+
|
|
201
|
+
if (mutationPlan.blockerUpdate) {
|
|
202
|
+
const nextValue = mutationPlan.blockerUpdate.nextValue;
|
|
203
|
+
checks.push(nextValue === null
|
|
204
|
+
? manifest.blocker === null
|
|
205
|
+
: JSON.stringify(manifest.blocker) === JSON.stringify(nextValue));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (mutationPlan.cursorUpdate) {
|
|
209
|
+
checks.push(sameCursor(manifest.cursor, mutationPlan.cursorUpdate));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (mutationPlan.sessionStateUpdate) {
|
|
213
|
+
checks.push(manifest.state === mutationPlan.sessionStateUpdate);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (mutationPlan.reviewStatusUpdate) {
|
|
217
|
+
const review = manifest.reviews.find((candidate) =>
|
|
218
|
+
candidate.type === mutationPlan.reviewStatusUpdate?.type
|
|
219
|
+
&& candidate.stack === mutationPlan.reviewStatusUpdate.stack
|
|
220
|
+
&& candidate.domainId === mutationPlan.reviewStatusUpdate.domainId,
|
|
221
|
+
);
|
|
222
|
+
checks.push(
|
|
223
|
+
review?.status === mutationPlan.reviewStatusUpdate.nextStatus
|
|
224
|
+
&& review.path === mutationPlan.reviewStatusUpdate.artifactRef,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return checks.length > 0 && checks.every(Boolean);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function canSafelyApplyPendingMutation(pending: UltraPlanPendingMutation): boolean {
|
|
232
|
+
const mutationPlan = pending.mutationPlan;
|
|
233
|
+
return mutationPlan.scenarioStatusUpdate === null
|
|
234
|
+
&& mutationPlan.reviewStatusUpdate === null
|
|
235
|
+
&& mutationPlan.recomputeProgress === false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function sameCursor(left: UltraPlanManifest["cursor"], right: UltraPlanManifest["cursor"]): boolean {
|
|
239
|
+
if (!left || !right) {
|
|
240
|
+
return left === right;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return left.targetType === right.targetType
|
|
244
|
+
&& left.stack === right.stack
|
|
245
|
+
&& left.domainId === right.domainId
|
|
246
|
+
&& left.level === right.level
|
|
247
|
+
&& left.scenarioId === right.scenarioId
|
|
248
|
+
&& left.phase === right.phase
|
|
249
|
+
&& left.status === right.status
|
|
250
|
+
&& left.summary === right.summary;
|
|
251
|
+
}
|