@gajae-code/coding-agent 0.11.6 → 0.11.8
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 +92 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +34 -4
- package/dist/types/config/model-profile-activation.d.ts +4 -3
- package/dist/types/config/model-profile-contract.d.ts +38 -0
- package/dist/types/config/settings-schema.d.ts +70 -7
- package/dist/types/edit/modes/patch.d.ts +13 -0
- package/dist/types/edit/path-mutation-lock.d.ts +15 -0
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -2
- package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +6 -5
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +43 -5
- package/dist/types/main.d.ts +32 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/tool-status-header.d.ts +4 -2
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +7 -2
- package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
- package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
- package/dist/types/modes/types.d.ts +4 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -1
- package/dist/types/sdk/bus/config-commands.d.ts +2 -2
- package/dist/types/sdk/bus/config.d.ts +2 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +26 -1
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +18 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +189 -4
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +1 -1
- package/dist/types/sdk/host/query/handlers.d.ts +9 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/sdk/prompt-status.d.ts +88 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/sdk/startup-capability.d.ts +3 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +30 -5
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +32 -9
- package/dist/types/session/internal/managed-session-storage.d.ts +27 -10
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/session-manager.d.ts +25 -4
- package/dist/types/session/session-storage.d.ts +18 -0
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +94 -0
- package/dist/types/tools/ask.d.ts +454 -174
- package/dist/types/tools/fetch.d.ts +5 -8
- package/dist/types/tools/image-gen.d.ts +63 -1
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -8
- package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
- package/scripts/dogfood-repository-binding.ts +209 -0
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/scripts/run-test-manifest.ts +4 -4
- package/src/capability/index.ts +50 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/args.ts +2 -92
- package/src/cli/notify-cli.ts +5 -38
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +52 -9
- package/src/commands/daemon.ts +22 -5
- package/src/commands/sdk.ts +44 -11
- package/src/config/keybindings.ts +158 -25
- package/src/config/model-profile-activation.ts +7 -18
- package/src/config/model-profile-contract.ts +161 -0
- package/src/config/model-profiles.ts +18 -0
- package/src/config/model-registry.ts +11 -1
- package/src/config/settings-schema.ts +73 -7
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
- package/src/discovery/ssh.ts +12 -1
- package/src/edit/index.ts +3 -6
- package/src/edit/modes/patch.ts +38 -1
- package/src/edit/modes/replace.ts +42 -0
- package/src/edit/path-mutation-lock.ts +112 -0
- package/src/eval/py/executor.ts +30 -11
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/gjc-runtime/launch-tmux.ts +1 -1
- package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/gjc-runtime/ralplan-runtime.ts +75 -3
- package/src/gjc-runtime/repository-binding.ts +267 -0
- package/src/gjc-runtime/session-state-sidecar.ts +25 -9
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
- package/src/gjc-runtime/tmux-sessions.ts +45 -6
- package/src/gjc-runtime/ultragoal-guard.ts +93 -15
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +456 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +23 -0
- package/src/internal-urls/docs-index.generated.ts +12 -10
- package/src/internal-urls/local-protocol.ts +64 -21
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +111 -37
- package/src/modes/DESIGN.md +5 -0
- package/src/modes/acp/acp-agent.ts +50 -16
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/model-selector.ts +73 -11
- package/src/modes/components/plan-preview-overlay.ts +1 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
- package/src/modes/components/tool-status-header.ts +25 -4
- package/src/modes/components/welcome.ts +22 -18
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/command-controller.ts +97 -54
- package/src/modes/controllers/event-controller.ts +10 -0
- package/src/modes/controllers/input-controller.ts +38 -2
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +149 -63
- package/src/modes/interactive-mode.ts +57 -34
- package/src/modes/prompt-action-autocomplete.ts +32 -13
- package/src/modes/prompt-suggestion-controller.ts +96 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/hotkeys-markdown.ts +64 -36
- package/src/modes/utils/ui-helpers.ts +5 -3
- package/src/prompts/agents/architect.md +0 -1
- package/src/prompts/agents/critic.md +0 -1
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/system/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +6 -4
- package/src/prompts/tools/cron.md +3 -1
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/json-rpc.ts +23 -9
- package/src/runtime-mcp/manager.ts +51 -53
- package/src/runtime-mcp/oauth-flow.ts +0 -72
- package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
- package/src/runtime-mcp/transports/http.ts +63 -22
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/lifecycle.ts +185 -36
- package/src/sdk/bus/chat-daemon-control.ts +19 -5
- package/src/sdk/bus/config-commands.ts +2 -2
- package/src/sdk/bus/config.ts +9 -1
- package/src/sdk/bus/conversation-store.ts +40 -6
- package/src/sdk/bus/index.ts +244 -70
- package/src/sdk/bus/lifecycle-commands.ts +63 -10
- package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
- package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
- package/src/sdk/bus/notification-orchestration.ts +12 -0
- package/src/sdk/bus/notification-service.ts +68 -20
- package/src/sdk/bus/prompt-reconciliation.ts +239 -0
- package/src/sdk/bus/recent-activity.ts +90 -26
- package/src/sdk/bus/telegram-daemon-contract.ts +18 -3
- package/src/sdk/bus/telegram-daemon-control.ts +147 -35
- package/src/sdk/bus/telegram-daemon.ts +2463 -450
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/host/control/dispatch.ts +1 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +2 -6
- package/src/sdk/host/query/handlers.ts +70 -5
- package/src/sdk/index.ts +12 -0
- package/src/sdk/prompt-status.ts +97 -0
- package/src/sdk/protocol/operation-inventory.generated.json +58 -0
- package/src/sdk/protocol/operation-registry.ts +18 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +31 -5
- package/src/sdk/startup-capability.ts +9 -1
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +292 -154
- package/src/session/blob-store.ts +25 -7
- package/src/session/btw-contract.ts +59 -0
- package/src/session/internal/managed-session-scope.ts +1163 -137
- package/src/session/internal/managed-session-storage.ts +397 -234
- package/src/session/internal/native-publish-outcome.ts +266 -0
- package/src/session/session-manager.ts +620 -403
- package/src/session/session-storage.ts +128 -11
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/task/gjc-command.ts +2 -2
- package/src/task/index.ts +121 -15
- package/src/task/receipt.ts +3 -0
- package/src/task/types.ts +34 -3
- package/src/tools/ask.ts +199 -89
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser/screenshot-format.ts +6 -1
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +13 -131
- package/src/tools/image-gen.ts +476 -23
- package/src/tools/index.ts +4 -0
- package/src/tools/read.ts +2 -0
- package/src/tools/render-utils.ts +7 -1
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +140 -27
- package/src/web/scrapers/docs-rs.ts +30 -5
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authoritative repository/worktree binding for plans and delegated tasks (#2901).
|
|
3
|
+
*
|
|
4
|
+
* Multi-repo parent directories must not let QA/review lanes infer a sibling repo
|
|
5
|
+
* from prose. Bindings carry a validated worktree root + git common-dir identity
|
|
6
|
+
* so spawn/handoff can fail closed on mismatch.
|
|
7
|
+
*/
|
|
8
|
+
import * as fssync from "node:fs";
|
|
9
|
+
import * as fs from "node:fs/promises";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { type GitRepository, head, repo } from "../utils/git";
|
|
12
|
+
|
|
13
|
+
export const REPOSITORY_BINDING_SCHEMA = "gjc.repository_binding.v1" as const;
|
|
14
|
+
|
|
15
|
+
export interface RepositoryBinding {
|
|
16
|
+
schema: typeof REPOSITORY_BINDING_SCHEMA;
|
|
17
|
+
/** Canonical realpath of the git worktree root (or resolved cwd root). */
|
|
18
|
+
worktreeRoot: string;
|
|
19
|
+
/** Git common dir realpath when available; null outside a git checkout. */
|
|
20
|
+
commonDir: string | null;
|
|
21
|
+
/** Optional repo-relative subdirectory the lane should operate under. */
|
|
22
|
+
relativeSubdir?: string;
|
|
23
|
+
/** Optional display path (may be non-canonical); never used for authority. */
|
|
24
|
+
displayPath?: string;
|
|
25
|
+
/** Optional baseline HEAD at capture time. */
|
|
26
|
+
head?: string;
|
|
27
|
+
/** Optional branch name at capture time (when not detached). */
|
|
28
|
+
branch?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type RepositoryBindingErrorCode =
|
|
32
|
+
| "not_a_repository"
|
|
33
|
+
| "identity_mismatch"
|
|
34
|
+
| "path_outside_root"
|
|
35
|
+
| "invalid_binding";
|
|
36
|
+
|
|
37
|
+
export class RepositoryBindingError extends Error {
|
|
38
|
+
readonly code: RepositoryBindingErrorCode;
|
|
39
|
+
|
|
40
|
+
constructor(code: RepositoryBindingErrorCode, message: string) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "RepositoryBindingError";
|
|
43
|
+
this.code = code;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function realpathOrResolve(target: string): Promise<string> {
|
|
48
|
+
try {
|
|
49
|
+
return await fs.realpath(target);
|
|
50
|
+
} catch {
|
|
51
|
+
return path.resolve(target);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Sync realpath for path-under-root checks (handles macOS /var → /private/var). */
|
|
56
|
+
function realpathSyncOrResolve(target: string): string {
|
|
57
|
+
try {
|
|
58
|
+
return fssync.realpathSync(target);
|
|
59
|
+
} catch {
|
|
60
|
+
return path.resolve(target);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
65
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
69
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Capture a durable binding from the active cwd/worktree. */
|
|
73
|
+
export async function captureRepositoryBinding(
|
|
74
|
+
cwd: string,
|
|
75
|
+
options: { relativeSubdir?: string; displayPath?: string } = {},
|
|
76
|
+
): Promise<RepositoryBinding> {
|
|
77
|
+
const resolvedCwd = await realpathOrResolve(cwd);
|
|
78
|
+
const repository = await repo.resolve(resolvedCwd);
|
|
79
|
+
const worktreeRoot = repository ? await realpathOrResolve(repository.repoRoot) : resolvedCwd;
|
|
80
|
+
const commonDir = repository ? await realpathOrResolve(repository.commonDir) : null;
|
|
81
|
+
|
|
82
|
+
let headSha: string | undefined;
|
|
83
|
+
let branch: string | undefined;
|
|
84
|
+
if (repository) {
|
|
85
|
+
const headState = await head.resolve(resolvedCwd);
|
|
86
|
+
if (headState) {
|
|
87
|
+
headSha = headState.commit || undefined;
|
|
88
|
+
branch = headState.kind === "ref" ? headState.branchName || undefined : undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const binding: RepositoryBinding = {
|
|
93
|
+
schema: REPOSITORY_BINDING_SCHEMA,
|
|
94
|
+
worktreeRoot,
|
|
95
|
+
commonDir,
|
|
96
|
+
...(options.relativeSubdir ? { relativeSubdir: normalizeRelativeSubdir(options.relativeSubdir) } : {}),
|
|
97
|
+
...(options.displayPath ? { displayPath: options.displayPath } : {}),
|
|
98
|
+
...(headSha ? { head: headSha } : {}),
|
|
99
|
+
...(branch ? { branch } : {}),
|
|
100
|
+
};
|
|
101
|
+
return binding;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeRelativeSubdir(relative: string): string {
|
|
105
|
+
const normalized = relative
|
|
106
|
+
.replaceAll("\\", "/")
|
|
107
|
+
.replace(/^\.\/+/u, "")
|
|
108
|
+
.replace(/\/+$/u, "");
|
|
109
|
+
if (normalized === "" || normalized === ".") {
|
|
110
|
+
throw new RepositoryBindingError("invalid_binding", "relativeSubdir must be a non-empty relative path");
|
|
111
|
+
}
|
|
112
|
+
if (path.isAbsolute(normalized) || normalized.split("/").includes("..")) {
|
|
113
|
+
throw new RepositoryBindingError("invalid_binding", "relativeSubdir must be repo-relative without '..' segments");
|
|
114
|
+
}
|
|
115
|
+
return normalized;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Parse a binding from JSON/plan payload; fail closed on malformed shapes. */
|
|
119
|
+
export function parseRepositoryBinding(value: unknown): RepositoryBinding {
|
|
120
|
+
if (!isObject(value)) {
|
|
121
|
+
throw new RepositoryBindingError("invalid_binding", "repository binding must be an object");
|
|
122
|
+
}
|
|
123
|
+
const schema = nonEmptyString(value.schema);
|
|
124
|
+
if (schema !== REPOSITORY_BINDING_SCHEMA) {
|
|
125
|
+
throw new RepositoryBindingError(
|
|
126
|
+
"invalid_binding",
|
|
127
|
+
`repository binding schema must be ${REPOSITORY_BINDING_SCHEMA}`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const worktreeRoot = nonEmptyString(value.worktreeRoot ?? value.worktree_root);
|
|
131
|
+
if (!worktreeRoot) {
|
|
132
|
+
throw new RepositoryBindingError("invalid_binding", "repository binding requires worktreeRoot");
|
|
133
|
+
}
|
|
134
|
+
const commonDirRaw = value.commonDir ?? value.common_dir;
|
|
135
|
+
const commonDir =
|
|
136
|
+
commonDirRaw === null || commonDirRaw === undefined
|
|
137
|
+
? null
|
|
138
|
+
: (nonEmptyString(commonDirRaw) ??
|
|
139
|
+
(() => {
|
|
140
|
+
throw new RepositoryBindingError("invalid_binding", "commonDir must be a string or null");
|
|
141
|
+
})());
|
|
142
|
+
const relativeSubdirRaw = value.relativeSubdir ?? value.relative_subdir;
|
|
143
|
+
const relativeSubdir =
|
|
144
|
+
relativeSubdirRaw === undefined ? undefined : normalizeRelativeSubdir(String(relativeSubdirRaw));
|
|
145
|
+
const displayPath = nonEmptyString(value.displayPath ?? value.display_path);
|
|
146
|
+
const head = nonEmptyString(value.head);
|
|
147
|
+
const branch = nonEmptyString(value.branch);
|
|
148
|
+
return {
|
|
149
|
+
schema: REPOSITORY_BINDING_SCHEMA,
|
|
150
|
+
worktreeRoot: path.resolve(worktreeRoot),
|
|
151
|
+
commonDir: commonDir === null ? null : path.resolve(commonDir),
|
|
152
|
+
...(relativeSubdir ? { relativeSubdir } : {}),
|
|
153
|
+
...(displayPath ? { displayPath } : {}),
|
|
154
|
+
...(head ? { head } : {}),
|
|
155
|
+
...(branch ? { branch } : {}),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** True when two bindings refer to the same repository identity (linked worktrees ok). */
|
|
160
|
+
export function repositoryBindingsMatch(left: RepositoryBinding, right: RepositoryBinding): boolean {
|
|
161
|
+
if (left.commonDir && right.commonDir) {
|
|
162
|
+
return path.resolve(left.commonDir) === path.resolve(right.commonDir);
|
|
163
|
+
}
|
|
164
|
+
// Non-git workspaces: require exact worktree root match.
|
|
165
|
+
return path.resolve(left.worktreeRoot) === path.resolve(right.worktreeRoot);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Ensure `cwd` is inside the bound repository (or the same linked worktree family).
|
|
170
|
+
* Fails closed on sibling-repo drift.
|
|
171
|
+
*/
|
|
172
|
+
export async function assertCwdMatchesRepositoryBinding(
|
|
173
|
+
cwd: string,
|
|
174
|
+
binding: RepositoryBinding,
|
|
175
|
+
): Promise<RepositoryBinding> {
|
|
176
|
+
const active = await captureRepositoryBinding(cwd);
|
|
177
|
+
if (!repositoryBindingsMatch(active, binding)) {
|
|
178
|
+
throw new RepositoryBindingError(
|
|
179
|
+
"identity_mismatch",
|
|
180
|
+
`Active worktree does not match plan/task repository binding. active=${active.worktreeRoot} (commonDir=${active.commonDir ?? "none"}) bound=${binding.worktreeRoot} (commonDir=${binding.commonDir ?? "none"}).`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return active;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Ensure a declared target path resolves under the bound worktree root. */
|
|
187
|
+
export function assertPathUnderRepositoryBinding(binding: RepositoryBinding, targetPath: string): string {
|
|
188
|
+
const root = realpathSyncOrResolve(binding.worktreeRoot);
|
|
189
|
+
const base = binding.relativeSubdir ? realpathSyncOrResolve(path.resolve(root, binding.relativeSubdir)) : root;
|
|
190
|
+
const candidate = path.isAbsolute(targetPath) ? path.resolve(targetPath) : path.resolve(base, targetPath);
|
|
191
|
+
// Prefer realpath when the path exists so macOS /var ↔ /private/var aliases match.
|
|
192
|
+
const resolved = realpathSyncOrResolve(candidate);
|
|
193
|
+
const relative = path.relative(root, resolved);
|
|
194
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
195
|
+
throw new RepositoryBindingError(
|
|
196
|
+
"path_outside_root",
|
|
197
|
+
`Path escapes bound repository root: ${targetPath} (root=${root})`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return resolved;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Public identity snapshot for receipts/handoffs (no display-only fields required).
|
|
205
|
+
* Always includes the schema + durable roots so downstream lanes can re-verify.
|
|
206
|
+
*/
|
|
207
|
+
export function publicRepositoryBinding(binding: RepositoryBinding): RepositoryBinding {
|
|
208
|
+
return {
|
|
209
|
+
schema: REPOSITORY_BINDING_SCHEMA,
|
|
210
|
+
worktreeRoot: path.resolve(binding.worktreeRoot),
|
|
211
|
+
commonDir: binding.commonDir === null ? null : path.resolve(binding.commonDir),
|
|
212
|
+
...(binding.relativeSubdir ? { relativeSubdir: binding.relativeSubdir } : {}),
|
|
213
|
+
...(binding.displayPath ? { displayPath: binding.displayPath } : {}),
|
|
214
|
+
...(binding.head ? { head: binding.head } : {}),
|
|
215
|
+
...(binding.branch ? { branch: binding.branch } : {}),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Resolve the authoritative binding for a delegated task before discovery/spawn.
|
|
221
|
+
*
|
|
222
|
+
* - Missing declaration → stamp from session cwd (never leave authority implicit).
|
|
223
|
+
* - Declared binding → parse + fail closed unless it matches the active session worktree.
|
|
224
|
+
* - relativeSubdir (when present) must resolve under the bound root.
|
|
225
|
+
*/
|
|
226
|
+
export async function resolveTaskRepositoryBinding(
|
|
227
|
+
sessionCwd: string,
|
|
228
|
+
declared: unknown | undefined,
|
|
229
|
+
): Promise<RepositoryBinding> {
|
|
230
|
+
const sessionBinding = await captureRepositoryBinding(sessionCwd, { displayPath: sessionCwd });
|
|
231
|
+
if (declared === undefined || declared === null) {
|
|
232
|
+
return publicRepositoryBinding(sessionBinding);
|
|
233
|
+
}
|
|
234
|
+
const taskBinding = parseRepositoryBinding(declared);
|
|
235
|
+
await assertCwdMatchesRepositoryBinding(sessionCwd, taskBinding);
|
|
236
|
+
if (taskBinding.relativeSubdir) {
|
|
237
|
+
assertPathUnderRepositoryBinding(taskBinding, ".");
|
|
238
|
+
}
|
|
239
|
+
return publicRepositoryBinding(taskBinding);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Ensure an execution/isolation root (cwd or worktree) still matches the bound identity.
|
|
244
|
+
* Used after isolation workspace creation so linked worktrees keep the source repository.
|
|
245
|
+
*/
|
|
246
|
+
export async function assertExecutionRootMatchesRepositoryBinding(
|
|
247
|
+
executionRoot: string,
|
|
248
|
+
binding: RepositoryBinding,
|
|
249
|
+
): Promise<RepositoryBinding> {
|
|
250
|
+
return await assertCwdMatchesRepositoryBinding(executionRoot, binding);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Optional helper for tests and diagnostics. */
|
|
254
|
+
export function bindingFromGitRepository(
|
|
255
|
+
repository: GitRepository,
|
|
256
|
+
options: { relativeSubdir?: string; displayPath?: string; head?: string; branch?: string } = {},
|
|
257
|
+
): RepositoryBinding {
|
|
258
|
+
return {
|
|
259
|
+
schema: REPOSITORY_BINDING_SCHEMA,
|
|
260
|
+
worktreeRoot: path.resolve(repository.repoRoot),
|
|
261
|
+
commonDir: path.resolve(repository.commonDir),
|
|
262
|
+
...(options.relativeSubdir ? { relativeSubdir: normalizeRelativeSubdir(options.relativeSubdir) } : {}),
|
|
263
|
+
...(options.displayPath ? { displayPath: options.displayPath } : {}),
|
|
264
|
+
...(options.head ? { head: options.head } : {}),
|
|
265
|
+
...(options.branch ? { branch: options.branch } : {}),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
@@ -911,15 +911,11 @@ async function operatorDispatchIdForOwner(
|
|
|
911
911
|
}
|
|
912
912
|
}
|
|
913
913
|
|
|
914
|
-
async function
|
|
914
|
+
async function observeOwnerTerminalPostmortem(
|
|
915
915
|
reason: postmortem.Reason,
|
|
916
|
-
|
|
917
|
-
stateFile: string,
|
|
916
|
+
owner: OwnerTerminalContext,
|
|
918
917
|
sessionId: string,
|
|
919
|
-
|
|
920
|
-
): Promise<void> {
|
|
921
|
-
const owner = context.ownerTerminal;
|
|
922
|
-
if (!owner) return;
|
|
918
|
+
): Promise<OwnerVerdict | null> {
|
|
923
919
|
try {
|
|
924
920
|
const now = new Date().toISOString();
|
|
925
921
|
const observation: Omit<ObserveTerminalRequest, "operator_dispatch_id"> = {
|
|
@@ -937,10 +933,27 @@ async function persistCoordinatorRuntimeStateFromOwnerTerminalPostmortem(
|
|
|
937
933
|
reason: "process_postmortem",
|
|
938
934
|
};
|
|
939
935
|
const operatorDispatchId = await operatorDispatchIdForOwner(owner, observation);
|
|
940
|
-
|
|
936
|
+
return await observeOwnerTerminal({
|
|
941
937
|
...observation,
|
|
942
938
|
...(operatorDispatchId ? { operator_dispatch_id: operatorDispatchId } : {}),
|
|
943
939
|
});
|
|
940
|
+
} catch {
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
async function persistCoordinatorRuntimeStateFromOwnerTerminalPostmortem(
|
|
946
|
+
context: RuntimeStateContext,
|
|
947
|
+
stateFile: string,
|
|
948
|
+
sessionId: string,
|
|
949
|
+
previous: Record<string, unknown>,
|
|
950
|
+
verdict: OwnerVerdict | null,
|
|
951
|
+
): Promise<void> {
|
|
952
|
+
const owner = context.ownerTerminal;
|
|
953
|
+
if (!owner) return;
|
|
954
|
+
try {
|
|
955
|
+
if (!verdict) throw new Error("owner terminal verdict unavailable");
|
|
956
|
+
const now = new Date().toISOString();
|
|
944
957
|
const expected = verdict.classification === "expected_operator_shutdown";
|
|
945
958
|
const state: RuntimeState = expected ? "completed" : "errored";
|
|
946
959
|
const payload = {
|
|
@@ -1009,6 +1022,9 @@ export async function persistCoordinatorRuntimeStateFromPostmortem(
|
|
|
1009
1022
|
const stateFile = runtimeStateFileForContext(context);
|
|
1010
1023
|
if (!stateFile) return;
|
|
1011
1024
|
const identity = normalizedIdentity(context);
|
|
1025
|
+
const ownerTerminalVerdict = context.ownerTerminal
|
|
1026
|
+
? await observeOwnerTerminalPostmortem(reason, context.ownerTerminal, identity.sessionId)
|
|
1027
|
+
: null;
|
|
1012
1028
|
await serializeStateFileWrite(
|
|
1013
1029
|
stateFile,
|
|
1014
1030
|
async () =>
|
|
@@ -1033,11 +1049,11 @@ export async function persistCoordinatorRuntimeStateFromPostmortem(
|
|
|
1033
1049
|
}
|
|
1034
1050
|
if (context.ownerTerminal) {
|
|
1035
1051
|
await persistCoordinatorRuntimeStateFromOwnerTerminalPostmortem(
|
|
1036
|
-
reason,
|
|
1037
1052
|
context,
|
|
1038
1053
|
stateFile,
|
|
1039
1054
|
identity.sessionId,
|
|
1040
1055
|
previous,
|
|
1056
|
+
ownerTerminalVerdict,
|
|
1041
1057
|
);
|
|
1042
1058
|
return;
|
|
1043
1059
|
}
|
|
@@ -1646,7 +1646,8 @@ export function isValidOwnerIntent(intent: unknown, request?: ObserveTerminalReq
|
|
|
1646
1646
|
intent.generation === request.owner_generation &&
|
|
1647
1647
|
intent.session_id === request.session_id &&
|
|
1648
1648
|
intent.server_key === request.socket_key &&
|
|
1649
|
-
|
|
1649
|
+
request.operator_dispatch_id !== undefined &&
|
|
1650
|
+
intent.dispatch_id === request.operator_dispatch_id &&
|
|
1650
1651
|
request.signal === "SIGTERM"
|
|
1651
1652
|
);
|
|
1652
1653
|
}
|
|
@@ -3,6 +3,7 @@ import * as crypto from "node:crypto";
|
|
|
3
3
|
import * as fsSync from "node:fs";
|
|
4
4
|
import * as fs from "node:fs/promises";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
+
import { Process } from "@gajae-code/natives";
|
|
6
7
|
import { readLinuxProcStartTime, readLinuxProcStartTimeSync } from "./linux-proc";
|
|
7
8
|
import { resolveGjcTmuxBinary } from "./psmux-detect";
|
|
8
9
|
import { tmuxRuntimeSessionPath } from "./session-layout";
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
lifecyclePaths,
|
|
44
45
|
type OwnerIsolationProbeSync,
|
|
45
46
|
type OwnerVerdict,
|
|
47
|
+
observeOwnerTerminal,
|
|
46
48
|
type PlanResponse,
|
|
47
49
|
planTmuxOwnerIsolationSync,
|
|
48
50
|
replaceOwnerGenerationSync,
|
|
@@ -963,6 +965,14 @@ async function readProcessStartTime(pid: number): Promise<string | null> {
|
|
|
963
965
|
return readLinuxProcStartTime(pid);
|
|
964
966
|
}
|
|
965
967
|
|
|
968
|
+
function exactManagedOwnerSupervisor(supervisorPid: number, supervisorStartTime: string): Process {
|
|
969
|
+
const supervisor = Process.fromPid(supervisorPid);
|
|
970
|
+
if (!supervisor) throw new Error("managed_owner_supervisor_unverifiable");
|
|
971
|
+
if (process.platform === "linux" && supervisor.incarnation !== `linux:${supervisorStartTime}`)
|
|
972
|
+
throw new Error("managed_owner_supervisor_incarnation_mismatch");
|
|
973
|
+
return supervisor;
|
|
974
|
+
}
|
|
975
|
+
|
|
966
976
|
async function readCurrentGeneration(stateDir: string, sessionId: string): Promise<string | null> {
|
|
967
977
|
try {
|
|
968
978
|
const value: unknown = JSON.parse(
|
|
@@ -1016,12 +1026,12 @@ async function requireUnchangedOwnerForCompatibilityCleanup(
|
|
|
1016
1026
|
initialServer: { pid: number; startTime: string },
|
|
1017
1027
|
listPanePids: (sessionName: string, env: NodeJS.ProcessEnv) => number[],
|
|
1018
1028
|
readStartTime: (pid: number) => Promise<string | null>,
|
|
1019
|
-
): Promise<
|
|
1029
|
+
): Promise<boolean> {
|
|
1020
1030
|
try {
|
|
1021
1031
|
const currentNativeSessionId = readNativeTmuxSessionId(nativeSessionId, env);
|
|
1022
1032
|
if (!currentNativeSessionId) {
|
|
1023
1033
|
if (readNativeTmuxSessionId(sessionName, env)) throw new Error(`gjc_tmux_owner_changed:${sessionName}`);
|
|
1024
|
-
return;
|
|
1034
|
+
return false;
|
|
1025
1035
|
}
|
|
1026
1036
|
const currentServer = requireSafeTmuxServerForMutation(resolveGjcTmuxCommand(env), env);
|
|
1027
1037
|
const panePids = listPanePids(nativeSessionId, env);
|
|
@@ -1054,10 +1064,11 @@ async function requireUnchangedOwnerForCompatibilityCleanup(
|
|
|
1054
1064
|
: null,
|
|
1055
1065
|
].filter((value): value is string => value !== null);
|
|
1056
1066
|
if (mismatches.length > 0) throw new Error(`gjc_tmux_owner_changed:${sessionName}:${mismatches.join(",")}`);
|
|
1067
|
+
return true;
|
|
1057
1068
|
} catch (error) {
|
|
1058
1069
|
if (!readNativeTmuxSessionId(nativeSessionId, env)) {
|
|
1059
1070
|
if (readNativeTmuxSessionId(sessionName, env)) throw new Error(`gjc_tmux_owner_changed:${sessionName}`);
|
|
1060
|
-
return;
|
|
1071
|
+
return false;
|
|
1061
1072
|
}
|
|
1062
1073
|
if (error instanceof Error && error.message.startsWith(`gjc_tmux_owner_changed:${sessionName}`))
|
|
1063
1074
|
throw new Error(`gjc_tmux_owner_changed:${sessionName}`);
|
|
@@ -1152,6 +1163,7 @@ export async function forceCloseGjcTmuxSession(
|
|
|
1152
1163
|
const now = deps.now ?? (() => new Date());
|
|
1153
1164
|
const sleep = deps.sleep ?? (ms => Bun.sleep(ms));
|
|
1154
1165
|
const dispatchId = crypto.randomUUID();
|
|
1166
|
+
let operatorVerdict: Promise<OwnerVerdict> | null = null;
|
|
1155
1167
|
await closeExactTmuxOwner(
|
|
1156
1168
|
{
|
|
1157
1169
|
stateDir: identity.stateDir,
|
|
@@ -1169,12 +1181,37 @@ export async function forceCloseGjcTmuxSession(
|
|
|
1169
1181
|
sendSigterm: async pid => {
|
|
1170
1182
|
if ((await (deps.readProcessStartTime ?? readProcessStartTime)(pid)) !== identity.startTime)
|
|
1171
1183
|
throw new Error("owner_pid_identity_mismatch");
|
|
1172
|
-
(deps.signalTerm
|
|
1184
|
+
if (deps.signalTerm) {
|
|
1185
|
+
deps.signalTerm(pid);
|
|
1186
|
+
} else {
|
|
1187
|
+
const supervisor = exactManagedOwnerSupervisor(pid, identity.startTime);
|
|
1188
|
+
if (!supervisor.signalRoot(15)) throw new Error("managed_owner_supervisor_signal_failed");
|
|
1189
|
+
operatorVerdict = supervisor
|
|
1190
|
+
.waitForExit({ timeoutMs: FORCE_CLOSE_VERDICT_TIMEOUT_MS - 500 })
|
|
1191
|
+
.then(async exited => {
|
|
1192
|
+
if (!exited) throw new Error("managed_owner_supervisor_exit_timeout");
|
|
1193
|
+
return await observeOwnerTerminal({
|
|
1194
|
+
schema_version: 1,
|
|
1195
|
+
op: "observe_terminal",
|
|
1196
|
+
session_id: identity.sessionId,
|
|
1197
|
+
owner_generation: identity.generation,
|
|
1198
|
+
state_dir: identity.stateDir,
|
|
1199
|
+
socket_key: identity.socketKey,
|
|
1200
|
+
observer: "raw_monitor",
|
|
1201
|
+
observed_at: now().toISOString(),
|
|
1202
|
+
signal: "SIGTERM",
|
|
1203
|
+
exit_code: null,
|
|
1204
|
+
exit_kind: "exact_owner_exit_observed",
|
|
1205
|
+
reason: "operator_observed_owner_exit",
|
|
1206
|
+
operator_dispatch_id: dispatchId,
|
|
1207
|
+
});
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1173
1210
|
},
|
|
1174
1211
|
|
|
1175
|
-
waitForVerdict: () => waitForExpectedVerdict(identity, sleep, now),
|
|
1212
|
+
waitForVerdict: () => operatorVerdict ?? waitForExpectedVerdict(identity, sleep, now),
|
|
1176
1213
|
cleanupSession: async () => {
|
|
1177
|
-
await requireUnchangedOwnerForCompatibilityCleanup(
|
|
1214
|
+
const cleanupRequired = await requireUnchangedOwnerForCompatibilityCleanup(
|
|
1178
1215
|
session.name,
|
|
1179
1216
|
nativeSessionId,
|
|
1180
1217
|
env,
|
|
@@ -1184,6 +1221,7 @@ export async function forceCloseGjcTmuxSession(
|
|
|
1184
1221
|
deps.listPanePids ?? readExactSessionPanePids,
|
|
1185
1222
|
deps.readProcessStartTime ?? readProcessStartTime,
|
|
1186
1223
|
);
|
|
1224
|
+
if (!cleanupRequired) return;
|
|
1187
1225
|
if (deps.cleanupSession) deps.cleanupSession(nativeSessionId, env);
|
|
1188
1226
|
else
|
|
1189
1227
|
runGuardedTmuxSessionCommand(
|
|
@@ -1192,6 +1230,7 @@ export async function forceCloseGjcTmuxSession(
|
|
|
1192
1230
|
initialServer.pid,
|
|
1193
1231
|
env,
|
|
1194
1232
|
`kill-session -t '${nativeSessionId}'`,
|
|
1233
|
+
identity.generation,
|
|
1195
1234
|
);
|
|
1196
1235
|
},
|
|
1197
1236
|
},
|
|
@@ -2,6 +2,10 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
|
|
3
3
|
import { resolveGjcSessionForRead, SessionResolutionError } from "./session-resolution";
|
|
4
4
|
import {
|
|
5
|
+
findCleanPauseCriticVerdict,
|
|
6
|
+
findLedgerReceiptEvent,
|
|
7
|
+
terminalCriticCeilingReached,
|
|
8
|
+
terminalCriticGateOverridden,
|
|
5
9
|
validateDeferredMemberReceiptFresh,
|
|
6
10
|
validateReceiptFreshBase,
|
|
7
11
|
validateSupersededFinalAggregateReceipt,
|
|
@@ -32,6 +36,7 @@ export type UltragoalGuardState =
|
|
|
32
36
|
| "active_dirty_quality_gate"
|
|
33
37
|
| "active_review_blocked_unrecorded"
|
|
34
38
|
| "active_review_blocked_recorded"
|
|
39
|
+
| "active_missing_critic_verdict"
|
|
35
40
|
| "unreadable_fail_closed";
|
|
36
41
|
|
|
37
42
|
export interface UltragoalGuardDiagnostic {
|
|
@@ -71,12 +76,12 @@ function objectiveMatches(currentGoal: CurrentGoalLike, plan: UltragoalPlan, ses
|
|
|
71
76
|
return plan.goals.some(goal => goal.objective === normalized);
|
|
72
77
|
}
|
|
73
78
|
|
|
74
|
-
function isKnownUltragoalObjective(currentObjective: string): boolean {
|
|
79
|
+
export function isKnownUltragoalObjective(currentObjective: string): boolean {
|
|
75
80
|
const normalized = currentObjective.trim();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
// Exact default aggregate objective only. Substring path sniffing previously
|
|
82
|
+
// treated arbitrary objectives that merely mentioned goals.json/ledger paths
|
|
83
|
+
// as Ultragoal-owned, which could mis-arm guards. Prefer fail-closed.
|
|
84
|
+
return normalized === DEFAULT_ULTRAGOAL_OBJECTIVE;
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
async function ultragoalReadPaths(
|
|
@@ -256,6 +261,31 @@ export function validateCompletionReceipt(input: {
|
|
|
256
261
|
goalId: input.goal.id,
|
|
257
262
|
};
|
|
258
263
|
}
|
|
264
|
+
if (input.receiptKind === "final-aggregate") {
|
|
265
|
+
const checkpointEvent = findLedgerReceiptEvent(input.ledger, receipt);
|
|
266
|
+
if (checkpointEvent) {
|
|
267
|
+
const qualityGate =
|
|
268
|
+
typeof checkpointEvent.qualityGateJson === "object" &&
|
|
269
|
+
checkpointEvent.qualityGateJson !== null &&
|
|
270
|
+
!Array.isArray(checkpointEvent.qualityGateJson)
|
|
271
|
+
? (checkpointEvent.qualityGateJson as Record<string, unknown>)
|
|
272
|
+
: undefined;
|
|
273
|
+
const criticReview =
|
|
274
|
+
qualityGate &&
|
|
275
|
+
typeof qualityGate.criticReview === "object" &&
|
|
276
|
+
qualityGate.criticReview !== null &&
|
|
277
|
+
!Array.isArray(qualityGate.criticReview)
|
|
278
|
+
? (qualityGate.criticReview as Record<string, unknown>)
|
|
279
|
+
: undefined;
|
|
280
|
+
if (criticReview?.verdict !== "OKAY") {
|
|
281
|
+
return {
|
|
282
|
+
state: "active_missing_critic_verdict",
|
|
283
|
+
message: `Ultragoal ${input.goal.id} final aggregate receipt checkpoint requires criticReview with verdict OKAY.`,
|
|
284
|
+
goalId: input.goal.id,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
259
289
|
if (receipt.validationBatch?.role === "deferred-member") {
|
|
260
290
|
return validateDeferredMemberReceiptFresh({
|
|
261
291
|
plan: input.plan,
|
|
@@ -274,6 +304,15 @@ export function validateCompletionReceipt(input: {
|
|
|
274
304
|
receiptKind: input.receiptKind,
|
|
275
305
|
});
|
|
276
306
|
if (baseDiagnostic) return baseDiagnostic;
|
|
307
|
+
if (input.receiptKind === "final-aggregate") {
|
|
308
|
+
if (terminalCriticCeilingReached(input.ledger) && !terminalCriticGateOverridden(input.ledger)) {
|
|
309
|
+
return {
|
|
310
|
+
state: "active_stale_receipt",
|
|
311
|
+
message: `Ultragoal ${input.goal.id} final aggregate receipt is stale because the terminal-critic ceiling is currently reached.`,
|
|
312
|
+
goalId: input.goal.id,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}
|
|
277
316
|
if (receipt.validationBatch?.role === "batch-close") {
|
|
278
317
|
for (const memberId of receipt.validationBatch.memberIds) {
|
|
279
318
|
const member = input.plan.goals.find(goal => goal.id === memberId);
|
|
@@ -856,10 +895,10 @@ export interface UltragoalPauseBlockDiagnostic {
|
|
|
856
895
|
|
|
857
896
|
/**
|
|
858
897
|
* While an Ultragoal run is active, `goal({"op":"pause"})` is only allowed when the
|
|
859
|
-
* current durable Ultragoal state is readable
|
|
860
|
-
*
|
|
861
|
-
*
|
|
862
|
-
* blocks pause rather than silently allowing a give-up.
|
|
898
|
+
* current durable Ultragoal state is readable, the latest `blocker_classified`
|
|
899
|
+
* event is `human_blocked`, and a later fresh clean pause terminal critic verdict is bound to
|
|
900
|
+
* that exact classification. Reads fail closed so unreadable durable state or
|
|
901
|
+
* ledger data blocks pause rather than silently allowing a give-up.
|
|
863
902
|
*/
|
|
864
903
|
export async function isUltragoalPauseBlocked(cwd: string): Promise<UltragoalPauseBlockDiagnostic> {
|
|
865
904
|
if (!cwd) return { blocked: false, reason: "No cwd to resolve durable Ultragoal state." };
|
|
@@ -880,14 +919,53 @@ export async function isUltragoalPauseBlocked(cwd: string): Promise<UltragoalPau
|
|
|
880
919
|
reason: `Unable to read durable Ultragoal ledger: ${error instanceof Error ? error.message : String(error)}`,
|
|
881
920
|
};
|
|
882
921
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
922
|
+
if (terminalCriticCeilingReached(ledger) && !terminalCriticGateOverridden(ledger)) {
|
|
923
|
+
return {
|
|
924
|
+
blocked: true,
|
|
925
|
+
reason:
|
|
926
|
+
"The Ultragoal run hit the terminal-critic ceiling; requires human/leader `gjc ultragoal record-critic-gate-override` before further terminal attempts.",
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const classification = [...ledger].reverse().find(event => event.event === "blocker_classified");
|
|
931
|
+
if (classification?.classification !== "human_blocked") {
|
|
932
|
+
return {
|
|
933
|
+
blocked: true,
|
|
934
|
+
reason:
|
|
935
|
+
"An Ultragoal run is active. Pausing requires the latest blocker_classified event to be human_blocked, followed by a bound clean pause terminal critic verdict.",
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
if (typeof classification.eventId !== "string" || !classification.eventId.trim()) {
|
|
939
|
+
return {
|
|
940
|
+
blocked: true,
|
|
941
|
+
reason:
|
|
942
|
+
"Pausing requires a later fresh clean pause terminal critic OKAY verdict bound to the latest human_blocked blocker_classified event; a REJECT/ITERATE/stale/missing verdict blocks the pause and the run must keep executing.",
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
let plan: UltragoalPlan | null;
|
|
946
|
+
try {
|
|
947
|
+
plan = await readUltragoalPlan(cwd);
|
|
948
|
+
} catch (error) {
|
|
949
|
+
return {
|
|
950
|
+
blocked: true,
|
|
951
|
+
reason: `Unable to read durable Ultragoal plan for pause critic verdict: ${error instanceof Error ? error.message : String(error)}`,
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
if (!plan) {
|
|
955
|
+
return { blocked: true, reason: "Unable to read durable Ultragoal plan for pause critic verdict." };
|
|
956
|
+
}
|
|
957
|
+
const criticVerdict = findCleanPauseCriticVerdict(plan, ledger, classification.eventId);
|
|
958
|
+
if (!criticVerdict) {
|
|
959
|
+
return {
|
|
960
|
+
blocked: true,
|
|
961
|
+
reason:
|
|
962
|
+
"Pausing requires a later fresh clean pause terminal critic OKAY verdict bound to the latest human_blocked blocker_classified event; a REJECT/ITERATE/stale/missing verdict blocks the pause and the run must keep executing.",
|
|
963
|
+
};
|
|
886
964
|
}
|
|
887
965
|
return {
|
|
888
|
-
blocked:
|
|
966
|
+
blocked: false,
|
|
889
967
|
reason:
|
|
890
|
-
"
|
|
968
|
+
"Latest blocker_classified event is human_blocked with a later fresh clean bound pause terminal critic verdict.",
|
|
891
969
|
};
|
|
892
970
|
}
|
|
893
971
|
|
|
@@ -902,7 +980,7 @@ export async function assertUltragoalPauseAllowed(cwd: string): Promise<void> {
|
|
|
902
980
|
[
|
|
903
981
|
diagnostic.reason,
|
|
904
982
|
"Resolvable blockers must be worked, not paused: investigate, `gjc ultragoal steer --kind add_subgoal`, delegate an executor, or `gjc ultragoal record-review-blockers`.",
|
|
905
|
-
'If the blocker is genuinely human-only, record `gjc ultragoal classify-blocker --classification human_blocked --evidence "<human-only dependency>"`
|
|
983
|
+
'If the blocker is genuinely human-only, record `gjc ultragoal classify-blocker --classification human_blocked --evidence "<human-only dependency>"`, then record a clean bound `gjc ultragoal record-critic-verdict --terminus pause --classification-event-id <eventId> --verdict OKAY --evidence "<critic evidence>"` before pausing.',
|
|
906
984
|
].join("\n"),
|
|
907
985
|
);
|
|
908
986
|
}
|