@gajae-code/coding-agent 0.11.11 → 0.12.1
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/CHANGELOG.md +131 -4
- package/dist/types/async/job-manager.d.ts +17 -3
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/commands/deep-interview.d.ts +9 -0
- package/dist/types/commands/harness.d.ts +22 -0
- package/dist/types/commands/read.d.ts +6 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/provider-auth-health.d.ts +14 -0
- package/dist/types/config/provider-ranking.d.ts +59 -0
- package/dist/types/config/settings-schema.d.ts +83 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/exa/mcp-client.d.ts +10 -1
- package/dist/types/exec/bash-executor.d.ts +45 -12
- package/dist/types/extensibility/extensions/types.d.ts +38 -4
- package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
- package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
- package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
- package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
- package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +70 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +27 -1
- package/dist/types/gjc-runtime/team-store.d.ts +12 -0
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +17 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +10 -19
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/session-observer-overlay.d.ts +22 -0
- package/dist/types/modes/components/settings-selector.d.ts +11 -0
- package/dist/types/modes/components/status-line/gh.d.ts +5 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/reminders/star-reminder.d.ts +3 -13
- package/dist/types/runtime-mcp/manager.d.ts +1 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
- package/dist/types/sdk/bus/config.d.ts +6 -2
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +44 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +44 -0
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +69 -0
- package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
- package/dist/types/sdk/bus/rich-render.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
- package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
- package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
- package/dist/types/sdk/prompt-status.d.ts +23 -3
- package/dist/types/sdk/session.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +82 -12
- package/dist/types/session/artifacts.d.ts +8 -0
- package/dist/types/session/blob-store.d.ts +57 -4
- package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +48 -19
- package/dist/types/session/session-storage.d.ts +4 -0
- package/dist/types/session/streaming-output.d.ts +96 -12
- package/dist/types/setup/credential-import.d.ts +12 -0
- package/dist/types/skill-state/active-state.d.ts +12 -4
- package/dist/types/skill-state/workflow-hud.d.ts +9 -0
- package/dist/types/task/executor.d.ts +3 -2
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/provider-retry-status.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +7 -1
- package/dist/types/task/types.d.ts +23 -0
- package/dist/types/tools/ask-answer-registry.d.ts +2 -2
- package/dist/types/tools/bash-interactive.d.ts +4 -1
- package/dist/types/tools/bash.d.ts +3 -1
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +17 -1
- package/dist/types/tools/image-gen.d.ts +4 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/output-meta.d.ts +44 -3
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -7
- package/dist/types/tools/resource-gc.d.ts +4 -0
- package/dist/types/tools/review.d.ts +5 -7
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/dist/types/utils/gh.d.ts +13 -0
- package/dist/types/utils/image-resize.d.ts +1 -0
- package/dist/types/web/search/providers/kimi.d.ts +2 -0
- package/dist/types/web/search/providers/searxng.d.ts +7 -0
- package/dist/types/web/search/providers/xai.d.ts +2 -0
- package/package.json +11 -13
- package/scripts/acp-conformance-agent.ts +218 -0
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-gjc-bundle-settings.ts +306 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/generate-sdk-operation-inventory.ts +5 -0
- package/scripts/resident-memory-bench.ts +1341 -0
- package/scripts/run-acp-conformance.ts +285 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/async/job-manager.ts +78 -20
- package/src/capability/index.ts +8 -0
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/plugin-cli.ts +176 -18
- package/src/cli/read-cli.ts +2 -1
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +29 -9
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/team.ts +20 -2
- package/src/config/file-lock.ts +11 -2
- package/src/config/model-registry.ts +21 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +74 -1
- package/src/config/settings.ts +246 -108
- package/src/coordinator-mcp/question-gate-codec.ts +1 -1
- package/src/coordinator-mcp/server.ts +5 -12
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +230 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +77 -41
- package/src/defaults/gjc/skills/team/SKILL.md +3 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +62 -39
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
- package/src/defaults/gjc-defaults.ts +4 -4
- package/src/eval/py/executor.ts +7 -2
- package/src/exa/mcp-client.ts +12 -3
- package/src/exec/bash-executor.ts +144 -27
- package/src/extensibility/extensions/runner.ts +15 -0
- package/src/extensibility/extensions/types.ts +46 -5
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
- package/src/extensibility/gjc-plugins/index.ts +19 -2
- package/src/extensibility/gjc-plugins/installer.ts +189 -50
- package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
- package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
- package/src/extensibility/gjc-plugins/observability.ts +3 -3
- package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
- package/src/extensibility/gjc-plugins/schema.ts +84 -12
- package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
- package/src/extensibility/gjc-plugins/types.ts +141 -0
- package/src/extensibility/gjc-plugins/validation.ts +9 -0
- package/src/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
- package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
- package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
- package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
- package/src/gjc-runtime/deep-interview-state.ts +12 -943
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/launch-tmux.ts +25 -6
- package/src/gjc-runtime/managed-owner-admission.ts +1 -1
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +1149 -98
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +70 -222
- package/src/gjc-runtime/team-launch.ts +42 -5
- package/src/gjc-runtime/team-runtime.ts +1085 -40
- package/src/gjc-runtime/team-store.ts +32 -0
- package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
- package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/gjc-runtime/team-workers.ts +1 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +28 -0
- package/src/gjc-runtime/tmux-sessions.ts +58 -13
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +414 -618
- package/src/gjc-runtime/workflow-manifest.generated.json +105 -449
- package/src/gjc-runtime/workflow-manifest.ts +38 -247
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/hooks/skill-state.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +21 -22
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/main.ts +9 -5
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +98 -0
- package/src/modes/acp/acp-agent.ts +339 -191
- package/src/modes/acp/acp-event-mapper.ts +14 -7
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/gjc-bundle-settings.ts +508 -0
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +61 -4
- package/src/modes/components/notifications-settings-editor.ts +12 -1
- package/src/modes/components/oauth-selector.ts +96 -28
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/session-observer-overlay.ts +457 -83
- package/src/modes/components/settings-selector.ts +116 -23
- package/src/modes/components/status-line/gh.ts +16 -0
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/components/tool-status-header.ts +3 -14
- package/src/modes/components/transcript-viewer-overlay.ts +99 -24
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +26 -2
- package/src/modes/controllers/input-controller.ts +18 -3
- package/src/modes/controllers/selector-controller.ts +127 -36
- package/src/modes/interactive-mode.ts +62 -3
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +22 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +94 -1
- package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
- package/src/prompts/agents/architect.md +9 -1
- package/src/prompts/agents/critic.md +9 -0
- package/src/prompts/system/eager-todo.md +8 -4
- package/src/prompts/system/system-prompt.md +11 -5
- package/src/prompts/tools/bash.md +5 -5
- package/src/prompts/tools/read.md +4 -2
- package/src/prompts/tools/search-tool-bm25.md +5 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/reminders/star-reminder.ts +4 -42
- package/src/runtime-mcp/manager.ts +19 -4
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/acp/adapter.ts +22 -6
- package/src/sdk/broker/lifecycle.ts +25 -6
- package/src/sdk/broker/transport.ts +35 -3
- package/src/sdk/bus/attachment-registry.ts +5 -1
- package/src/sdk/bus/chat-daemon-cli.ts +4 -39
- package/src/sdk/bus/chat-daemon-control.ts +6 -3
- package/src/sdk/bus/config.ts +18 -2
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +984 -146
- package/src/sdk/bus/kind-aware-reconciliation.ts +317 -0
- package/src/sdk/bus/lifecycle-commands.ts +4 -1
- package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/reconciliation-store.ts +303 -0
- package/src/sdk/bus/rich-draft.ts +1 -1
- package/src/sdk/bus/rich-render.ts +2 -2
- package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
- package/src/sdk/bus/telegram-cli.ts +33 -8
- package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
- package/src/sdk/bus/telegram-daemon.ts +916 -23
- package/src/sdk/bus/telegram-reference.ts +40 -3
- package/src/sdk/bus/threaded-render.ts +48 -3
- package/src/sdk/bus/topic-registry.ts +20 -0
- package/src/sdk/host/control/dispatch.ts +5 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +23 -1
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/host/query/revision-store.ts +36 -2
- package/src/sdk/host/reverse-leases.ts +44 -7
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/prompt-status.ts +16 -3
- package/src/sdk/protocol/operation-inventory.generated.json +51 -0
- package/src/sdk/protocol/operation-registry.ts +7 -2
- package/src/sdk/session.ts +101 -10
- package/src/session/agent-session.ts +1286 -220
- package/src/session/artifacts.ts +22 -0
- package/src/session/auth-broker-config.ts +10 -3
- package/src/session/blob-store.ts +915 -24
- package/src/session/internal/managed-session-scope.ts +44 -6
- package/src/session/internal/managed-session-storage.ts +48 -6
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +2364 -438
- package/src/session/session-storage.ts +9 -1
- package/src/session/streaming-output.ts +500 -111
- package/src/setup/credential-import.ts +75 -13
- package/src/setup/provider-onboarding.ts +19 -4
- package/src/skill-state/active-state.ts +32 -10
- package/src/skill-state/workflow-hud.ts +42 -2
- package/src/skill-state/workflow-mutation-guard.ts +560 -8
- package/src/task/executor.ts +122 -39
- package/src/task/index.ts +23 -14
- package/src/task/provider-retry-status.ts +16 -0
- package/src/task/receipt.ts +23 -7
- package/src/task/render.ts +6 -2
- package/src/task/types.ts +108 -0
- package/src/tools/ask-answer-registry.ts +12 -6
- package/src/tools/ask.ts +169 -118
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash-interactive.ts +8 -3
- package/src/tools/bash.ts +503 -60
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/fetch.ts +114 -23
- package/src/tools/find.ts +1 -0
- package/src/tools/image-gen.ts +29 -5
- package/src/tools/index.ts +4 -0
- package/src/tools/output-meta.ts +281 -20
- package/src/tools/path-utils.ts +8 -3
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1560 -310
- package/src/tools/resource-gc.ts +182 -18
- package/src/tools/review.ts +5 -7
- package/src/tools/search.ts +1 -0
- package/src/tools/subagent-render.ts +6 -2
- package/src/tools/subagent.ts +6 -0
- package/src/tools/telegram-send.ts +6 -3
- package/src/tools/tool-result.ts +14 -1
- package/src/utils/gh.ts +39 -0
- package/src/utils/image-resize.ts +3 -0
- package/src/web/search/providers/anthropic.ts +4 -4
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/providers/kimi.ts +15 -5
- package/src/web/search/providers/searxng.ts +47 -4
- package/src/web/search/providers/xai.ts +7 -2
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
- package/src/gjc-runtime/deep-interview-draft.ts +0 -843
- package/src/gjc-runtime/deep-interview-payload.ts +0 -262
- package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
- package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as crypto from "node:crypto";
|
|
2
|
-
import * as os from "node:os";
|
|
3
2
|
import * as path from "node:path";
|
|
3
|
+
import { getConfigRootDir } from "@gajae-code/utils";
|
|
4
4
|
import type { WorkflowHudSummary } from "../skill-state/active-state";
|
|
5
5
|
import { buildUltragoalHudSummary as buildWorkflowUltragoalHudSummary } from "../skill-state/workflow-hud";
|
|
6
6
|
import { renderCliWriteReceipt } from "./cli-write-receipt";
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
computeCriticVerdictPlanGeneration,
|
|
20
20
|
computeUltragoalPlanGeneration,
|
|
21
21
|
countNonOkayTerminalCriticVerdicts,
|
|
22
|
+
finalAggregateReceiptMissingCriticOkay,
|
|
22
23
|
findFreshBatchCloseReceipt,
|
|
23
24
|
findLedgerReceiptEvent,
|
|
24
25
|
isCleanPauseCriticVerdictShape,
|
|
@@ -85,47 +86,6 @@ export type UltragoalGoalStatus =
|
|
|
85
86
|
| "review_blocked"
|
|
86
87
|
| "superseded";
|
|
87
88
|
|
|
88
|
-
export type UltragoalPipelineMetadataSource = "original_plan_graph" | "legacy_brief_only" | "steering";
|
|
89
|
-
export type UltragoalPipelineOverlapState =
|
|
90
|
-
| "none"
|
|
91
|
-
| "open"
|
|
92
|
-
| "joined_clean"
|
|
93
|
-
| "blocked_disjoint_continue"
|
|
94
|
-
| "quarantine_required"
|
|
95
|
-
| "rebaseline_complete";
|
|
96
|
-
|
|
97
|
-
export interface UltragoalPipelineTargets extends JsonObject {
|
|
98
|
-
files: string[];
|
|
99
|
-
surfaces: string[];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export interface UltragoalPipelineMetadata extends JsonObject {
|
|
103
|
-
schemaVersion: 1;
|
|
104
|
-
goalId: string;
|
|
105
|
-
source: UltragoalPipelineMetadataSource;
|
|
106
|
-
eligible: boolean;
|
|
107
|
-
dependsOn: string[];
|
|
108
|
-
independentOf: string[];
|
|
109
|
-
targets: UltragoalPipelineTargets;
|
|
110
|
-
metadataHash: string;
|
|
111
|
-
overlap: UltragoalPipelineOverlapState;
|
|
112
|
-
overlapId?: string;
|
|
113
|
-
priorGoalId?: string;
|
|
114
|
-
nextGoalId?: string;
|
|
115
|
-
blockerFootprints?: UltragoalPipelineTargets[];
|
|
116
|
-
invalidationReason?: string;
|
|
117
|
-
invalidatedAt?: string;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export interface UltragoalGoalMetadataInput {
|
|
121
|
-
schemaVersion: 1;
|
|
122
|
-
goalId: string;
|
|
123
|
-
source: UltragoalPipelineMetadataSource;
|
|
124
|
-
dependsOn?: string[];
|
|
125
|
-
independentOf?: string[];
|
|
126
|
-
targets?: Partial<UltragoalPipelineTargets>;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
89
|
export interface UltragoalValidationBatchMetadata extends JsonObject {
|
|
130
90
|
schemaVersion: 1;
|
|
131
91
|
batchId: string;
|
|
@@ -142,39 +102,6 @@ export interface UltragoalValidationBatchInput {
|
|
|
142
102
|
finalGoalId: string;
|
|
143
103
|
}
|
|
144
104
|
|
|
145
|
-
export interface UltragoalPipelineOverlapHandles extends JsonObject {
|
|
146
|
-
review: JsonObject;
|
|
147
|
-
qa: JsonObject;
|
|
148
|
-
implementation: JsonObject;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export interface UltragoalPipelineOverlapReceipt extends JsonObject {
|
|
152
|
-
ok: true;
|
|
153
|
-
event: string;
|
|
154
|
-
overlap_id: string;
|
|
155
|
-
prior_goal_id: string;
|
|
156
|
-
next_goal_id?: string;
|
|
157
|
-
goal_id?: string;
|
|
158
|
-
status?: UltragoalPipelineOverlapState;
|
|
159
|
-
next_goal_status?: UltragoalGoalStatus;
|
|
160
|
-
goals_path: string;
|
|
161
|
-
ledger_path: string;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export type UltragoalPipelineLedgerEventName =
|
|
165
|
-
| "pipeline_overlap_started"
|
|
166
|
-
| "pipeline_overlap_joined"
|
|
167
|
-
| "pipeline_overlap_blocked"
|
|
168
|
-
| "pipeline_overlap_quarantined"
|
|
169
|
-
| "pipeline_overlap_rebaselined";
|
|
170
|
-
|
|
171
|
-
export interface UltragoalPipelineLedgerEvent extends UltragoalLedgerEvent {
|
|
172
|
-
event: UltragoalPipelineLedgerEventName;
|
|
173
|
-
schemaVersion: 1;
|
|
174
|
-
overlapId: string;
|
|
175
|
-
priorGoalId: string;
|
|
176
|
-
nextGoalId: string;
|
|
177
|
-
}
|
|
178
105
|
export interface UltragoalGoal {
|
|
179
106
|
id: string;
|
|
180
107
|
title: string;
|
|
@@ -187,7 +114,6 @@ export interface UltragoalGoal {
|
|
|
187
114
|
evidence?: string;
|
|
188
115
|
steering?: Record<string, unknown>;
|
|
189
116
|
completionVerification?: UltragoalCompletionVerification;
|
|
190
|
-
pipelineMetadata?: UltragoalPipelineMetadata;
|
|
191
117
|
validationBatch?: UltragoalValidationBatchMetadata;
|
|
192
118
|
}
|
|
193
119
|
|
|
@@ -322,7 +248,6 @@ export interface UltragoalStatusSummary {
|
|
|
322
248
|
nudgeRemaining?: number;
|
|
323
249
|
nudgeGoalId?: string;
|
|
324
250
|
nudgeTargetKind?: UltragoalNudgeTargetKind;
|
|
325
|
-
pipelineOverlap?: JsonObject;
|
|
326
251
|
}
|
|
327
252
|
|
|
328
253
|
export interface UltragoalCommandResult {
|
|
@@ -490,8 +415,7 @@ export async function resolveUltragoalNudgeBudget(cwd: string): Promise<{ budget
|
|
|
490
415
|
const projectPath = path.join(gjcRoot(cwd), "settings.json");
|
|
491
416
|
const project = await readSettingsNudgeBudget(projectPath);
|
|
492
417
|
if (project !== null) return { budget: project, source: projectPath };
|
|
493
|
-
const
|
|
494
|
-
const userPath = path.join(userDir, "settings.json");
|
|
418
|
+
const userPath = path.join(getConfigRootDir(), "settings.json");
|
|
495
419
|
const user = await readSettingsNudgeBudget(userPath);
|
|
496
420
|
if (user !== null) return { budget: user, source: userPath };
|
|
497
421
|
return { budget: DEFAULT_ULTRAGOAL_NUDGE_BUDGET, source: "default" };
|
|
@@ -742,61 +666,6 @@ export function stringArray(value: unknown): string[] | null {
|
|
|
742
666
|
return Array.isArray(value) && value.every(item => typeof item === "string") ? value.map(item => item.trim()) : null;
|
|
743
667
|
}
|
|
744
668
|
|
|
745
|
-
function normalizePipelineStringArray(value: unknown, fieldName: string): string[] {
|
|
746
|
-
const items = stringArray(value) ?? [];
|
|
747
|
-
const filtered = items.filter(item => item.length > 0);
|
|
748
|
-
if (items.length !== filtered.length) throw new Error(`${fieldName} must contain only non-empty strings`);
|
|
749
|
-
return filtered;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
function normalizePipelinePath(value: string, fieldName: string): string {
|
|
753
|
-
const raw = value.trim();
|
|
754
|
-
if (raw.split(/[\\/]+/).includes("..")) throw new Error(`${fieldName} contains unsafe path ${value}`);
|
|
755
|
-
const normalized = normalizeRepoPath(raw);
|
|
756
|
-
if (
|
|
757
|
-
!normalized ||
|
|
758
|
-
normalized.startsWith("../") ||
|
|
759
|
-
normalized === ".." ||
|
|
760
|
-
path.isAbsolute(normalized) ||
|
|
761
|
-
normalized.includes("\0")
|
|
762
|
-
) {
|
|
763
|
-
throw new Error(`${fieldName} contains unsafe path ${value}`);
|
|
764
|
-
}
|
|
765
|
-
return normalized;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
function normalizePipelineTargets(value: unknown, fieldName: string): UltragoalPipelineTargets {
|
|
769
|
-
const record = typeof value === "object" && value !== null && !Array.isArray(value) ? (value as JsonObject) : {};
|
|
770
|
-
const files = normalizePipelineStringArray(record.files, `${fieldName}.files`).map(item =>
|
|
771
|
-
normalizePipelinePath(item, `${fieldName}.files`),
|
|
772
|
-
);
|
|
773
|
-
const surfaces = normalizePipelineStringArray(record.surfaces, `${fieldName}.surfaces`).map(normalizeSurfaceToken);
|
|
774
|
-
if (new Set(files).size !== files.length) throw new Error(`${fieldName}.files contains duplicate normalized paths`);
|
|
775
|
-
if (new Set(surfaces).size !== surfaces.length)
|
|
776
|
-
throw new Error(`${fieldName}.surfaces contains duplicate normalized surfaces`);
|
|
777
|
-
return { files, surfaces };
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
function pipelineMetadataHashBasis(metadata: Omit<UltragoalPipelineMetadata, "metadataHash">): JsonObject {
|
|
781
|
-
return {
|
|
782
|
-
schemaVersion: metadata.schemaVersion,
|
|
783
|
-
goalId: metadata.goalId,
|
|
784
|
-
source: metadata.source,
|
|
785
|
-
dependsOn: metadata.dependsOn,
|
|
786
|
-
independentOf: metadata.independentOf,
|
|
787
|
-
targets: metadata.targets,
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
export function hashPipelineMetadata(metadata: Omit<UltragoalPipelineMetadata, "metadataHash">): string {
|
|
792
|
-
return hashStructuredValue(pipelineMetadataHashBasis(metadata));
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
function withPipelineMetadataHash(
|
|
796
|
-
metadata: Omit<UltragoalPipelineMetadata, "metadataHash">,
|
|
797
|
-
): UltragoalPipelineMetadata {
|
|
798
|
-
return { ...metadata, metadataHash: hashPipelineMetadata(metadata) } as UltragoalPipelineMetadata;
|
|
799
|
-
}
|
|
800
669
|
function validationBatchHashBasis(metadata: Omit<UltragoalValidationBatchMetadata, "metadataHash">): JsonObject {
|
|
801
670
|
return {
|
|
802
671
|
schemaVersion: metadata.schemaVersion,
|
|
@@ -900,22 +769,12 @@ function normalizeSavedValidationBatch(record: unknown, id: string): UltragoalVa
|
|
|
900
769
|
return normalized;
|
|
901
770
|
}
|
|
902
771
|
|
|
903
|
-
function pipelineMetadataConflictsWithValidationBatch(metadata: UltragoalPipelineMetadata | undefined): boolean {
|
|
904
|
-
return metadata?.eligible === true || metadata?.source === "original_plan_graph" || metadata?.source === "steering";
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
export function validateValidationBatchPipelineExclusion(goal: UltragoalGoal): void {
|
|
908
|
-
if (goal.validationBatch && pipelineMetadataConflictsWithValidationBatch(goal.pipelineMetadata)) {
|
|
909
|
-
throw new Error(`Goal ${goal.id} cannot combine validationBatch with eligible pipeline metadata`);
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
772
|
function requireFreshValidationBatchMetadata(goal: UltragoalGoal): UltragoalValidationBatchMetadata | undefined {
|
|
913
773
|
const metadata = goal.validationBatch;
|
|
914
774
|
if (!metadata) return undefined;
|
|
915
775
|
const { metadataHash, ...basis } = metadata;
|
|
916
776
|
if (metadataHash !== hashValidationBatch(basis))
|
|
917
777
|
throw new Error(`Goal ${goal.id} has stale validation batch metadata hash`);
|
|
918
|
-
validateValidationBatchPipelineExclusion(goal);
|
|
919
778
|
return metadata;
|
|
920
779
|
}
|
|
921
780
|
|
|
@@ -1048,245 +907,6 @@ function requireValidationBatchSteeringAllowed(
|
|
|
1048
907
|
`steer ${kind} cannot invalidate validation batch ${metadata.batchId} while member ${blocker.id} has a fresh deferred receipt`,
|
|
1049
908
|
);
|
|
1050
909
|
}
|
|
1051
|
-
function legacyPipelineMetadata(goalId: string): UltragoalPipelineMetadata {
|
|
1052
|
-
const basis: Omit<UltragoalPipelineMetadata, "metadataHash"> = {
|
|
1053
|
-
schemaVersion: 1,
|
|
1054
|
-
goalId,
|
|
1055
|
-
source: "legacy_brief_only",
|
|
1056
|
-
eligible: false,
|
|
1057
|
-
dependsOn: [],
|
|
1058
|
-
independentOf: [],
|
|
1059
|
-
targets: { files: [], surfaces: [] },
|
|
1060
|
-
overlap: "none",
|
|
1061
|
-
invalidationReason: "missing_pipeline_metadata",
|
|
1062
|
-
};
|
|
1063
|
-
return withPipelineMetadataHash(basis);
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
function normalizePipelineMetadataRecord(value: unknown, goalIds: ReadonlySet<string>): UltragoalPipelineMetadata {
|
|
1067
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1068
|
-
throw new Error("goal metadata rows must be objects");
|
|
1069
|
-
const record = value as JsonObject;
|
|
1070
|
-
if (record.schemaVersion !== 1) throw new Error("goal metadata schemaVersion must be 1");
|
|
1071
|
-
const goalId = nonEmptyString(record.goalId);
|
|
1072
|
-
if (!goalId || !goalIds.has(goalId)) throw new Error(`goal metadata references unknown goal id ${goalId ?? ""}`);
|
|
1073
|
-
const source = record.source;
|
|
1074
|
-
if (source !== "original_plan_graph" && source !== "legacy_brief_only" && source !== "steering") {
|
|
1075
|
-
throw new Error("goal metadata source must be original_plan_graph, legacy_brief_only, or steering");
|
|
1076
|
-
}
|
|
1077
|
-
const dependsOn = normalizePipelineStringArray(record.dependsOn, `metadata ${goalId}.dependsOn`);
|
|
1078
|
-
const independentOf = normalizePipelineStringArray(record.independentOf, `metadata ${goalId}.independentOf`);
|
|
1079
|
-
if (dependsOn.includes(goalId) || independentOf.includes(goalId))
|
|
1080
|
-
throw new Error(`goal metadata ${goalId} cannot reference itself`);
|
|
1081
|
-
for (const id of [...dependsOn, ...independentOf]) {
|
|
1082
|
-
if (!goalIds.has(id)) throw new Error(`goal metadata ${goalId} references unknown goal id ${id}`);
|
|
1083
|
-
}
|
|
1084
|
-
if (dependsOn.some(id => independentOf.includes(id)))
|
|
1085
|
-
throw new Error(`goal metadata ${goalId} has dependency/independence conflict`);
|
|
1086
|
-
const targets = requireNonEmptyPipelineTargets(record.targets, `metadata ${goalId}.targets`);
|
|
1087
|
-
const basis: Omit<UltragoalPipelineMetadata, "metadataHash"> = {
|
|
1088
|
-
schemaVersion: 1,
|
|
1089
|
-
goalId,
|
|
1090
|
-
source,
|
|
1091
|
-
eligible: false,
|
|
1092
|
-
dependsOn,
|
|
1093
|
-
independentOf,
|
|
1094
|
-
targets,
|
|
1095
|
-
overlap: "none",
|
|
1096
|
-
};
|
|
1097
|
-
return withPipelineMetadataHash(basis);
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
export function targetsAreDisjoint(left: UltragoalPipelineTargets, right: UltragoalPipelineTargets): boolean {
|
|
1101
|
-
return (
|
|
1102
|
-
left.files.every(file => !right.files.includes(file)) &&
|
|
1103
|
-
left.surfaces.every(surface => !right.surfaces.includes(surface))
|
|
1104
|
-
);
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
export function targetsOverlap(left: UltragoalPipelineTargets, right: UltragoalPipelineTargets): boolean {
|
|
1108
|
-
return (
|
|
1109
|
-
left.files.some(file => right.files.includes(file)) ||
|
|
1110
|
-
left.surfaces.some(surface => right.surfaces.includes(surface))
|
|
1111
|
-
);
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
export function requireNonEmptyPipelineTargets(value: unknown, fieldName: string): UltragoalPipelineTargets {
|
|
1115
|
-
const targets = normalizePipelineTargets(value, fieldName);
|
|
1116
|
-
if (targets.files.length === 0 && targets.surfaces.length === 0)
|
|
1117
|
-
throw new Error(`${fieldName} requires files or surfaces`);
|
|
1118
|
-
return targets;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
export function collectPipelineBlockerFootprints(result: JsonObject, fieldName: string): UltragoalPipelineTargets[] {
|
|
1122
|
-
const raw = Array.isArray(result.blockers)
|
|
1123
|
-
? result.blockers
|
|
1124
|
-
: Array.isArray(result.blockerFootprints)
|
|
1125
|
-
? result.blockerFootprints
|
|
1126
|
-
: [];
|
|
1127
|
-
return raw.map((item, index) => {
|
|
1128
|
-
const record = requireJsonObjectValue(item, `${fieldName}.blockers[${index}]`);
|
|
1129
|
-
const footprint = typeof record.footprint === "object" && record.footprint !== null ? record.footprint : record;
|
|
1130
|
-
return requireNonEmptyPipelineTargets(footprint, `${fieldName}.blockers[${index}].footprint`);
|
|
1131
|
-
});
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
function pipelineTargetsCoverPath(targets: UltragoalPipelineTargets, filePath: string): boolean {
|
|
1135
|
-
const normalized = normalizeRepoPath(filePath);
|
|
1136
|
-
return targets.files.some(target => normalized === target || normalized.startsWith(`${target}/`));
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
export function pipelinePeer(plan: UltragoalPlan, metadata: UltragoalPipelineMetadata): UltragoalGoal | undefined {
|
|
1140
|
-
const peerId = metadata.goalId === metadata.priorGoalId ? metadata.nextGoalId : metadata.priorGoalId;
|
|
1141
|
-
return peerId ? plan.goals.find(goal => goal.id === peerId) : undefined;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
export function handleIdsFromValue(value: JsonObject | JsonObject[], fieldName: string): string[] {
|
|
1145
|
-
const records = Array.isArray(value) ? value : [value];
|
|
1146
|
-
const ids = records.map(
|
|
1147
|
-
(record, index) =>
|
|
1148
|
-
nonEmptyString(record.id) ??
|
|
1149
|
-
nonEmptyString(record.handleId) ??
|
|
1150
|
-
nonEmptyString(record.name) ??
|
|
1151
|
-
`${fieldName}-${index}`,
|
|
1152
|
-
);
|
|
1153
|
-
if (ids.some(id => id.length === 0)) throw new Error(`${fieldName} handles require ids`);
|
|
1154
|
-
return ids;
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
export function resultHandleIds(value: JsonObject, fieldName: string): string[] {
|
|
1158
|
-
const ids = stringArray(value.handleIds) ?? stringArray(value.handles) ?? [];
|
|
1159
|
-
if (ids.length === 0) throw new Error(`${fieldName} requires handleIds`);
|
|
1160
|
-
return ids;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
export function requireCoveredHandles(expected: readonly string[], actual: readonly string[], fieldName: string): void {
|
|
1164
|
-
const missing = expected.filter(id => !actual.includes(id));
|
|
1165
|
-
if (missing.length > 0) throw new Error(`${fieldName} is missing handle coverage for ${missing.join(", ")}`);
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
function validatePipelineEligibility(metadata: UltragoalPipelineMetadata[]): UltragoalPipelineMetadata[] {
|
|
1169
|
-
const byId = new Map(metadata.map(item => [item.goalId, item]));
|
|
1170
|
-
return metadata.map(item => {
|
|
1171
|
-
const invalidationReasons: string[] = [];
|
|
1172
|
-
if (item.source !== "original_plan_graph") invalidationReasons.push("not_original_plan_graph");
|
|
1173
|
-
if (item.targets.files.length === 0 && item.targets.surfaces.length === 0)
|
|
1174
|
-
invalidationReasons.push("empty_targets");
|
|
1175
|
-
for (const otherId of item.independentOf) {
|
|
1176
|
-
const other = byId.get(otherId);
|
|
1177
|
-
if (!other?.independentOf.includes(item.goalId))
|
|
1178
|
-
invalidationReasons.push(`missing_symmetric_independence:${otherId}`);
|
|
1179
|
-
if (other && !targetsAreDisjoint(item.targets, other.targets))
|
|
1180
|
-
invalidationReasons.push(`shared_targets:${otherId}`);
|
|
1181
|
-
}
|
|
1182
|
-
const eligible = invalidationReasons.length === 0;
|
|
1183
|
-
return {
|
|
1184
|
-
...item,
|
|
1185
|
-
eligible,
|
|
1186
|
-
...(eligible ? {} : { invalidationReason: invalidationReasons.join(",") || "ineligible" }),
|
|
1187
|
-
};
|
|
1188
|
-
});
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
function parseGoalMetadataInput(value: unknown, goalIds: ReadonlySet<string>): UltragoalPipelineMetadata[] {
|
|
1192
|
-
if (!Array.isArray(value)) throw new Error("goal metadata JSON must be an array");
|
|
1193
|
-
const seen = new Set<string>();
|
|
1194
|
-
const metadata = value.map(row => {
|
|
1195
|
-
const item = normalizePipelineMetadataRecord(row, goalIds);
|
|
1196
|
-
if (seen.has(item.goalId)) throw new Error(`duplicate goal metadata for ${item.goalId}`);
|
|
1197
|
-
seen.add(item.goalId);
|
|
1198
|
-
return item;
|
|
1199
|
-
});
|
|
1200
|
-
return validatePipelineEligibility(metadata);
|
|
1201
|
-
}
|
|
1202
|
-
function normalizeSavedPipelineMetadata(value: unknown, goalId: string): UltragoalPipelineMetadata | undefined {
|
|
1203
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
1204
|
-
const record = value as JsonObject;
|
|
1205
|
-
const source =
|
|
1206
|
-
record.source === "original_plan_graph" || record.source === "legacy_brief_only" || record.source === "steering"
|
|
1207
|
-
? record.source
|
|
1208
|
-
: "legacy_brief_only";
|
|
1209
|
-
const overlap =
|
|
1210
|
-
record.overlap === "open" ||
|
|
1211
|
-
record.overlap === "joined_clean" ||
|
|
1212
|
-
record.overlap === "blocked_disjoint_continue" ||
|
|
1213
|
-
record.overlap === "quarantine_required" ||
|
|
1214
|
-
record.overlap === "rebaseline_complete"
|
|
1215
|
-
? record.overlap
|
|
1216
|
-
: "none";
|
|
1217
|
-
const basis: Omit<UltragoalPipelineMetadata, "metadataHash"> = {
|
|
1218
|
-
schemaVersion: 1,
|
|
1219
|
-
goalId,
|
|
1220
|
-
source,
|
|
1221
|
-
eligible: record.eligible === true,
|
|
1222
|
-
dependsOn: normalizePipelineStringArray(record.dependsOn, `metadata ${goalId}.dependsOn`),
|
|
1223
|
-
independentOf: normalizePipelineStringArray(record.independentOf, `metadata ${goalId}.independentOf`),
|
|
1224
|
-
targets: normalizePipelineTargets(record.targets, `metadata ${goalId}.targets`),
|
|
1225
|
-
overlap,
|
|
1226
|
-
...(nonEmptyString(record.overlapId) ? { overlapId: nonEmptyString(record.overlapId)! } : {}),
|
|
1227
|
-
...(nonEmptyString(record.priorGoalId) ? { priorGoalId: nonEmptyString(record.priorGoalId)! } : {}),
|
|
1228
|
-
...(nonEmptyString(record.nextGoalId) ? { nextGoalId: nonEmptyString(record.nextGoalId)! } : {}),
|
|
1229
|
-
...(Array.isArray(record.blockerFootprints)
|
|
1230
|
-
? {
|
|
1231
|
-
blockerFootprints: record.blockerFootprints.map((item, index) =>
|
|
1232
|
-
normalizePipelineTargets(item, `metadata ${goalId}.blockerFootprints[${index}]`),
|
|
1233
|
-
),
|
|
1234
|
-
}
|
|
1235
|
-
: {}),
|
|
1236
|
-
...(nonEmptyString(record.invalidationReason)
|
|
1237
|
-
? { invalidationReason: nonEmptyString(record.invalidationReason)! }
|
|
1238
|
-
: {}),
|
|
1239
|
-
...(nonEmptyString(record.invalidatedAt) ? { invalidatedAt: nonEmptyString(record.invalidatedAt)! } : {}),
|
|
1240
|
-
};
|
|
1241
|
-
return {
|
|
1242
|
-
...basis,
|
|
1243
|
-
metadataHash: nonEmptyString(record.metadataHash) ?? hashPipelineMetadata(basis),
|
|
1244
|
-
} as UltragoalPipelineMetadata;
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
function currentPipelineHash(metadata: UltragoalPipelineMetadata): string {
|
|
1248
|
-
return hashPipelineMetadata(metadata);
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
export function requireFreshPipelineMetadata(goal: UltragoalGoal): UltragoalPipelineMetadata {
|
|
1252
|
-
const metadata = goal.pipelineMetadata;
|
|
1253
|
-
if (!metadata) throw new Error(`Goal ${goal.id} has no pipeline metadata`);
|
|
1254
|
-
if (metadata.metadataHash !== currentPipelineHash(metadata))
|
|
1255
|
-
throw new Error(`Goal ${goal.id} has stale pipeline metadata hash`);
|
|
1256
|
-
return metadata;
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
export function openPipelineOverlap(
|
|
1260
|
-
plan: UltragoalPlan,
|
|
1261
|
-
): { prior: UltragoalGoal; next: UltragoalGoal; overlapId: string } | null {
|
|
1262
|
-
const openGoals = plan.goals.filter(goal => goal.pipelineMetadata?.overlap === "open");
|
|
1263
|
-
if (openGoals.length === 0) return null;
|
|
1264
|
-
const overlapId = openGoals[0]?.pipelineMetadata?.overlapId;
|
|
1265
|
-
if (!overlapId) return null;
|
|
1266
|
-
const peers = openGoals.filter(goal => goal.pipelineMetadata?.overlapId === overlapId);
|
|
1267
|
-
if (peers.length !== 2) return null;
|
|
1268
|
-
const prior = peers[0];
|
|
1269
|
-
const next = peers[1];
|
|
1270
|
-
if (!prior || !next) return null;
|
|
1271
|
-
return { prior, next, overlapId };
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
function invalidatePipelineMetadata(goal: UltragoalGoal, reason: string, now: string): void {
|
|
1275
|
-
const basis: Omit<UltragoalPipelineMetadata, "metadataHash"> = {
|
|
1276
|
-
schemaVersion: 1,
|
|
1277
|
-
goalId: goal.id,
|
|
1278
|
-
source: goal.pipelineMetadata?.source ?? "steering",
|
|
1279
|
-
eligible: false,
|
|
1280
|
-
dependsOn: goal.pipelineMetadata?.dependsOn ?? [],
|
|
1281
|
-
independentOf: goal.pipelineMetadata?.independentOf ?? [],
|
|
1282
|
-
targets: goal.pipelineMetadata?.targets ?? { files: [], surfaces: [] },
|
|
1283
|
-
overlap: "none",
|
|
1284
|
-
invalidationReason: reason,
|
|
1285
|
-
invalidatedAt: now,
|
|
1286
|
-
};
|
|
1287
|
-
goal.pipelineMetadata = withPipelineMetadataHash(basis);
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
910
|
function normalizeGoalStatus(value: unknown): UltragoalGoalStatus {
|
|
1291
911
|
switch (value) {
|
|
1292
912
|
case "pending":
|
|
@@ -1327,7 +947,6 @@ function normalizePlan(raw: unknown): UltragoalPlan {
|
|
|
1327
947
|
const title = nonEmptyString(goalRecord.title) ?? id;
|
|
1328
948
|
const objective = nonEmptyString(goalRecord.objective) ?? title;
|
|
1329
949
|
const goalCreatedAt = nonEmptyString(goalRecord.createdAt) ?? createdAt;
|
|
1330
|
-
const pipelineMetadata = normalizeSavedPipelineMetadata(goalRecord.pipelineMetadata, id);
|
|
1331
950
|
const validationBatch = normalizeSavedValidationBatch(goalRecord.validationBatch, id);
|
|
1332
951
|
return {
|
|
1333
952
|
...goalRecord,
|
|
@@ -1348,7 +967,6 @@ function normalizePlan(raw: unknown): UltragoalPlan {
|
|
|
1348
967
|
typeof goalRecord.completionVerification === "object" && goalRecord.completionVerification !== null
|
|
1349
968
|
? (goalRecord.completionVerification as UltragoalCompletionVerification)
|
|
1350
969
|
: undefined,
|
|
1351
|
-
pipelineMetadata,
|
|
1352
970
|
validationBatch,
|
|
1353
971
|
};
|
|
1354
972
|
});
|
|
@@ -1411,7 +1029,6 @@ export async function getUltragoalStatus(cwd: string, sessionId?: string | null)
|
|
|
1411
1029
|
if (!plan) return { exists: false, status: "missing", paths, counts, goals: [] };
|
|
1412
1030
|
for (const goal of plan.goals) counts[goal.status] += 1;
|
|
1413
1031
|
const currentGoal = plan.goals.find(goal => SCHEDULABLE_STATUSES.has(goal.status));
|
|
1414
|
-
const overlap = openPipelineOverlap(plan);
|
|
1415
1032
|
let status: UltragoalStatusSummary["status"] = "pending";
|
|
1416
1033
|
if (plan.goals.length > 0 && plan.goals.every(goal => TERMINAL_OR_SKIPPED_STATUSES.has(goal.status)))
|
|
1417
1034
|
status = "complete";
|
|
@@ -1441,16 +1058,6 @@ export async function getUltragoalStatus(cwd: string, sessionId?: string | null)
|
|
|
1441
1058
|
counts,
|
|
1442
1059
|
goals: plan.goals,
|
|
1443
1060
|
...nudgeFields,
|
|
1444
|
-
...(overlap
|
|
1445
|
-
? {
|
|
1446
|
-
pipelineOverlap: {
|
|
1447
|
-
overlapId: overlap.overlapId,
|
|
1448
|
-
priorGoalId: overlap.prior.id,
|
|
1449
|
-
nextGoalId: overlap.next.id,
|
|
1450
|
-
status: overlap.next.pipelineMetadata?.overlap,
|
|
1451
|
-
},
|
|
1452
|
-
}
|
|
1453
|
-
: {}),
|
|
1454
1061
|
};
|
|
1455
1062
|
}
|
|
1456
1063
|
export function buildUltragoalHudSummary(
|
|
@@ -1525,8 +1132,6 @@ export async function createUltragoalPlan(input: {
|
|
|
1525
1132
|
brief: string;
|
|
1526
1133
|
gjcGoalMode?: UltragoalGjcGoalMode;
|
|
1527
1134
|
sessionId?: string | null;
|
|
1528
|
-
goalMetadata?: UltragoalGoalMetadataInput[];
|
|
1529
|
-
goalMetadataJson?: string;
|
|
1530
1135
|
validationBatches?: UltragoalValidationBatchInput[];
|
|
1531
1136
|
validationBatchJson?: string;
|
|
1532
1137
|
}): Promise<UltragoalPlan> {
|
|
@@ -1545,31 +1150,18 @@ export async function createUltragoalPlan(input: {
|
|
|
1545
1150
|
updatedAt: now,
|
|
1546
1151
|
}));
|
|
1547
1152
|
const goalIds = new Set(goals.map(goal => goal.id));
|
|
1548
|
-
const metadataInput = input.goalMetadataJson
|
|
1549
|
-
? await readStructuredValue(input.cwd, input.goalMetadataJson)
|
|
1550
|
-
: input.goalMetadata;
|
|
1551
1153
|
const validationBatchInput = input.validationBatchJson
|
|
1552
1154
|
? await readStructuredValue(input.cwd, input.validationBatchJson)
|
|
1553
1155
|
: input.validationBatches;
|
|
1554
|
-
if (metadataInput !== undefined && validationBatchInput !== undefined) {
|
|
1555
|
-
const metadataRows = Array.isArray(metadataInput) ? metadataInput : [];
|
|
1556
|
-
const batchRows = Array.isArray(validationBatchInput) ? validationBatchInput : [];
|
|
1557
|
-
if (metadataRows.length > 0 && batchRows.length > 0)
|
|
1558
|
-
throw new Error("validation-batch-json and goal-metadata-json are mutually exclusive");
|
|
1559
|
-
}
|
|
1560
|
-
const metadata = metadataInput === undefined ? [] : parseGoalMetadataInput(metadataInput, goalIds);
|
|
1561
1156
|
const validationBatches =
|
|
1562
1157
|
validationBatchInput === undefined
|
|
1563
1158
|
? []
|
|
1564
1159
|
: parseValidationBatchInput(validationBatchInput, goalIds, input.gjcGoalMode ?? "aggregate");
|
|
1565
|
-
const metadataByGoalId = new Map(metadata.map(item => [item.goalId, item]));
|
|
1566
1160
|
const validationBatchByGoalId = new Map<string, UltragoalValidationBatchMetadata>();
|
|
1567
1161
|
for (const batch of validationBatches)
|
|
1568
1162
|
for (const memberId of batch.memberIds) validationBatchByGoalId.set(memberId, batch);
|
|
1569
1163
|
for (const goal of goals) {
|
|
1570
|
-
goal.pipelineMetadata = metadataByGoalId.get(goal.id) ?? legacyPipelineMetadata(goal.id);
|
|
1571
1164
|
goal.validationBatch = validationBatchByGoalId.get(goal.id);
|
|
1572
|
-
validateValidationBatchPipelineExclusion(goal);
|
|
1573
1165
|
}
|
|
1574
1166
|
const repositoryBinding = await captureRepositoryBinding(input.cwd, {
|
|
1575
1167
|
displayPath: input.cwd,
|
|
@@ -1604,41 +1196,6 @@ export interface UltragoalRunCompletionState {
|
|
|
1604
1196
|
hasBlockers: boolean;
|
|
1605
1197
|
needsFinalAggregateReceipt: boolean;
|
|
1606
1198
|
}
|
|
1607
|
-
export function requireJsonObjectValue(value: unknown, fieldName: string): JsonObject {
|
|
1608
|
-
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1609
|
-
throw new Error(`${fieldName} must be an object`);
|
|
1610
|
-
if (Object.keys(value).length === 0) throw new Error(`${fieldName} must be non-empty`);
|
|
1611
|
-
return value as JsonObject;
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
export function requireJsonObjectOrArrayValue(value: unknown, fieldName: string): JsonObject | JsonObject[] {
|
|
1615
|
-
if (Array.isArray(value)) {
|
|
1616
|
-
if (value.length === 0) throw new Error(`${fieldName} must be non-empty`);
|
|
1617
|
-
return value.map((item, index) => requireJsonObjectValue(item, `${fieldName}[${index}]`));
|
|
1618
|
-
}
|
|
1619
|
-
return requireJsonObjectValue(value, fieldName);
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
async function readRequiredJsonObject(cwd: string, value: string, fieldName: string): Promise<JsonObject> {
|
|
1623
|
-
return requireJsonObjectValue(await readStructuredValue(cwd, value), fieldName);
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
async function readRequiredJsonObjectOrArray(
|
|
1627
|
-
cwd: string,
|
|
1628
|
-
value: string,
|
|
1629
|
-
fieldName: string,
|
|
1630
|
-
): Promise<JsonObject | JsonObject[]> {
|
|
1631
|
-
return requireJsonObjectOrArrayValue(await readStructuredValue(cwd, value), fieldName);
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
import {
|
|
1635
|
-
joinUltragoalPipelineOverlap,
|
|
1636
|
-
rebaselineUltragoalPipelineOverlap,
|
|
1637
|
-
startUltragoalPipelineOverlap,
|
|
1638
|
-
} from "./ultragoal-pipeline";
|
|
1639
|
-
|
|
1640
|
-
export { joinUltragoalPipelineOverlap, rebaselineUltragoalPipelineOverlap, startUltragoalPipelineOverlap };
|
|
1641
|
-
|
|
1642
1199
|
export function getUltragoalRunCompletionState(
|
|
1643
1200
|
plan: UltragoalPlan,
|
|
1644
1201
|
options: { retryFailed?: boolean } = {},
|
|
@@ -1767,6 +1324,72 @@ export function nonEmptyStringArray(value: unknown): string[] | null {
|
|
|
1767
1324
|
return strings.length === value.length && strings.length > 0 ? strings : null;
|
|
1768
1325
|
}
|
|
1769
1326
|
|
|
1327
|
+
export interface UltragoalQualityGateDiagnostic {
|
|
1328
|
+
path: string;
|
|
1329
|
+
code: string;
|
|
1330
|
+
message: string;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* Collects every quality-gate defect in one pass instead of throwing on the first.
|
|
1335
|
+
* Authoring a valid gate is otherwise an edit/retry loop at the most expensive phase
|
|
1336
|
+
* of a run (#3474). The aggregate error message keeps each individual message verbatim
|
|
1337
|
+
* so existing callers and assertions that match on a single message still work, and
|
|
1338
|
+
* `diagnostics` carries the machine-readable stable `path` + `code` pairs.
|
|
1339
|
+
*/
|
|
1340
|
+
export class UltragoalQualityGateError extends Error {
|
|
1341
|
+
readonly diagnostics: readonly UltragoalQualityGateDiagnostic[];
|
|
1342
|
+
constructor(diagnostics: readonly UltragoalQualityGateDiagnostic[]) {
|
|
1343
|
+
super(diagnostics.map(diagnostic => diagnostic.message).join("\n"));
|
|
1344
|
+
this.name = "UltragoalQualityGateError";
|
|
1345
|
+
this.diagnostics = diagnostics;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
class QualityGateDiagnostics {
|
|
1350
|
+
private readonly collected: UltragoalQualityGateDiagnostic[] = [];
|
|
1351
|
+
|
|
1352
|
+
/**
|
|
1353
|
+
* Runs one independent check. A thrown error is recorded and swallowed so later
|
|
1354
|
+
* checks still run; unrelated defects therefore surface together.
|
|
1355
|
+
*/
|
|
1356
|
+
check(path: string, code: string, run: () => void): boolean {
|
|
1357
|
+
try {
|
|
1358
|
+
run();
|
|
1359
|
+
return true;
|
|
1360
|
+
} catch (error) {
|
|
1361
|
+
this.add(path, code, error instanceof Error ? error.message : String(error));
|
|
1362
|
+
return false;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
async checkAsync(path: string, code: string, run: () => Promise<void>): Promise<boolean> {
|
|
1367
|
+
try {
|
|
1368
|
+
await run();
|
|
1369
|
+
return true;
|
|
1370
|
+
} catch (error) {
|
|
1371
|
+
this.add(path, code, error instanceof Error ? error.message : String(error));
|
|
1372
|
+
return false;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
add(path: string, code: string, message: string): void {
|
|
1377
|
+
this.collected.push({ path, code, message });
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
get empty(): boolean {
|
|
1381
|
+
return this.collected.length === 0;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
get diagnostics(): readonly UltragoalQualityGateDiagnostic[] {
|
|
1385
|
+
return this.collected;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
throwIfAny(): void {
|
|
1389
|
+
if (this.collected.length > 0) throw new UltragoalQualityGateError(this.collected);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1770
1393
|
function requireNonEmptyString(value: unknown, fieldName: string): void {
|
|
1771
1394
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
1772
1395
|
throw new Error(`qualityGate ${fieldName} must be a non-empty string`);
|
|
@@ -2561,10 +2184,56 @@ function requireValidationBatchTuple(
|
|
|
2561
2184
|
}
|
|
2562
2185
|
}
|
|
2563
2186
|
|
|
2187
|
+
const DEFERRABLE_REVIEW_LANES = new Set(["architectReview", "executorQa"]);
|
|
2188
|
+
const DECLARABLE_DEFERRED_LANES: Record<string, string> = {
|
|
2189
|
+
targetedVerification: "targetedVerification",
|
|
2190
|
+
aiSlopCleaner: "aiSlopCleaner",
|
|
2191
|
+
iteration: "iteration",
|
|
2192
|
+
};
|
|
2193
|
+
|
|
2194
|
+
/**
|
|
2195
|
+
* Declaration-vs-evidence check. Per-subgoal enforcement is relaxed: the agent
|
|
2196
|
+
* chooses which verification lanes to run and declares them in `ranLanes`. The
|
|
2197
|
+
* runtime does not dictate that set, but it fails closed when the declaration and
|
|
2198
|
+
* the submitted evidence disagree in either direction, so a declaration can never
|
|
2199
|
+
* be cheaper than the proof behind it.
|
|
2200
|
+
*/
|
|
2201
|
+
function validateDeferredLaneDeclaration(deferred: JsonObject, fieldName: string): void {
|
|
2202
|
+
const declared = stringArray(deferred.ranLanes)?.filter(lane => lane.length > 0);
|
|
2203
|
+
// `ranLanes` is optional for compatibility with gates that only carry the
|
|
2204
|
+
// mandatory targeted-verification lane; an explicit empty array is a claim
|
|
2205
|
+
// that nothing ran and is rejected, because targetedVerification is required.
|
|
2206
|
+
if (declared === undefined) return;
|
|
2207
|
+
const declaredSet = new Set(declared);
|
|
2208
|
+
if (declaredSet.size !== declared.length) throw new Error(`${fieldName}.ranLanes must not repeat a lane`);
|
|
2209
|
+
for (const lane of declaredSet) {
|
|
2210
|
+
if (DEFERRABLE_REVIEW_LANES.has(lane)) {
|
|
2211
|
+
throw new Error(
|
|
2212
|
+
`${fieldName}.ranLanes cannot declare ${lane}: a deferred gate structurally cannot carry review-lane evidence, so it is deferred to the boundary`,
|
|
2213
|
+
);
|
|
2214
|
+
}
|
|
2215
|
+
const evidenceKey = DECLARABLE_DEFERRED_LANES[lane];
|
|
2216
|
+
if (!evidenceKey) throw new Error(`${fieldName}.ranLanes contains unknown lane ${lane}`);
|
|
2217
|
+
const laneRecord = qualityGateObject(deferred[evidenceKey]);
|
|
2218
|
+
if (!laneRecord || !nonEmptyString(laneRecord.evidence)) {
|
|
2219
|
+
throw new Error(`${fieldName}.ranLanes declares ${lane} but ${fieldName}.${evidenceKey}.evidence is missing`);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
if (!declaredSet.has("targetedVerification")) {
|
|
2223
|
+
throw new Error(`${fieldName}.ranLanes must declare targetedVerification`);
|
|
2224
|
+
}
|
|
2225
|
+
for (const [lane, evidenceKey] of Object.entries(DECLARABLE_DEFERRED_LANES)) {
|
|
2226
|
+
if (declaredSet.has(lane)) continue;
|
|
2227
|
+
if (qualityGateObject(deferred[evidenceKey])) {
|
|
2228
|
+
throw new Error(`${fieldName}.${evidenceKey} is present but ${lane} is not declared in ${fieldName}.ranLanes`);
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2564
2233
|
function validateDeferredCompletionQualityGate(
|
|
2565
2234
|
gate: JsonObject,
|
|
2566
2235
|
goal: UltragoalGoal,
|
|
2567
|
-
metadata: UltragoalValidationBatchMetadata,
|
|
2236
|
+
metadata: UltragoalValidationBatchMetadata | undefined,
|
|
2568
2237
|
changeSet?: UltragoalChangeSet,
|
|
2569
2238
|
): void {
|
|
2570
2239
|
const allowedKeys = new Set(["deferredToBatch"]);
|
|
@@ -2575,9 +2244,11 @@ function validateDeferredCompletionQualityGate(
|
|
|
2575
2244
|
if (!deferred) throw new Error("deferred qualityGate requires deferredToBatch object");
|
|
2576
2245
|
if (deferred.kind !== "validation-batch-deferred")
|
|
2577
2246
|
throw new Error("deferredToBatch.kind must be validation-batch-deferred");
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2247
|
+
if (metadata) {
|
|
2248
|
+
requireValidationBatchTuple(metadata, deferred, "deferredToBatch");
|
|
2249
|
+
if (goal.id === metadata.finalGoalId)
|
|
2250
|
+
throw new Error("final validation batch goal cannot use deferredToBatch quality gate");
|
|
2251
|
+
}
|
|
2581
2252
|
const deferredLanes = stringArray(deferred.deferredLanes)?.filter(Boolean).sort();
|
|
2582
2253
|
if (deferredLanes?.join(",") !== "architectReview,executorQa")
|
|
2583
2254
|
throw new Error("deferredToBatch.deferredLanes must be architectReview and executorQa");
|
|
@@ -2585,16 +2256,23 @@ function validateDeferredCompletionQualityGate(
|
|
|
2585
2256
|
if (!targeted || targeted.status !== PASSED_STATUS || !nonEmptyStringArray(targeted.commands))
|
|
2586
2257
|
throw new Error("deferredToBatch.targetedVerification must pass with non-empty commands");
|
|
2587
2258
|
requireNonEmptyString(targeted.evidence, "deferredToBatch.targetedVerification.evidence");
|
|
2259
|
+
// The ai-slop-cleaner pass and a full verification rerun are no longer
|
|
2260
|
+
// mandatory per subgoal; they are boundary duties. When either is supplied it
|
|
2261
|
+
// must still be internally consistent and blocker-free.
|
|
2588
2262
|
const cleaner = qualityGateObject(deferred.aiSlopCleaner);
|
|
2589
|
-
if (
|
|
2590
|
-
|
|
2263
|
+
if (cleaner) {
|
|
2264
|
+
if (cleaner.status !== PASSED_STATUS) throw new Error("deferredToBatch.aiSlopCleaner must pass when present");
|
|
2265
|
+
requireNonEmptyString(cleaner.evidence, "deferredToBatch.aiSlopCleaner.evidence");
|
|
2266
|
+
}
|
|
2591
2267
|
const iteration = qualityGateObject(deferred.iteration);
|
|
2592
|
-
if (
|
|
2593
|
-
throw new Error("deferredToBatch.iteration must pass
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2268
|
+
if (iteration) {
|
|
2269
|
+
if (iteration.status !== PASSED_STATUS) throw new Error("deferredToBatch.iteration must pass when present");
|
|
2270
|
+
if (!nonEmptyStringArray(iteration.rerunCommands))
|
|
2271
|
+
throw new Error("deferredToBatch.iteration.rerunCommands must be non-empty");
|
|
2272
|
+
requireNonEmptyString(iteration.evidence, "deferredToBatch.iteration.evidence");
|
|
2273
|
+
requireEmptyBlockers(iteration.blockers, "deferredToBatch.iteration.blockers");
|
|
2274
|
+
}
|
|
2275
|
+
validateDeferredLaneDeclaration(deferred, "deferredToBatch");
|
|
2598
2276
|
const declaredChangeSet = qualityGateObject(deferred.changeSet);
|
|
2599
2277
|
if (!declaredChangeSet) throw new Error("deferredToBatch.changeSet is required");
|
|
2600
2278
|
if (declaredChangeSet.memberGoalId !== goal.id)
|
|
@@ -2606,6 +2284,80 @@ function validateDeferredCompletionQualityGate(
|
|
|
2606
2284
|
if (declaredChangeSet.changeSetHash !== changeSetHashForPaths(paths))
|
|
2607
2285
|
throw new Error("deferredToBatch.changeSet.changeSetHash does not match declared paths");
|
|
2608
2286
|
}
|
|
2287
|
+
const COHORT_LANE_KEYS = ["cleaner", "architect", "qa"] as const;
|
|
2288
|
+
|
|
2289
|
+
/**
|
|
2290
|
+
* Frozen-source-hash review cohort (#3473). One boundary generation runs at most one
|
|
2291
|
+
* cleaner, one architect, and one QA lane, every lane verdict is bound to the same
|
|
2292
|
+
* immutable source hash, and findings must be joined before any repair starts. Later
|
|
2293
|
+
* generations are delta-only. Cohort state rides the existing `iteration` gate key so
|
|
2294
|
+
* no new top-level quality-gate key is introduced.
|
|
2295
|
+
*/
|
|
2296
|
+
function validateReviewCohort(gate: JsonObject, iteration: JsonObject): void {
|
|
2297
|
+
const cohort = qualityGateObject(iteration.reviewCohort);
|
|
2298
|
+
if (!cohort) throw new Error("qualityGate iteration.reviewCohort is required at the review boundary");
|
|
2299
|
+
const generation = cohort.reviewGeneration;
|
|
2300
|
+
if (typeof generation !== "number" || !Number.isInteger(generation) || generation < 1)
|
|
2301
|
+
throw new Error("iteration.reviewCohort.reviewGeneration must be an integer >= 1");
|
|
2302
|
+
const sourceHash = nonEmptyString(cohort.sourceHash);
|
|
2303
|
+
if (!sourceHash) throw new Error("iteration.reviewCohort.sourceHash is required");
|
|
2304
|
+
if (cohort.joined !== true)
|
|
2305
|
+
throw new Error("iteration.reviewCohort.joined must be true: all lane findings must join before checkpoint");
|
|
2306
|
+
const lanes = qualityGateObject(cohort.lanes);
|
|
2307
|
+
if (!lanes) throw new Error("iteration.reviewCohort.lanes is required");
|
|
2308
|
+
const unsupportedLanes = Object.keys(lanes).filter(key => !(COHORT_LANE_KEYS as readonly string[]).includes(key));
|
|
2309
|
+
if (unsupportedLanes.length > 0)
|
|
2310
|
+
throw new Error(`iteration.reviewCohort.lanes contains unsupported lanes: ${unsupportedLanes.join(", ")}`);
|
|
2311
|
+
for (const lane of COHORT_LANE_KEYS) {
|
|
2312
|
+
if (Array.isArray(lanes[lane]))
|
|
2313
|
+
throw new Error(`iteration.reviewCohort.lanes.${lane} must be one lane per generation, not a list`);
|
|
2314
|
+
const record = qualityGateObject(lanes[lane]);
|
|
2315
|
+
if (!record) throw new Error(`iteration.reviewCohort.lanes.${lane} is required`);
|
|
2316
|
+
const laneHash = nonEmptyString(record.sourceHash);
|
|
2317
|
+
if (!laneHash) throw new Error(`iteration.reviewCohort.lanes.${lane}.sourceHash is required`);
|
|
2318
|
+
if (laneHash !== sourceHash)
|
|
2319
|
+
throw new Error(
|
|
2320
|
+
`iteration.reviewCohort.lanes.${lane}.sourceHash does not match the frozen cohort sourceHash: every lane must inspect the same immutable source`,
|
|
2321
|
+
);
|
|
2322
|
+
if (record.status !== "CLEAR" && record.status !== PASSED_STATUS)
|
|
2323
|
+
throw new Error(`iteration.reviewCohort.lanes.${lane}.status must be CLEAR or passed`);
|
|
2324
|
+
requireNonEmptyString(record.evidence, `iteration.reviewCohort.lanes.${lane}.evidence`);
|
|
2325
|
+
requireEmptyBlockers(record.blockers, `iteration.reviewCohort.lanes.${lane}.blockers`);
|
|
2326
|
+
}
|
|
2327
|
+
// Generation 1 is the full cohort review; every later generation exists only
|
|
2328
|
+
// because one consolidated blocker batch produced it, so its scope is the delta.
|
|
2329
|
+
if (generation > 1) {
|
|
2330
|
+
if (cohort.deltaOnly !== true)
|
|
2331
|
+
throw new Error("iteration.reviewCohort.deltaOnly must be true for reviewGeneration > 1");
|
|
2332
|
+
requireNonEmptyString(cohort.priorGenerationSourceHash, "iteration.reviewCohort.priorGenerationSourceHash");
|
|
2333
|
+
if (nonEmptyString(cohort.priorGenerationSourceHash) === sourceHash)
|
|
2334
|
+
throw new Error(
|
|
2335
|
+
"iteration.reviewCohort.priorGenerationSourceHash must differ from sourceHash: a new generation requires a new frozen source",
|
|
2336
|
+
);
|
|
2337
|
+
const deltaPaths = stringArray(cohort.deltaPaths)?.filter(path => path.length > 0);
|
|
2338
|
+
if (!deltaPaths || deltaPaths.length === 0)
|
|
2339
|
+
throw new Error("iteration.reviewCohort.deltaPaths must be non-empty for reviewGeneration > 1");
|
|
2340
|
+
const expansion = qualityGateObject(cohort.scopeExpansion);
|
|
2341
|
+
if (expansion) {
|
|
2342
|
+
requireNonEmptyString(expansion.severity, "iteration.reviewCohort.scopeExpansion.severity");
|
|
2343
|
+
requireNonEmptyString(expansion.novelty, "iteration.reviewCohort.scopeExpansion.novelty");
|
|
2344
|
+
requireNonEmptyString(expansion.justification, "iteration.reviewCohort.scopeExpansion.justification");
|
|
2345
|
+
}
|
|
2346
|
+
} else if (cohort.deltaOnly === true) {
|
|
2347
|
+
throw new Error("iteration.reviewCohort.deltaOnly cannot be true for the first reviewGeneration");
|
|
2348
|
+
}
|
|
2349
|
+
// The terminal critic is one verdict on the final joined generation, never a
|
|
2350
|
+
// per-lane or per-generation vote.
|
|
2351
|
+
const critic = qualityGateObject(gate.criticReview);
|
|
2352
|
+
if (critic) {
|
|
2353
|
+
const criticHash = nonEmptyString(critic.sourceHash);
|
|
2354
|
+
if (criticHash && criticHash !== sourceHash)
|
|
2355
|
+
throw new Error(
|
|
2356
|
+
"criticReview.sourceHash must match the final joined cohort sourceHash: the terminal critic runs once on the terminal generation",
|
|
2357
|
+
);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2609
2361
|
async function validateCompletionQualityGate(
|
|
2610
2362
|
cwd: string,
|
|
2611
2363
|
gate: JsonObject,
|
|
@@ -2622,8 +2374,26 @@ async function validateCompletionQualityGate(
|
|
|
2622
2374
|
? chooseReceiptKind(options.plan, options.ledger, options.goal, "complete")
|
|
2623
2375
|
: undefined;
|
|
2624
2376
|
const isFinalAggregate = receiptKind === "final-aggregate";
|
|
2377
|
+
// Every independent defect is collected instead of thrown, so one validate run
|
|
2378
|
+
// reports the whole list rather than forcing an edit/retry loop per field (#3474).
|
|
2379
|
+
const found = new QualityGateDiagnostics();
|
|
2625
2380
|
if (batchMode && options.goal && options.goal.id !== batchMode.finalGoalId) {
|
|
2626
|
-
|
|
2381
|
+
found.check("deferredToBatch", "deferred_gate_invalid", () => {
|
|
2382
|
+
validateDeferredCompletionQualityGate(gate, options.goal!, batchMode, options.changeSet);
|
|
2383
|
+
});
|
|
2384
|
+
found.throwIfAny();
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
// Boundary-by-default: in aggregate mode every checkpoint before the run's
|
|
2388
|
+
// final boundary may present the lightweight deferred gate, so heavyweight
|
|
2389
|
+
// architect/QA review runs once per boundary instead of once per story. The
|
|
2390
|
+
// boundary is derived from `chooseReceiptKind` rather than from synthesized
|
|
2391
|
+
// durable batch metadata, which would restale/deadlock on appended goals.
|
|
2392
|
+
if (!batchMode && options.goal && receiptKind === "per-goal" && qualityGateObject(gate.deferredToBatch)) {
|
|
2393
|
+
found.check("deferredToBatch", "deferred_gate_invalid", () => {
|
|
2394
|
+
validateDeferredCompletionQualityGate(gate, options.goal!, undefined, options.changeSet);
|
|
2395
|
+
});
|
|
2396
|
+
found.throwIfAny();
|
|
2627
2397
|
return;
|
|
2628
2398
|
}
|
|
2629
2399
|
if (batchMode && options.goal && options.goal.id === batchMode.finalGoalId) {
|
|
@@ -2636,13 +2406,22 @@ async function validateCompletionQualityGate(
|
|
|
2636
2406
|
]);
|
|
2637
2407
|
const unsupportedKeys = Object.keys(gate).filter(key => !allowedKeys.has(key));
|
|
2638
2408
|
if (unsupportedKeys.length > 0)
|
|
2639
|
-
|
|
2409
|
+
found.add(
|
|
2410
|
+
"qualityGate",
|
|
2411
|
+
"unsupported_keys",
|
|
2412
|
+
`qualityGate contains unsupported keys: ${unsupportedKeys.join(", ")}`,
|
|
2413
|
+
);
|
|
2640
2414
|
if (!qualityGateObject(gate.validationBatchClose))
|
|
2641
|
-
|
|
2415
|
+
found.add(
|
|
2416
|
+
"validationBatchClose",
|
|
2417
|
+
"missing_validation_batch_close",
|
|
2418
|
+
"final validation batch goal requires validationBatchClose",
|
|
2419
|
+
);
|
|
2642
2420
|
}
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2421
|
+
if (qualityGateObject(gate.codeReview)) {
|
|
2422
|
+
found.add(
|
|
2423
|
+
"codeReview",
|
|
2424
|
+
"legacy_code_review_gate",
|
|
2646
2425
|
"checkpoint --status complete requires architect review approval through architectReview, executorQa, and iteration quality-gate evidence; legacy codeReview-only gates are not sufficient",
|
|
2647
2426
|
);
|
|
2648
2427
|
}
|
|
@@ -2653,13 +2432,23 @@ async function validateCompletionQualityGate(
|
|
|
2653
2432
|
);
|
|
2654
2433
|
const unsupportedKeys = Object.keys(gate).filter(key => !allowedKeys.has(key));
|
|
2655
2434
|
if (unsupportedKeys.length > 0) {
|
|
2656
|
-
|
|
2435
|
+
found.add(
|
|
2436
|
+
"qualityGate",
|
|
2437
|
+
"unsupported_keys",
|
|
2438
|
+
`qualityGate contains unsupported keys: ${unsupportedKeys.join(", ")}`,
|
|
2439
|
+
);
|
|
2657
2440
|
}
|
|
2658
2441
|
const architectReview = qualityGateObject(gate.architectReview);
|
|
2659
2442
|
const executorQa = qualityGateObject(gate.executorQa);
|
|
2660
2443
|
const iteration = qualityGateObject(gate.iteration);
|
|
2661
2444
|
if (!architectReview || !executorQa || !iteration) {
|
|
2662
|
-
|
|
2445
|
+
found.add(
|
|
2446
|
+
"qualityGate",
|
|
2447
|
+
"missing_required_sections",
|
|
2448
|
+
"qualityGate requires architectReview, executorQa, and iteration objects",
|
|
2449
|
+
);
|
|
2450
|
+
found.throwIfAny();
|
|
2451
|
+
return;
|
|
2663
2452
|
}
|
|
2664
2453
|
if (isFinalAggregate) {
|
|
2665
2454
|
if (
|
|
@@ -2667,18 +2456,28 @@ async function validateCompletionQualityGate(
|
|
|
2667
2456
|
terminalCriticCeilingReached(options.ledger) &&
|
|
2668
2457
|
!terminalCriticGateOverridden(options.ledger)
|
|
2669
2458
|
) {
|
|
2670
|
-
|
|
2459
|
+
found.add(
|
|
2460
|
+
"criticReview",
|
|
2461
|
+
"terminal_critic_ceiling",
|
|
2671
2462
|
"checkpoint --status complete blocked: terminal-critic ceiling reached; requires human/leader gjc ultragoal record-critic-gate-override before completion",
|
|
2672
2463
|
);
|
|
2673
2464
|
}
|
|
2674
2465
|
const criticReview = qualityGateObject(gate.criticReview);
|
|
2675
2466
|
if (criticReview?.verdict !== "OKAY") {
|
|
2676
|
-
|
|
2467
|
+
found.add(
|
|
2468
|
+
"criticReview.verdict",
|
|
2469
|
+
"critic_verdict_not_okay",
|
|
2677
2470
|
"checkpoint --status complete (final aggregate) requires criticReview with verdict OKAY, non-empty evidence, and empty blockers",
|
|
2678
2471
|
);
|
|
2679
2472
|
}
|
|
2680
|
-
|
|
2681
|
-
|
|
2473
|
+
if (criticReview) {
|
|
2474
|
+
found.check("criticReview.evidence", "missing_evidence", () =>
|
|
2475
|
+
requireNonEmptyString(criticReview.evidence, "criticReview.evidence"),
|
|
2476
|
+
);
|
|
2477
|
+
found.check("criticReview.blockers", "non_empty_blockers", () =>
|
|
2478
|
+
requireEmptyBlockers(criticReview.blockers, "criticReview.blockers"),
|
|
2479
|
+
);
|
|
2480
|
+
}
|
|
2682
2481
|
}
|
|
2683
2482
|
if (
|
|
2684
2483
|
architectReview.architectureStatus !== CLEAN_ARCHITECT_STATUS ||
|
|
@@ -2686,39 +2485,75 @@ async function validateCompletionQualityGate(
|
|
|
2686
2485
|
architectReview.codeStatus !== CLEAN_ARCHITECT_STATUS ||
|
|
2687
2486
|
architectReview.recommendation !== APPROVE_RECOMMENDATION
|
|
2688
2487
|
) {
|
|
2689
|
-
|
|
2488
|
+
found.add(
|
|
2489
|
+
"architectReview",
|
|
2490
|
+
"architect_not_clear",
|
|
2690
2491
|
"checkpoint --status complete requires architect review approval: architectReview architecture/product/code must be CLEAR and recommendation must be APPROVE",
|
|
2691
2492
|
);
|
|
2692
2493
|
}
|
|
2693
2494
|
if (!nonEmptyStringArray(architectReview.commands)) {
|
|
2694
|
-
|
|
2495
|
+
found.add(
|
|
2496
|
+
"architectReview.commands",
|
|
2497
|
+
"missing_command_array",
|
|
2498
|
+
"qualityGate architectReview.commands must be a non-empty string array",
|
|
2499
|
+
);
|
|
2695
2500
|
}
|
|
2696
|
-
|
|
2697
|
-
|
|
2501
|
+
found.check("architectReview.evidence", "missing_evidence", () =>
|
|
2502
|
+
requireNonEmptyString(architectReview.evidence, "architectReview.evidence"),
|
|
2503
|
+
);
|
|
2504
|
+
found.check("architectReview.blockers", "non_empty_blockers", () =>
|
|
2505
|
+
requireEmptyBlockers(architectReview.blockers, "architectReview.blockers"),
|
|
2506
|
+
);
|
|
2698
2507
|
if (
|
|
2699
2508
|
executorQa.status !== PASSED_STATUS ||
|
|
2700
2509
|
executorQa.e2eStatus !== PASSED_STATUS ||
|
|
2701
2510
|
executorQa.redTeamStatus !== PASSED_STATUS
|
|
2702
2511
|
) {
|
|
2703
|
-
|
|
2512
|
+
found.add(
|
|
2513
|
+
"executorQa",
|
|
2514
|
+
"executor_qa_not_passed",
|
|
2515
|
+
"qualityGate executorQa status, e2eStatus, and redTeamStatus must be passed",
|
|
2516
|
+
);
|
|
2704
2517
|
}
|
|
2705
2518
|
if (!nonEmptyStringArray(executorQa.e2eCommands) || !nonEmptyStringArray(executorQa.redTeamCommands)) {
|
|
2706
|
-
|
|
2519
|
+
found.add(
|
|
2520
|
+
"executorQa.e2eCommands",
|
|
2521
|
+
"missing_command_array",
|
|
2522
|
+
"qualityGate executorQa e2eCommands and redTeamCommands must be non-empty string arrays",
|
|
2523
|
+
);
|
|
2707
2524
|
}
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2525
|
+
found.check("executorQa.evidence", "missing_evidence", () =>
|
|
2526
|
+
requireNonEmptyString(executorQa.evidence, "executorQa.evidence"),
|
|
2527
|
+
);
|
|
2528
|
+
found.check("executorQa.blockers", "non_empty_blockers", () =>
|
|
2529
|
+
requireEmptyBlockers(executorQa.blockers, "executorQa.blockers"),
|
|
2530
|
+
);
|
|
2531
|
+
await found.checkAsync("executorQa", "executor_qa_evidence_invalid", () =>
|
|
2532
|
+
validateExecutorQaRedTeamEvidence(cwd, executorQa, { changeSet: options.changeSet }),
|
|
2533
|
+
);
|
|
2711
2534
|
if (iteration.status !== PASSED_STATUS || iteration.fullRerun !== true) {
|
|
2712
|
-
|
|
2535
|
+
found.add("iteration", "iteration_not_passed", "qualityGate iteration must be passed with fullRerun true");
|
|
2713
2536
|
}
|
|
2714
2537
|
if (!nonEmptyStringArray(iteration.rerunCommands)) {
|
|
2715
|
-
|
|
2538
|
+
found.add(
|
|
2539
|
+
"iteration.rerunCommands",
|
|
2540
|
+
"missing_command_array",
|
|
2541
|
+
"qualityGate iteration.rerunCommands must be a non-empty string array",
|
|
2542
|
+
);
|
|
2716
2543
|
}
|
|
2717
|
-
|
|
2718
|
-
|
|
2544
|
+
found.check("iteration.evidence", "missing_evidence", () =>
|
|
2545
|
+
requireNonEmptyString(iteration.evidence, "iteration.evidence"),
|
|
2546
|
+
);
|
|
2547
|
+
found.check("iteration.blockers", "non_empty_blockers", () =>
|
|
2548
|
+
requireEmptyBlockers(iteration.blockers, "iteration.blockers"),
|
|
2549
|
+
);
|
|
2550
|
+
found.check("iteration.reviewCohort", "review_cohort_invalid", () => validateReviewCohort(gate, iteration));
|
|
2719
2551
|
if (batchMode && options.goal && options.plan && options.ledger) {
|
|
2720
|
-
|
|
2552
|
+
found.check("validationBatchClose", "batch_close_invalid", () =>
|
|
2553
|
+
validateBatchCloseQualityGate(gate, options.plan!, batchMode, options.ledger!, options.changeSet),
|
|
2554
|
+
);
|
|
2721
2555
|
}
|
|
2556
|
+
found.throwIfAny();
|
|
2722
2557
|
}
|
|
2723
2558
|
|
|
2724
2559
|
function validateBatchCloseQualityGate(
|
|
@@ -2940,6 +2775,56 @@ function hydrateReviewedBatchReplacementClose(input: {
|
|
|
2940
2775
|
},
|
|
2941
2776
|
};
|
|
2942
2777
|
}
|
|
2778
|
+
/**
|
|
2779
|
+
* Read-only quality-gate validation (#3474). Applies exactly the same rules as
|
|
2780
|
+
* `checkpoint --status complete` — including deferred-vs-boundary gate selection and
|
|
2781
|
+
* artifact existence checks — but never touches `goals.json`, `ledger.jsonl`, or goal
|
|
2782
|
+
* state, and reports every diagnostic in one run instead of the first failure.
|
|
2783
|
+
*/
|
|
2784
|
+
export async function validateUltragoalQualityGateReadOnly(input: {
|
|
2785
|
+
cwd: string;
|
|
2786
|
+
qualityGateJson: string;
|
|
2787
|
+
goalId?: string;
|
|
2788
|
+
sessionId?: string | null;
|
|
2789
|
+
}): Promise<{ valid: boolean; errors: readonly UltragoalQualityGateDiagnostic[] }> {
|
|
2790
|
+
const sessionId = input.sessionId?.trim() || currentUltragoalSessionId(input.cwd);
|
|
2791
|
+
const plan = await readUltragoalPlan(input.cwd, sessionId);
|
|
2792
|
+
const goal = input.goalId
|
|
2793
|
+
? plan?.goals.find(item => item.id === input.goalId)
|
|
2794
|
+
: plan?.goals.find(item => SCHEDULABLE_STATUSES.has(item.status));
|
|
2795
|
+
if (input.goalId && !goal) {
|
|
2796
|
+
return {
|
|
2797
|
+
valid: false,
|
|
2798
|
+
errors: [{ path: "goalId", code: "unknown_goal", message: `Unknown ultragoal goal ${input.goalId}` }],
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
const gate = qualityGateObject(await readStructuredValue(input.cwd, input.qualityGateJson));
|
|
2802
|
+
if (!gate) {
|
|
2803
|
+
return {
|
|
2804
|
+
valid: false,
|
|
2805
|
+
errors: [{ path: "qualityGate", code: "not_an_object", message: "qualityGate must be a JSON object" }],
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
const ledger = plan ? await readUltragoalLedger(input.cwd, sessionId) : undefined;
|
|
2809
|
+
const changeSet = await computeCheckpointChangeSet(input.cwd);
|
|
2810
|
+
try {
|
|
2811
|
+
await validateCompletionQualityGate(input.cwd, gate, { changeSet, plan: plan ?? undefined, goal, ledger });
|
|
2812
|
+
return { valid: true, errors: [] };
|
|
2813
|
+
} catch (error) {
|
|
2814
|
+
if (error instanceof UltragoalQualityGateError) return { valid: false, errors: error.diagnostics };
|
|
2815
|
+
return {
|
|
2816
|
+
valid: false,
|
|
2817
|
+
errors: [
|
|
2818
|
+
{
|
|
2819
|
+
path: "qualityGate",
|
|
2820
|
+
code: "validation_failed",
|
|
2821
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2822
|
+
},
|
|
2823
|
+
],
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2943
2828
|
async function readRequiredCompletionQualityGate(
|
|
2944
2829
|
cwd: string,
|
|
2945
2830
|
value: string | undefined,
|
|
@@ -2979,46 +2864,6 @@ async function readRequiredCompletionQualityGate(
|
|
|
2979
2864
|
return hydratedGate;
|
|
2980
2865
|
}
|
|
2981
2866
|
|
|
2982
|
-
function validatePipelineCheckpointSafety(
|
|
2983
|
-
plan: UltragoalPlan,
|
|
2984
|
-
goal: UltragoalGoal,
|
|
2985
|
-
changeSet?: UltragoalChangeSet,
|
|
2986
|
-
): void {
|
|
2987
|
-
const metadata = goal.pipelineMetadata;
|
|
2988
|
-
if (!metadata) return;
|
|
2989
|
-
validateValidationBatchPipelineExclusion(goal);
|
|
2990
|
-
requireFreshPipelineMetadata(goal);
|
|
2991
|
-
if (metadata.overlap === "open") {
|
|
2992
|
-
throw new Error(
|
|
2993
|
-
`Cannot complete ${goal.id} while pipeline overlap ${metadata.overlapId ?? ""} is open; join or quarantine first.`,
|
|
2994
|
-
);
|
|
2995
|
-
}
|
|
2996
|
-
if (metadata.overlap === "quarantine_required") {
|
|
2997
|
-
throw new Error(
|
|
2998
|
-
`Cannot complete ${goal.id} while pipeline overlap ${metadata.overlapId ?? ""} requires rebaseline.`,
|
|
2999
|
-
);
|
|
3000
|
-
}
|
|
3001
|
-
if (metadata.goalId === metadata.priorGoalId && metadata.overlap !== "none" && metadata.overlap !== "joined_clean") {
|
|
3002
|
-
throw new Error(
|
|
3003
|
-
`Cannot complete ${goal.id} without a clean join for pipeline overlap ${metadata.overlapId ?? ""}.`,
|
|
3004
|
-
);
|
|
3005
|
-
}
|
|
3006
|
-
const peer = pipelinePeer(plan, metadata);
|
|
3007
|
-
if (changeSet && metadata.overlap !== "none") {
|
|
3008
|
-
const peerTargets = peer?.pipelineMetadata?.targets;
|
|
3009
|
-
for (const row of changeSet.paths) {
|
|
3010
|
-
const ownedByGoal = pipelineTargetsCoverPath(metadata.targets, row.path);
|
|
3011
|
-
const ownedByPeer = peerTargets ? pipelineTargetsCoverPath(peerTargets, row.path) : false;
|
|
3012
|
-
if (ownedByGoal && ownedByPeer)
|
|
3013
|
-
throw new Error(`Cannot complete ${goal.id} with shared pipeline change-set path ${row.path}.`);
|
|
3014
|
-
if (!ownedByGoal && !ownedByPeer)
|
|
3015
|
-
throw new Error(`Cannot complete ${goal.id} with unattributable pipeline change-set path ${row.path}.`);
|
|
3016
|
-
if (!ownedByGoal && ownedByPeer)
|
|
3017
|
-
throw new Error(`Cannot complete ${goal.id} with next-goal pipeline change-set path ${row.path}.`);
|
|
3018
|
-
}
|
|
3019
|
-
}
|
|
3020
|
-
}
|
|
3021
|
-
|
|
3022
2867
|
function validateCompleteCheckpointTargetGoal(goal: UltragoalGoal): void {
|
|
3023
2868
|
if (COMPLETE_CHECKPOINT_ALLOWED_PRE_STATUSES.has(goal.status)) return;
|
|
3024
2869
|
if (goal.status === "pending") {
|
|
@@ -3091,8 +2936,13 @@ export async function checkpointUltragoalGoal(input: {
|
|
|
3091
2936
|
// receipt, the replay is a genuine re-verification: it must run the full
|
|
3092
2937
|
// quality gate and mint a fresh receipt, otherwise a completed goal with a
|
|
3093
2938
|
// context-staled receipt can never be repaired (different evidence is
|
|
3094
|
-
// rejected on complete goals by design). A
|
|
3095
|
-
//
|
|
2939
|
+
// rejected on complete goals by design). A final-aggregate receipt whose
|
|
2940
|
+
// recorded checkpoint gate lacks a clean criticReview OKAY is likewise
|
|
2941
|
+
// repair-eligible: it is not "stale", but the completion guard rejects it
|
|
2942
|
+
// forever (active_missing_critic_verdict), so a no-op replay would leave
|
|
2943
|
+
// the run permanently unable to complete even after the terminal critic
|
|
2944
|
+
// records OKAY. A mutated goal row keeps the fail-loud tamper handling in
|
|
2945
|
+
// the idempotent branch below.
|
|
3096
2946
|
const staleCompleteReceiptReplay =
|
|
3097
2947
|
input.status === "complete" &&
|
|
3098
2948
|
goal.status === "complete" &&
|
|
@@ -3100,13 +2950,14 @@ export async function checkpointUltragoalGoal(input: {
|
|
|
3100
2950
|
Boolean(matchingIdempotentEvent) &&
|
|
3101
2951
|
(!goal.completionVerification ||
|
|
3102
2952
|
(goal.completionVerification.verifiedAt === goal.updatedAt &&
|
|
3103
|
-
validateReceiptFreshBase({
|
|
2953
|
+
(validateReceiptFreshBase({
|
|
3104
2954
|
plan,
|
|
3105
2955
|
ledger: ledgerBefore,
|
|
3106
2956
|
goal,
|
|
3107
2957
|
receipt: goal.completionVerification,
|
|
3108
2958
|
receiptKind: goal.completionVerification.receiptKind,
|
|
3109
|
-
}) !== null
|
|
2959
|
+
}) !== null ||
|
|
2960
|
+
finalAggregateReceiptMissingCriticOkay(ledgerBefore, goal.completionVerification))));
|
|
3110
2961
|
if (
|
|
3111
2962
|
goal.status === input.status &&
|
|
3112
2963
|
goal.evidence === evidence &&
|
|
@@ -3155,9 +3006,8 @@ export async function checkpointUltragoalGoal(input: {
|
|
|
3155
3006
|
return plan;
|
|
3156
3007
|
}
|
|
3157
3008
|
const changeSet = input.status === "complete" ? await computeCheckpointChangeSet(input.cwd) : undefined;
|
|
3158
|
-
if (input.status === "complete") {
|
|
3159
|
-
|
|
3160
|
-
if (!staleCompleteReceiptReplay) validateCompleteCheckpointTargetGoal(goal);
|
|
3009
|
+
if (input.status === "complete" && !staleCompleteReceiptReplay) {
|
|
3010
|
+
validateCompleteCheckpointTargetGoal(goal);
|
|
3161
3011
|
}
|
|
3162
3012
|
const qualityGateJson =
|
|
3163
3013
|
input.status === "complete"
|
|
@@ -3414,7 +3264,6 @@ async function addUltragoalSubgoalToPlan(input: {
|
|
|
3414
3264
|
createdAt: now,
|
|
3415
3265
|
updatedAt: now,
|
|
3416
3266
|
steering: { kind, evidence, rationale },
|
|
3417
|
-
pipelineMetadata: legacyPipelineMetadata(nextId),
|
|
3418
3267
|
});
|
|
3419
3268
|
input.plan.updatedAt = now;
|
|
3420
3269
|
await writePlan(input.cwd, input.plan);
|
|
@@ -3465,7 +3314,6 @@ async function splitUltragoalSubgoal(input: {
|
|
|
3465
3314
|
target.evidence = evidence;
|
|
3466
3315
|
target.updatedAt = now;
|
|
3467
3316
|
target.steering = { kind, evidence, rationale, replacementGoalIds };
|
|
3468
|
-
invalidatePipelineMetadata(target, "split_subgoal_superseded", now);
|
|
3469
3317
|
clearValidationBatchForBatch(input.plan, target.validationBatch);
|
|
3470
3318
|
const replacementGoals = replacements.map(
|
|
3471
3319
|
(replacement, index): UltragoalGoal => ({
|
|
@@ -3476,7 +3324,6 @@ async function splitUltragoalSubgoal(input: {
|
|
|
3476
3324
|
createdAt: now,
|
|
3477
3325
|
updatedAt: now,
|
|
3478
3326
|
steering: { kind: "split_replacement", sourceGoalId: target.id, evidence, rationale },
|
|
3479
|
-
pipelineMetadata: legacyPipelineMetadata(replacementGoalIds[index]!),
|
|
3480
3327
|
}),
|
|
3481
3328
|
);
|
|
3482
3329
|
const targetIndex = input.plan.goals.findIndex(goal => goal.id === target.id);
|
|
@@ -3575,7 +3422,6 @@ async function revisePendingUltragoalWording(input: {
|
|
|
3575
3422
|
const now = new Date().toISOString();
|
|
3576
3423
|
goal.updatedAt = now;
|
|
3577
3424
|
goal.steering = { kind, evidence, rationale, changedFields };
|
|
3578
|
-
invalidatePipelineMetadata(goal, "revised_pending_wording", now);
|
|
3579
3425
|
clearValidationBatchForBatch(input.plan, goal.validationBatch);
|
|
3580
3426
|
input.plan.updatedAt = now;
|
|
3581
3427
|
await writePlan(input.cwd, input.plan);
|
|
@@ -4138,17 +3984,7 @@ const FLAGS_WITH_VALUES = new Set([
|
|
|
4138
3984
|
"--replacements-json",
|
|
4139
3985
|
"--order-json",
|
|
4140
3986
|
"--classification",
|
|
4141
|
-
"--goal-metadata-json",
|
|
4142
3987
|
"--validation-batch-json",
|
|
4143
|
-
"--prior-goal-id",
|
|
4144
|
-
"--next-goal-id",
|
|
4145
|
-
"--review-handles-json",
|
|
4146
|
-
"--qa-handles-json",
|
|
4147
|
-
"--implementation-handle-json",
|
|
4148
|
-
"--overlap-id",
|
|
4149
|
-
"--review-result-json",
|
|
4150
|
-
"--qa-result-json",
|
|
4151
|
-
"--target-state-json",
|
|
4152
3988
|
]);
|
|
4153
3989
|
|
|
4154
3990
|
function isHelpArg(arg: string): boolean {
|
|
@@ -4295,10 +4131,8 @@ function renderUltragoalHelp(args: readonly string[]): string | null {
|
|
|
4295
4131
|
" classify-blocker",
|
|
4296
4132
|
" record-critic-verdict",
|
|
4297
4133
|
" record-critic-gate-override",
|
|
4134
|
+
" quality-gate validate",
|
|
4298
4135
|
|
|
4299
|
-
" start-pipeline-overlap",
|
|
4300
|
-
" join-pipeline-overlap",
|
|
4301
|
-
" rebaseline-pipeline-overlap",
|
|
4302
4136
|
"",
|
|
4303
4137
|
"Run `gjc ultragoal checkpoint --help`, `gjc ultragoal review --help`, `gjc ultragoal classify-blocker --help`, `gjc ultragoal record-critic-verdict --help`, or `gjc ultragoal record-critic-gate-override --help` for command-specific requirements.",
|
|
4304
4138
|
"",
|
|
@@ -4629,18 +4463,11 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
|
|
|
4629
4463
|
return { status: 0, stdout: renderStatus(await getUltragoalStatus(cwd, sessionId), json) };
|
|
4630
4464
|
case "create":
|
|
4631
4465
|
case "create-goals": {
|
|
4632
|
-
if (
|
|
4633
|
-
flagValue(args, "--goal-metadata-json") !== undefined &&
|
|
4634
|
-
flagValue(args, "--validation-batch-json") !== undefined
|
|
4635
|
-
) {
|
|
4636
|
-
throw new Error("--validation-batch-json and --goal-metadata-json are mutually exclusive");
|
|
4637
|
-
}
|
|
4638
4466
|
const mode = flagValue(args, "--gjc-goal-mode") === "per-story" ? "per-story" : "aggregate";
|
|
4639
4467
|
const plan = await createUltragoalPlan({
|
|
4640
4468
|
cwd,
|
|
4641
4469
|
brief: await readBrief(cwd, args),
|
|
4642
4470
|
gjcGoalMode: mode,
|
|
4643
|
-
goalMetadataJson: flagValue(args, "--goal-metadata-json"),
|
|
4644
4471
|
validationBatchJson: flagValue(args, "--validation-batch-json"),
|
|
4645
4472
|
});
|
|
4646
4473
|
return {
|
|
@@ -4682,6 +4509,38 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
|
|
|
4682
4509
|
stdout: renderCheckpointContinuation(result, status, json, cwd),
|
|
4683
4510
|
};
|
|
4684
4511
|
}
|
|
4512
|
+
case "quality-gate": {
|
|
4513
|
+
const positional = args.filter(arg => !arg.startsWith("--"));
|
|
4514
|
+
const subcommand = positional[1];
|
|
4515
|
+
if (subcommand !== "validate") {
|
|
4516
|
+
return {
|
|
4517
|
+
status: 1,
|
|
4518
|
+
stderr: `Unknown gjc ultragoal quality-gate subcommand: ${subcommand ?? "(missing)"}; only validate is supported\n`,
|
|
4519
|
+
};
|
|
4520
|
+
}
|
|
4521
|
+
const qualityGateJson = flagValue(args, "--quality-gate-json");
|
|
4522
|
+
if (!qualityGateJson?.trim()) {
|
|
4523
|
+
return { status: 1, stderr: "quality-gate validate requires --quality-gate-json\n" };
|
|
4524
|
+
}
|
|
4525
|
+
const result = await validateUltragoalQualityGateReadOnly({
|
|
4526
|
+
cwd,
|
|
4527
|
+
qualityGateJson,
|
|
4528
|
+
goalId: flagValue(args, "--goal-id"),
|
|
4529
|
+
});
|
|
4530
|
+
if (json) {
|
|
4531
|
+
return {
|
|
4532
|
+
status: result.valid ? 0 : 1,
|
|
4533
|
+
stdout: `${JSON.stringify({ valid: result.valid, errors: result.errors }, null, 2)}\n`,
|
|
4534
|
+
};
|
|
4535
|
+
}
|
|
4536
|
+
if (result.valid) return { status: 0, stdout: "quality gate is valid.\n" };
|
|
4537
|
+
return {
|
|
4538
|
+
status: 1,
|
|
4539
|
+
stderr: `${result.errors.length} quality-gate error(s):\n${result.errors
|
|
4540
|
+
.map(diagnostic => ` ${diagnostic.path} [${diagnostic.code}]: ${diagnostic.message}`)
|
|
4541
|
+
.join("\n")}\n`,
|
|
4542
|
+
};
|
|
4543
|
+
}
|
|
4685
4544
|
case "review": {
|
|
4686
4545
|
const result = await runUltragoalReview(cwd, args);
|
|
4687
4546
|
return {
|
|
@@ -4781,65 +4640,6 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
|
|
|
4781
4640
|
: `Recorded terminal critic gate override event-id=${String(event.eventId)}.\n`,
|
|
4782
4641
|
};
|
|
4783
4642
|
}
|
|
4784
|
-
case "start-pipeline-overlap": {
|
|
4785
|
-
const receipt = await startUltragoalPipelineOverlap({
|
|
4786
|
-
cwd,
|
|
4787
|
-
priorGoalId: flagValue(args, "--prior-goal-id") ?? "",
|
|
4788
|
-
nextGoalId: flagValue(args, "--next-goal-id") ?? "",
|
|
4789
|
-
reviewHandles: await readRequiredJsonObjectOrArray(
|
|
4790
|
-
cwd,
|
|
4791
|
-
flagValue(args, "--review-handles-json") ?? "",
|
|
4792
|
-
"review handles",
|
|
4793
|
-
),
|
|
4794
|
-
qaHandles: await readRequiredJsonObjectOrArray(
|
|
4795
|
-
cwd,
|
|
4796
|
-
flagValue(args, "--qa-handles-json") ?? "",
|
|
4797
|
-
"QA handles",
|
|
4798
|
-
),
|
|
4799
|
-
implementationHandle: await readRequiredJsonObject(
|
|
4800
|
-
cwd,
|
|
4801
|
-
flagValue(args, "--implementation-handle-json") ?? "",
|
|
4802
|
-
"implementation handle",
|
|
4803
|
-
),
|
|
4804
|
-
});
|
|
4805
|
-
return {
|
|
4806
|
-
status: 0,
|
|
4807
|
-
stdout: json ? renderCliWriteReceipt(receipt) : `Started pipeline overlap ${receipt.overlap_id}.\n`,
|
|
4808
|
-
};
|
|
4809
|
-
}
|
|
4810
|
-
case "join-pipeline-overlap": {
|
|
4811
|
-
const receipt = await joinUltragoalPipelineOverlap({
|
|
4812
|
-
cwd,
|
|
4813
|
-
overlapId: flagValue(args, "--overlap-id") ?? "",
|
|
4814
|
-
reviewResult: await readRequiredJsonObject(
|
|
4815
|
-
cwd,
|
|
4816
|
-
flagValue(args, "--review-result-json") ?? "",
|
|
4817
|
-
"review result",
|
|
4818
|
-
),
|
|
4819
|
-
qaResult: await readRequiredJsonObject(cwd, flagValue(args, "--qa-result-json") ?? "", "QA result"),
|
|
4820
|
-
});
|
|
4821
|
-
return {
|
|
4822
|
-
status: 0,
|
|
4823
|
-
stdout: json ? renderCliWriteReceipt(receipt) : `Joined pipeline overlap ${receipt.overlap_id}.\n`,
|
|
4824
|
-
};
|
|
4825
|
-
}
|
|
4826
|
-
case "rebaseline-pipeline-overlap": {
|
|
4827
|
-
const receipt = await rebaselineUltragoalPipelineOverlap({
|
|
4828
|
-
cwd,
|
|
4829
|
-
overlapId: flagValue(args, "--overlap-id") ?? "",
|
|
4830
|
-
goalId: flagValue(args, "--goal-id") ?? "",
|
|
4831
|
-
evidence: flagValue(args, "--evidence") ?? "",
|
|
4832
|
-
targetState: await readRequiredJsonObject(
|
|
4833
|
-
cwd,
|
|
4834
|
-
flagValue(args, "--target-state-json") ?? "",
|
|
4835
|
-
"target state",
|
|
4836
|
-
),
|
|
4837
|
-
});
|
|
4838
|
-
return {
|
|
4839
|
-
status: 0,
|
|
4840
|
-
stdout: json ? renderCliWriteReceipt(receipt) : `Rebaselined pipeline overlap ${receipt.overlap_id}.\n`,
|
|
4841
|
-
};
|
|
4842
|
-
}
|
|
4843
4643
|
default:
|
|
4844
4644
|
return { status: 1, stderr: `Unknown gjc ultragoal command: ${command}\n` };
|
|
4845
4645
|
}
|
|
@@ -4860,9 +4660,6 @@ const RECONCILE_COMMANDS = new Set([
|
|
|
4860
4660
|
"classify-blocker",
|
|
4861
4661
|
"record-critic-verdict",
|
|
4862
4662
|
"record-critic-gate-override",
|
|
4863
|
-
"start-pipeline-overlap",
|
|
4864
|
-
"join-pipeline-overlap",
|
|
4865
|
-
"rebaseline-pipeline-overlap",
|
|
4866
4663
|
]);
|
|
4867
4664
|
|
|
4868
4665
|
/**
|
|
@@ -4899,7 +4696,6 @@ async function reconcileUltragoalState(cwd: string): Promise<void> {
|
|
|
4899
4696
|
if (summary.nudgeRemaining !== undefined) payload.nudge_remaining = summary.nudgeRemaining;
|
|
4900
4697
|
if (summary.nudgeGoalId !== undefined) payload.nudge_goal_id = summary.nudgeGoalId;
|
|
4901
4698
|
if (summary.nudgeTargetKind !== undefined) payload.nudge_target_kind = summary.nudgeTargetKind;
|
|
4902
|
-
if (summary.pipelineOverlap) payload.pipeline_overlap = summary.pipelineOverlap;
|
|
4903
4699
|
const ledgerText = await Bun.file(summary.paths.ledgerPath)
|
|
4904
4700
|
.text()
|
|
4905
4701
|
.catch(() => "");
|