oh-my-codex 0.18.0 → 0.18.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/Cargo.lock +6 -6
- package/Cargo.toml +1 -1
- package/README.md +43 -19
- package/crates/omx-api/src/lib.rs +66 -9
- package/crates/omx-sparkshell/src/exec.rs +125 -3
- package/crates/omx-sparkshell/src/main.rs +126 -36
- package/crates/omx-sparkshell/tests/execution.rs +225 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +15 -7
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +76 -3
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +49 -1
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/install-docs-contract.test.d.ts +2 -0
- package/dist/cli/__tests__/install-docs-contract.test.d.ts.map +1 -0
- package/dist/cli/__tests__/install-docs-contract.test.js +55 -0
- package/dist/cli/__tests__/install-docs-contract.test.js.map +1 -0
- package/dist/cli/__tests__/launch-fallback.test.js +115 -0
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/question.test.js +27 -41
- package/dist/cli/__tests__/question.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +94 -35
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +20 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-packaging.test.js +1 -0
- package/dist/cli/__tests__/sparkshell-packaging.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +227 -4
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +72 -1
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/codex-feature-probe.d.ts +5 -0
- package/dist/cli/codex-feature-probe.d.ts.map +1 -1
- package/dist/cli/codex-feature-probe.js +13 -7
- package/dist/cli/codex-feature-probe.js.map +1 -1
- package/dist/cli/doctor.d.ts +7 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +119 -10
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +3 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +345 -90
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/plugin-marketplace.d.ts +2 -0
- package/dist/cli/plugin-marketplace.d.ts.map +1 -1
- package/dist/cli/plugin-marketplace.js +15 -1
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +71 -11
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.d.ts +7 -1
- package/dist/cli/sparkshell.d.ts.map +1 -1
- package/dist/cli/sparkshell.js +13 -3
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/ultragoal.d.ts +1 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +184 -10
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/update.d.ts +2 -0
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +14 -3
- package/dist/cli/update.js.map +1 -1
- package/dist/compat/__tests__/doctor-contract.test.js +3 -0
- package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
- package/dist/config/__tests__/codex-feature-flags.test.js +11 -1
- package/dist/config/__tests__/codex-feature-flags.test.js.map +1 -1
- package/dist/config/__tests__/codex-hooks.test.js +19 -8
- package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
- package/dist/config/__tests__/commit-lore-guard.test.d.ts +2 -0
- package/dist/config/__tests__/commit-lore-guard.test.d.ts.map +1 -0
- package/dist/config/__tests__/commit-lore-guard.test.js +20 -0
- package/dist/config/__tests__/commit-lore-guard.test.js.map +1 -0
- package/dist/config/codex-feature-flags.d.ts +4 -0
- package/dist/config/codex-feature-flags.d.ts.map +1 -1
- package/dist/config/codex-feature-flags.js +4 -0
- package/dist/config/codex-feature-flags.js.map +1 -1
- package/dist/config/codex-hooks.js +6 -6
- package/dist/config/codex-hooks.js.map +1 -1
- package/dist/config/commit-lore-guard.d.ts +1 -0
- package/dist/config/commit-lore-guard.d.ts.map +1 -1
- package/dist/config/commit-lore-guard.js +29 -3
- package/dist/config/commit-lore-guard.js.map +1 -1
- package/dist/config/generator.d.ts +3 -1
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +24 -10
- package/dist/config/generator.js.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -0
- package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.js +5 -1
- package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +10 -6
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts +1 -1
- package/dist/hooks/__tests__/consensus-execution-handoff.test.js +13 -11
- package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js +4 -3
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +4 -3
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +33 -0
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js +26 -3
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
- package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
- package/dist/hooks/extensibility/dispatcher.js +29 -14
- package/dist/hooks/extensibility/dispatcher.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +8 -3
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
- package/dist/hooks/prompt-guidance-contract.js +3 -2
- package/dist/hooks/prompt-guidance-contract.js.map +1 -1
- package/dist/hud/__tests__/hud-tmux-injection.test.js +14 -8
- package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +2 -2
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/resource-leak-watch.test.d.ts +2 -0
- package/dist/hud/__tests__/resource-leak-watch.test.d.ts.map +1 -0
- package/dist/hud/__tests__/resource-leak-watch.test.js +28 -0
- package/dist/hud/__tests__/resource-leak-watch.test.js.map +1 -0
- package/dist/hud/index.d.ts +1 -1
- package/dist/hud/index.d.ts.map +1 -1
- package/dist/hud/index.js +10 -4
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/tmux.js +2 -2
- package/dist/hud/tmux.js.map +1 -1
- package/dist/notifications/__tests__/http-client-resource.test.d.ts +2 -0
- package/dist/notifications/__tests__/http-client-resource.test.d.ts.map +1 -0
- package/dist/notifications/__tests__/http-client-resource.test.js +41 -0
- package/dist/notifications/__tests__/http-client-resource.test.js.map +1 -0
- package/dist/notifications/__tests__/verbosity.test.js +20 -0
- package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
- package/dist/notifications/config.d.ts.map +1 -1
- package/dist/notifications/config.js +6 -3
- package/dist/notifications/config.js.map +1 -1
- package/dist/notifications/http-client.d.ts.map +1 -1
- package/dist/notifications/http-client.js +78 -27
- package/dist/notifications/http-client.js.map +1 -1
- package/dist/notifications/types.d.ts +2 -0
- package/dist/notifications/types.d.ts.map +1 -1
- package/dist/openclaw/__tests__/dispatcher.test.js +49 -1
- package/dist/openclaw/__tests__/dispatcher.test.js.map +1 -1
- package/dist/openclaw/dispatcher.d.ts +7 -4
- package/dist/openclaw/dispatcher.d.ts.map +1 -1
- package/dist/openclaw/dispatcher.js +32 -69
- package/dist/openclaw/dispatcher.js.map +1 -1
- package/dist/pipeline/__tests__/orchestrator.test.js +65 -3
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +50 -5
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/index.d.ts +8 -2
- package/dist/pipeline/index.d.ts.map +1 -1
- package/dist/pipeline/index.js +5 -2
- package/dist/pipeline/index.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts +5 -4
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +56 -15
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/stages/code-review.d.ts +2 -2
- package/dist/pipeline/stages/code-review.d.ts.map +1 -1
- package/dist/pipeline/stages/code-review.js +5 -3
- package/dist/pipeline/stages/code-review.js.map +1 -1
- package/dist/pipeline/stages/deep-interview.d.ts +15 -0
- package/dist/pipeline/stages/deep-interview.d.ts.map +1 -0
- package/dist/pipeline/stages/deep-interview.js +32 -0
- package/dist/pipeline/stages/deep-interview.js.map +1 -0
- package/dist/pipeline/stages/ralph-verify.d.ts +5 -5
- package/dist/pipeline/stages/ralph-verify.d.ts.map +1 -1
- package/dist/pipeline/stages/ralph-verify.js +2 -2
- package/dist/pipeline/stages/ralph-verify.js.map +1 -1
- package/dist/pipeline/stages/ultragoal.d.ts +19 -0
- package/dist/pipeline/stages/ultragoal.d.ts.map +1 -0
- package/dist/pipeline/stages/ultragoal.js +38 -0
- package/dist/pipeline/stages/ultragoal.js.map +1 -0
- package/dist/pipeline/stages/ultraqa.d.ts +30 -0
- package/dist/pipeline/stages/ultraqa.d.ts.map +1 -0
- package/dist/pipeline/stages/ultraqa.js +46 -0
- package/dist/pipeline/stages/ultraqa.js.map +1 -0
- package/dist/pipeline/types.d.ts +8 -6
- package/dist/pipeline/types.d.ts.map +1 -1
- package/dist/pipeline/types.js +2 -2
- package/dist/scripts/__tests__/codex-native-hook.test.js +705 -45
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +23 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/__tests__/verify-native-agents.test.js +16 -1
- package/dist/scripts/__tests__/verify-native-agents.test.js.map +1 -1
- package/dist/scripts/cleanup-explore-harness.js +1 -0
- package/dist/scripts/cleanup-explore-harness.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +158 -10
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +9 -1
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/notify-hook/process-runner.d.ts.map +1 -1
- package/dist/scripts/notify-hook/process-runner.js +39 -17
- package/dist/scripts/notify-hook/process-runner.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +9 -5
- package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +7 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +3 -0
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +99 -1
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +2 -2
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/verify-native-agents.js +2 -2
- package/dist/scripts/verify-native-agents.js.map +1 -1
- package/dist/sidecar/__tests__/resource-leak-watch.test.d.ts +2 -0
- package/dist/sidecar/__tests__/resource-leak-watch.test.d.ts.map +1 -0
- package/dist/sidecar/__tests__/resource-leak-watch.test.js +38 -0
- package/dist/sidecar/__tests__/resource-leak-watch.test.js.map +1 -0
- package/dist/sidecar/index.d.ts +1 -1
- package/dist/sidecar/index.d.ts.map +1 -1
- package/dist/sidecar/index.js +29 -12
- package/dist/sidecar/index.js.map +1 -1
- package/dist/state/__tests__/operations-ralph-phase.test.js +88 -1
- package/dist/state/__tests__/operations-ralph-phase.test.js.map +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +11 -0
- package/dist/state/operations.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +111 -3
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +39 -18
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +714 -10
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js +57 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/__tests__/steering-fixtures.d.ts +68 -0
- package/dist/ultragoal/__tests__/steering-fixtures.d.ts.map +1 -0
- package/dist/ultragoal/__tests__/steering-fixtures.js +259 -0
- package/dist/ultragoal/__tests__/steering-fixtures.js.map +1 -0
- package/dist/ultragoal/__tests__/steering-fixtures.test.d.ts +2 -0
- package/dist/ultragoal/__tests__/steering-fixtures.test.d.ts.map +1 -0
- package/dist/ultragoal/__tests__/steering-fixtures.test.js +65 -0
- package/dist/ultragoal/__tests__/steering-fixtures.test.js.map +1 -0
- package/dist/ultragoal/artifacts.d.ts +97 -2
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +811 -256
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/utils/__tests__/sleep-resource.test.d.ts +2 -0
- package/dist/utils/__tests__/sleep-resource.test.d.ts.map +1 -0
- package/dist/utils/__tests__/sleep-resource.test.js +39 -0
- package/dist/utils/__tests__/sleep-resource.test.js.map +1 -0
- package/dist/utils/sleep.d.ts.map +1 -1
- package/dist/utils/sleep.js +17 -6
- package/dist/utils/sleep.js.map +1 -1
- package/package.json +2 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +4 -3
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +56 -0
- package/plugins/oh-my-codex/hooks/hooks.json +77 -0
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +77 -47
- package/plugins/oh-my-codex/skills/cancel/SKILL.md +2 -2
- package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +8 -8
- package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/pipeline/SKILL.md +22 -11
- package/plugins/oh-my-codex/skills/plan/SKILL.md +8 -8
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +7 -0
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +4 -4
- package/plugins/oh-my-codex/skills/team/SKILL.md +1 -1
- package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +38 -4
- package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +1 -1
- package/prompts/planner.md +1 -1
- package/skills/autopilot/SKILL.md +77 -47
- package/skills/cancel/SKILL.md +2 -2
- package/skills/deep-interview/SKILL.md +8 -8
- package/skills/omx-setup/SKILL.md +1 -1
- package/skills/pipeline/SKILL.md +22 -11
- package/skills/plan/SKILL.md +8 -8
- package/skills/ralph/SKILL.md +7 -0
- package/skills/ralplan/SKILL.md +4 -4
- package/skills/team/SKILL.md +1 -1
- package/skills/ultragoal/SKILL.md +38 -4
- package/skills/ultrawork/SKILL.md +1 -1
- package/src/scripts/__tests__/codex-native-hook.test.ts +867 -81
- package/src/scripts/__tests__/smoke-packed-install.test.ts +31 -0
- package/src/scripts/__tests__/verify-native-agents.test.ts +21 -1
- package/src/scripts/cleanup-explore-harness.ts +1 -0
- package/src/scripts/codex-native-hook.ts +156 -10
- package/src/scripts/codex-native-pre-post.ts +16 -1
- package/src/scripts/notify-hook/process-runner.ts +40 -16
- package/src/scripts/notify-hook/team-dispatch.ts +9 -5
- package/src/scripts/notify-hook/team-tmux-guard.ts +7 -0
- package/src/scripts/smoke-packed-install.ts +105 -0
- package/src/scripts/sync-plugin-mirror.ts +3 -3
- package/src/scripts/verify-native-agents.ts +2 -2
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { appendFile, mkdir, open, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join, relative } from 'node:path';
|
|
4
4
|
import { formatCodexGoalReconciliation, parseCodexGoalSnapshot, reconcileCodexGoalSnapshot, } from '../goal-workflows/codex-goal-snapshot.js';
|
|
5
5
|
export const ULTRAGOAL_DIR = '.omx/ultragoal';
|
|
6
6
|
export const ULTRAGOAL_BRIEF = 'brief.md';
|
|
7
7
|
export const ULTRAGOAL_GOALS = 'goals.json';
|
|
8
8
|
export const ULTRAGOAL_LEDGER = 'ledger.jsonl';
|
|
9
|
+
const ULTRAGOAL_MUTATION_LOCK = '.mutation.lock';
|
|
10
|
+
export const ULTRAGOAL_STEERING_MUTATION_KINDS = [
|
|
11
|
+
'add_subgoal',
|
|
12
|
+
'split_subgoal',
|
|
13
|
+
'reorder_pending',
|
|
14
|
+
'revise_pending_wording',
|
|
15
|
+
'annotate_ledger',
|
|
16
|
+
'mark_blocked_superseded',
|
|
17
|
+
];
|
|
18
|
+
export const ULTRAGOAL_STEERING_SOURCES = [
|
|
19
|
+
'user_prompt_submit',
|
|
20
|
+
'finding',
|
|
21
|
+
'cli',
|
|
22
|
+
];
|
|
9
23
|
export class UltragoalError extends Error {
|
|
10
24
|
}
|
|
11
25
|
function iso(now = new Date()) {
|
|
@@ -32,6 +46,51 @@ function cleanLine(line) {
|
|
|
32
46
|
function normalizeObjective(value) {
|
|
33
47
|
return value.replace(/\s+/g, ' ').trim();
|
|
34
48
|
}
|
|
49
|
+
function normalizeBlockerEvidence(value) {
|
|
50
|
+
return (value ?? '')
|
|
51
|
+
.toLowerCase()
|
|
52
|
+
.replace(/https?:\/\/\S+/g, ' ')
|
|
53
|
+
.replace(/[`"'()[\]{}:,;]/g, ' ')
|
|
54
|
+
.replace(/\s+/g, ' ')
|
|
55
|
+
.trim();
|
|
56
|
+
}
|
|
57
|
+
function classifyExternalAuthorizationBlocker(evidence) {
|
|
58
|
+
const normalized = normalizeBlockerEvidence(evidence);
|
|
59
|
+
if (!normalized)
|
|
60
|
+
return null;
|
|
61
|
+
const mentionsAuthorization = /\b(auth|authorization|credential|credentials|token|permission|permissions|scope|scopes|access|unauthorized|forbidden|401|403)\b/.test(normalized);
|
|
62
|
+
const mentionsMissingAuthority = /\b(unset|missing|required|requires|without|omit|omits|not set|not available|no read packages|read packages)\b/.test(normalized);
|
|
63
|
+
if (!mentionsAuthorization || !mentionsMissingAuthority)
|
|
64
|
+
return null;
|
|
65
|
+
const mentionsGhcr = /\b(ghcr|github container registry|read packages|imagepullsecret|package api|anonymous image|container image)\b/.test(normalized);
|
|
66
|
+
if (mentionsGhcr) {
|
|
67
|
+
const has401 = /\b(401|unauthorized|anonymous pull|authentication required)\b/.test(normalized);
|
|
68
|
+
const has403 = /\b(403|forbidden|read packages|package api)\b/.test(normalized);
|
|
69
|
+
const status = [has401 ? 'HTTP_401_ANONYMOUS' : null, has403 ? 'HTTP_403_NO_READ_PACKAGES' : null]
|
|
70
|
+
.filter((part) => Boolean(part))
|
|
71
|
+
.join('+') || 'AUTHORIZATION_REQUIRED';
|
|
72
|
+
return {
|
|
73
|
+
signature: `GHCR_PULL_ACCESS:${status}:GHCR_VISIBILITY_OR_CREDENTIAL_REQUIRED`,
|
|
74
|
+
requiredDecision: 'make the GHCR package public, or provide/authorize a least-privilege read:packages credential and imagePullSecret/SOPS path',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
signature: 'EXTERNAL_AUTHORIZATION_REQUIRED',
|
|
79
|
+
requiredDecision: 'provide the missing external authorization/credential, or explicitly choose a different unblock path',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function sameBlockerOccurrences(entries, goalId, signature) {
|
|
83
|
+
return entries.filter((entry) => (entry.goalId === goalId
|
|
84
|
+
&& (entry.event === 'goal_failed' || entry.event === 'goal_needs_user_decision')
|
|
85
|
+
&& entry.blockerSignature === signature)).length;
|
|
86
|
+
}
|
|
87
|
+
function clearGoalBlockerFields(goal) {
|
|
88
|
+
goal.blockedReason = undefined;
|
|
89
|
+
goal.blockerSignature = undefined;
|
|
90
|
+
goal.blockerOccurrenceCount = undefined;
|
|
91
|
+
goal.requiredExternalDecision = undefined;
|
|
92
|
+
goal.nonRetriable = undefined;
|
|
93
|
+
}
|
|
35
94
|
function textMentionsUltragoalPlanArtifact(value) {
|
|
36
95
|
const normalized = (value ?? '').toLowerCase();
|
|
37
96
|
return normalized.includes(ULTRAGOAL_DIR.toLowerCase())
|
|
@@ -82,7 +141,7 @@ function buildCompletedLegacyGoalRemediation(goal) {
|
|
|
82
141
|
return [
|
|
83
142
|
'If get_goal returns a different completed legacy/thread objective, do not repeat --status complete in this thread.',
|
|
84
143
|
`Record a non-terminal blocker with: omx ultragoal checkpoint --goal-id ${goal.id} --status blocked --evidence "<completed legacy Codex goal blocks create_goal in this thread>" --codex-goal-json "<different completed get_goal JSON or path>".`,
|
|
85
|
-
'Then continue
|
|
144
|
+
'Then continue only from a Codex goal context with no active/completed conflicting goal, in the same repo/worktree, and create the intended goal there.',
|
|
86
145
|
].join(' ');
|
|
87
146
|
}
|
|
88
147
|
function codexGoalMode(plan) {
|
|
@@ -91,35 +150,78 @@ function codexGoalMode(plan) {
|
|
|
91
150
|
function isResolvedStatus(status) {
|
|
92
151
|
return status === 'complete' || status === 'review_blocked';
|
|
93
152
|
}
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (fallback.length <= 4000)
|
|
102
|
-
return fallback;
|
|
153
|
+
function isScheduleEligibleGoal(goal) {
|
|
154
|
+
return goal.steeringStatus !== 'superseded' && goal.steeringStatus !== 'blocked';
|
|
155
|
+
}
|
|
156
|
+
export const ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE = `Complete the durable ultragoal plan in ${ULTRAGOAL_DIR}/${ULTRAGOAL_GOALS}, including later accepted/appended stories, under the original brief constraints; use ${ULTRAGOAL_DIR}/${ULTRAGOAL_LEDGER} as the audit trail.`;
|
|
157
|
+
function aggregateCodexObjective(_goals) {
|
|
158
|
+
if (ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE.length <= 4000)
|
|
159
|
+
return ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE;
|
|
103
160
|
throw new UltragoalError('Generated aggregate Codex objective exceeds the 4,000 character goal limit.');
|
|
104
161
|
}
|
|
162
|
+
function isLegacyEnumeratedAggregateObjective(objective) {
|
|
163
|
+
if (!objective)
|
|
164
|
+
return false;
|
|
165
|
+
return (objective.startsWith(`Complete all ultragoal stories in ${ULTRAGOAL_DIR}/${ULTRAGOAL_GOALS}: `)
|
|
166
|
+
|| objective === `Complete all ultragoal stories listed in ${ULTRAGOAL_DIR}/${ULTRAGOAL_GOALS}. Use ${ULTRAGOAL_DIR}/${ULTRAGOAL_LEDGER} as the durable audit trail.`);
|
|
167
|
+
}
|
|
168
|
+
function compatibleCodexObjectives(plan) {
|
|
169
|
+
return (plan.codexObjectiveAliases ?? [])
|
|
170
|
+
.filter((objective) => isLegacyEnumeratedAggregateObjective(objective));
|
|
171
|
+
}
|
|
105
172
|
function expectedCodexObjective(plan, goal) {
|
|
106
173
|
return codexGoalMode(plan) === 'aggregate'
|
|
107
174
|
? (plan.codexObjective ?? aggregateCodexObjective(plan.goals))
|
|
108
175
|
: goal.objective;
|
|
109
176
|
}
|
|
177
|
+
function isSupersededResolved(goal, plan) {
|
|
178
|
+
if (goal.steeringStatus !== 'superseded')
|
|
179
|
+
return false;
|
|
180
|
+
const replacements = goal.supersededBy ?? [];
|
|
181
|
+
if (replacements.length === 0)
|
|
182
|
+
return false;
|
|
183
|
+
return replacements.every((id) => {
|
|
184
|
+
const replacement = plan.goals.find((candidate) => candidate.id === id);
|
|
185
|
+
return replacement !== undefined && isResolvedStatus(replacement.status);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function isCompletionBlocking(goal, plan) {
|
|
189
|
+
if (goal.steeringStatus === 'superseded')
|
|
190
|
+
return !isSupersededResolved(goal, plan);
|
|
191
|
+
if (goal.steeringStatus === 'blocked')
|
|
192
|
+
return true;
|
|
193
|
+
return !isResolvedStatus(goal.status);
|
|
194
|
+
}
|
|
195
|
+
function isCompletionBlockingForFinalCandidate(candidate, finalCandidate, plan) {
|
|
196
|
+
if (candidate.id === finalCandidate.id)
|
|
197
|
+
return false;
|
|
198
|
+
if (candidate.steeringStatus === 'superseded') {
|
|
199
|
+
const replacements = candidate.supersededBy ?? [];
|
|
200
|
+
if (replacements.length === 0)
|
|
201
|
+
return true;
|
|
202
|
+
return !replacements.every((id) => {
|
|
203
|
+
if (id === finalCandidate.id)
|
|
204
|
+
return true;
|
|
205
|
+
const replacement = plan.goals.find((goal) => goal.id === id);
|
|
206
|
+
return replacement !== undefined && isResolvedStatus(replacement.status);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return isCompletionBlocking(candidate, plan);
|
|
210
|
+
}
|
|
211
|
+
function isScheduleEligible(goal) {
|
|
212
|
+
return goal.steeringStatus !== 'superseded' && goal.steeringStatus !== 'blocked';
|
|
213
|
+
}
|
|
110
214
|
export function isFinalRunCompletionCandidate(plan, goal) {
|
|
111
|
-
return plan.goals.every((candidate) => candidate
|
|
215
|
+
return plan.goals.every((candidate) => !isCompletionBlockingForFinalCandidate(candidate, goal, plan));
|
|
112
216
|
}
|
|
113
217
|
export function isUltragoalDone(plan) {
|
|
114
218
|
if (plan.aggregateCompletion?.status === 'complete')
|
|
115
219
|
return true;
|
|
116
220
|
if (plan.goals.length === 0)
|
|
117
221
|
return true;
|
|
118
|
-
if (plan.goals.some((goal) => goal
|
|
119
|
-
return false;
|
|
120
|
-
if (!plan.goals.every((goal) => isResolvedStatus(goal.status)))
|
|
222
|
+
if (plan.goals.some((goal) => isCompletionBlocking(goal, plan)))
|
|
121
223
|
return false;
|
|
122
|
-
const latestNonReviewBlocked = [...plan.goals].reverse().find((goal) => goal.status !== 'review_blocked');
|
|
224
|
+
const latestNonReviewBlocked = [...plan.goals].reverse().find((goal) => goal.status !== 'review_blocked' && goal.steeringStatus !== 'superseded');
|
|
123
225
|
return latestNonReviewBlocked?.status === 'complete';
|
|
124
226
|
}
|
|
125
227
|
function titleFromObjective(objective, fallback) {
|
|
@@ -155,6 +257,37 @@ function normalizeGoalId(title, index) {
|
|
|
155
257
|
.replace(/-+$/g, '');
|
|
156
258
|
return `G${String(index + 1).padStart(3, '0')}${slug ? `-${slug}` : ''}`;
|
|
157
259
|
}
|
|
260
|
+
function sleep(ms) {
|
|
261
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
262
|
+
}
|
|
263
|
+
async function withUltragoalMutationLock(cwd, operation) {
|
|
264
|
+
await mkdir(ultragoalDir(cwd), { recursive: true });
|
|
265
|
+
const lockPath = join(ultragoalDir(cwd), ULTRAGOAL_MUTATION_LOCK);
|
|
266
|
+
let handle;
|
|
267
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
268
|
+
try {
|
|
269
|
+
handle = await open(lockPath, 'wx');
|
|
270
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, createdAt: iso() }));
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
const code = error.code;
|
|
275
|
+
if (code !== 'EEXIST')
|
|
276
|
+
throw error;
|
|
277
|
+
await sleep(Math.min(25 + attempt * 5, 250));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (!handle) {
|
|
281
|
+
throw new UltragoalError(`Timed out waiting for ultragoal mutation lock at ${repoRelative(cwd, lockPath)}.`);
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
return await operation();
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
await handle.close().catch(() => undefined);
|
|
288
|
+
await rm(lockPath, { force: true }).catch(() => undefined);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
158
291
|
async function appendLedger(cwd, entry) {
|
|
159
292
|
await mkdir(ultragoalDir(cwd), { recursive: true });
|
|
160
293
|
const path = ultragoalLedgerPath(cwd);
|
|
@@ -173,49 +306,69 @@ export async function readUltragoalPlan(cwd) {
|
|
|
173
306
|
if (parsed.version !== 1 || !Array.isArray(parsed.goals)) {
|
|
174
307
|
throw new UltragoalError(`Invalid ultragoal plan at ${repoRelative(cwd, path)}.`);
|
|
175
308
|
}
|
|
309
|
+
if (codexGoalMode(parsed) === 'aggregate' && isLegacyEnumeratedAggregateObjective(parsed.codexObjective)) {
|
|
310
|
+
const previousObjective = parsed.codexObjective;
|
|
311
|
+
const now = iso();
|
|
312
|
+
parsed.codexObjective = aggregateCodexObjective(parsed.goals);
|
|
313
|
+
parsed.codexObjectiveAliases = Array.from(new Set([...(parsed.codexObjectiveAliases ?? []), previousObjective].filter((value) => typeof value === 'string' && value.length > 0)));
|
|
314
|
+
parsed.updatedAt = now;
|
|
315
|
+
await writePlan(cwd, parsed);
|
|
316
|
+
await appendLedger(cwd, {
|
|
317
|
+
ts: now,
|
|
318
|
+
event: 'aggregate_objective_migrated',
|
|
319
|
+
message: 'Migrated legacy enumerated aggregate Codex objective to the stable pointer objective.',
|
|
320
|
+
before: { codexObjective: previousObjective },
|
|
321
|
+
after: { codexObjective: parsed.codexObjective },
|
|
322
|
+
});
|
|
323
|
+
}
|
|
176
324
|
return parsed;
|
|
177
325
|
}
|
|
178
326
|
async function writePlan(cwd, plan) {
|
|
179
327
|
await mkdir(ultragoalDir(cwd), { recursive: true });
|
|
180
|
-
|
|
328
|
+
const path = ultragoalGoalsPath(cwd);
|
|
329
|
+
const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
330
|
+
await writeFile(tmpPath, `${JSON.stringify(plan, null, 2)}\n`);
|
|
331
|
+
await rename(tmpPath, path);
|
|
181
332
|
}
|
|
182
333
|
export async function createUltragoalPlan(cwd, options) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
plan.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
334
|
+
return withUltragoalMutationLock(cwd, async () => {
|
|
335
|
+
if (!options.force && existsSync(ultragoalGoalsPath(cwd))) {
|
|
336
|
+
throw new UltragoalError(`Refusing to overwrite existing ${ULTRAGOAL_DIR}/${ULTRAGOAL_GOALS}; pass --force to recreate it.`);
|
|
337
|
+
}
|
|
338
|
+
const now = iso(options.now);
|
|
339
|
+
const sourceGoals = options.goals?.length
|
|
340
|
+
? options.goals
|
|
341
|
+
: deriveGoalCandidates(options.brief);
|
|
342
|
+
const candidates = sourceGoals
|
|
343
|
+
.map((goal, index) => ({
|
|
344
|
+
id: normalizeGoalId(goal.title ?? titleFromObjective(goal.objective, `Goal ${index + 1}`), index),
|
|
345
|
+
title: goal.title ?? titleFromObjective(goal.objective, `Goal ${index + 1}`),
|
|
346
|
+
objective: goal.objective.trim(),
|
|
347
|
+
status: 'pending',
|
|
348
|
+
tokenBudget: goal.tokenBudget,
|
|
349
|
+
attempt: 0,
|
|
350
|
+
createdAt: now,
|
|
351
|
+
updatedAt: now,
|
|
352
|
+
}));
|
|
353
|
+
const plan = {
|
|
354
|
+
version: 1,
|
|
355
|
+
createdAt: now,
|
|
356
|
+
updatedAt: now,
|
|
357
|
+
briefPath: `${ULTRAGOAL_DIR}/${ULTRAGOAL_BRIEF}`,
|
|
358
|
+
goalsPath: `${ULTRAGOAL_DIR}/${ULTRAGOAL_GOALS}`,
|
|
359
|
+
ledgerPath: `${ULTRAGOAL_DIR}/${ULTRAGOAL_LEDGER}`,
|
|
360
|
+
codexGoalMode: options.codexGoalMode ?? 'aggregate',
|
|
361
|
+
goals: candidates,
|
|
362
|
+
};
|
|
363
|
+
if (plan.codexGoalMode === 'aggregate')
|
|
364
|
+
plan.codexObjective = aggregateCodexObjective(candidates);
|
|
365
|
+
await mkdir(ultragoalDir(cwd), { recursive: true });
|
|
366
|
+
await writeFile(ultragoalBriefPath(cwd), options.brief.endsWith('\n') ? options.brief : `${options.brief}\n`);
|
|
367
|
+
await writePlan(cwd, plan);
|
|
368
|
+
await writeFile(ultragoalLedgerPath(cwd), '');
|
|
369
|
+
await appendLedger(cwd, { ts: now, event: 'plan_created', message: `${candidates.length} goal(s) created` });
|
|
370
|
+
return plan;
|
|
371
|
+
});
|
|
219
372
|
}
|
|
220
373
|
export function summarizeUltragoalPlan(plan) {
|
|
221
374
|
return {
|
|
@@ -225,6 +378,9 @@ export function summarizeUltragoalPlan(plan) {
|
|
|
225
378
|
complete: plan.goals.filter((goal) => goal.status === 'complete').length,
|
|
226
379
|
failed: plan.goals.filter((goal) => goal.status === 'failed').length,
|
|
227
380
|
reviewBlocked: plan.goals.filter((goal) => goal.status === 'review_blocked').length,
|
|
381
|
+
needsUserDecision: plan.goals.filter((goal) => goal.status === 'needs_user_decision').length,
|
|
382
|
+
superseded: plan.goals.filter((goal) => goal.steeringStatus === 'superseded').length,
|
|
383
|
+
steeringBlocked: plan.goals.filter((goal) => goal.steeringStatus === 'blocked').length,
|
|
228
384
|
aggregateComplete: plan.aggregateCompletion?.status === 'complete',
|
|
229
385
|
activeGoalId: plan.activeGoalId,
|
|
230
386
|
};
|
|
@@ -235,7 +391,34 @@ function assertNonEmpty(value, label) {
|
|
|
235
391
|
throw new UltragoalError(`Missing ${label}.`);
|
|
236
392
|
return trimmed;
|
|
237
393
|
}
|
|
238
|
-
function
|
|
394
|
+
export function parseUltragoalSteeringDirective(raw) {
|
|
395
|
+
const trimmed = raw.trim();
|
|
396
|
+
if (!trimmed || trimmed.length < 5)
|
|
397
|
+
return null;
|
|
398
|
+
try {
|
|
399
|
+
const parsed = JSON.parse(trimmed);
|
|
400
|
+
if (!parsed || typeof parsed !== 'object')
|
|
401
|
+
return null;
|
|
402
|
+
if (!parsed.kind || typeof parsed.kind !== 'string')
|
|
403
|
+
return null;
|
|
404
|
+
if (!parsed.source || typeof parsed.source !== 'string')
|
|
405
|
+
return null;
|
|
406
|
+
if (!parsed.evidence || typeof parsed.evidence !== 'string')
|
|
407
|
+
return null;
|
|
408
|
+
if (!parsed.rationale || typeof parsed.rationale !== 'string')
|
|
409
|
+
return null;
|
|
410
|
+
if (!ULTRAGOAL_STEERING_MUTATION_KINDS.includes(parsed.kind))
|
|
411
|
+
return null;
|
|
412
|
+
if (!ULTRAGOAL_STEERING_SOURCES.includes(parsed.source))
|
|
413
|
+
return null;
|
|
414
|
+
return parsed;
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function appendGoalToPlan(plan, options, nowOverride) {
|
|
421
|
+
const now = nowOverride ?? iso(options.now);
|
|
239
422
|
const title = assertNonEmpty(options.title, '--title');
|
|
240
423
|
const objective = assertNonEmpty(options.objective, '--objective');
|
|
241
424
|
const goal = {
|
|
@@ -253,19 +436,360 @@ function appendGoalToPlan(plan, options, now) {
|
|
|
253
436
|
return goal;
|
|
254
437
|
}
|
|
255
438
|
export async function addUltragoalGoal(cwd, options) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
439
|
+
return withUltragoalMutationLock(cwd, async () => {
|
|
440
|
+
const plan = await readUltragoalPlan(cwd);
|
|
441
|
+
const now = iso(options.now);
|
|
442
|
+
const goal = appendGoalToPlan(plan, options);
|
|
443
|
+
await writePlan(cwd, plan);
|
|
444
|
+
await appendLedger(cwd, {
|
|
445
|
+
ts: now,
|
|
446
|
+
event: 'goal_added',
|
|
447
|
+
goalId: goal.id,
|
|
448
|
+
status: goal.status,
|
|
449
|
+
evidence: options.evidence,
|
|
450
|
+
message: goal.title,
|
|
451
|
+
});
|
|
452
|
+
return { plan, goal };
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
function proposalTargetIds(proposal) {
|
|
456
|
+
return proposal.targetGoalIds?.length ? proposal.targetGoalIds : (proposal.targetGoalId ? [proposal.targetGoalId] : []);
|
|
457
|
+
}
|
|
458
|
+
function steeringTargets(plan, proposal) {
|
|
459
|
+
return proposalTargetIds(proposal).map((id) => {
|
|
460
|
+
const goal = plan.goals.find((candidate) => candidate.id === id);
|
|
461
|
+
if (!goal)
|
|
462
|
+
throw new UltragoalError(`Unknown ultragoal id: ${id}`);
|
|
463
|
+
return goal;
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
function mentionsWeakenedCompletion(...values) {
|
|
467
|
+
const normalized = values.filter(Boolean).join(' ').toLowerCase();
|
|
468
|
+
return /\b(skip|bypass|weaken|remove|omit|auto[-\s]?complete|mark complete|complete faster)\b/.test(normalized)
|
|
469
|
+
&& /\b(test|tests|verification|review|quality gate|complete|completion)\b/.test(normalized);
|
|
470
|
+
}
|
|
471
|
+
function hasProtectedSteeringPayload(value) {
|
|
472
|
+
if (!value || typeof value !== 'object')
|
|
473
|
+
return false;
|
|
474
|
+
const protectedKeys = new Set([
|
|
475
|
+
'aggregateCompletion',
|
|
476
|
+
'brief',
|
|
477
|
+
'briefPath',
|
|
478
|
+
'codexObjective',
|
|
479
|
+
'constraints',
|
|
480
|
+
'completedAt',
|
|
481
|
+
'qualityGate',
|
|
482
|
+
'status',
|
|
483
|
+
]);
|
|
484
|
+
const stack = [value];
|
|
485
|
+
while (stack.length > 0) {
|
|
486
|
+
const current = stack.pop();
|
|
487
|
+
if (!current || typeof current !== 'object')
|
|
488
|
+
continue;
|
|
489
|
+
for (const [key, child] of Object.entries(current)) {
|
|
490
|
+
if (protectedKeys.has(key))
|
|
491
|
+
return true;
|
|
492
|
+
if (key.toLowerCase().includes('complete'))
|
|
493
|
+
return true;
|
|
494
|
+
if (child && typeof child === 'object')
|
|
495
|
+
stack.push(child);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
function protectedIntentText(proposal) {
|
|
501
|
+
const after = proposal.after;
|
|
502
|
+
const childTexts = rawChildGoalsFromProposal(proposal).flatMap((child) => {
|
|
503
|
+
if (!child || typeof child !== 'object' || Array.isArray(child))
|
|
504
|
+
return [];
|
|
505
|
+
const candidate = child;
|
|
506
|
+
return [candidate.title, candidate.objective];
|
|
507
|
+
});
|
|
508
|
+
return [
|
|
509
|
+
proposal.title,
|
|
510
|
+
proposal.objective,
|
|
511
|
+
proposal.revisedTitle,
|
|
512
|
+
proposal.revisedObjective,
|
|
513
|
+
after?.title,
|
|
514
|
+
after?.objective,
|
|
515
|
+
proposal.rationale,
|
|
516
|
+
proposal.directiveText,
|
|
517
|
+
...childTexts,
|
|
518
|
+
]
|
|
519
|
+
.filter((value) => typeof value === 'string')
|
|
520
|
+
.join('\n')
|
|
521
|
+
.toLowerCase();
|
|
522
|
+
}
|
|
523
|
+
function rawChildGoalsFromProposal(proposal) {
|
|
524
|
+
if (Array.isArray(proposal.childGoals) && proposal.childGoals.length > 0)
|
|
525
|
+
return proposal.childGoals;
|
|
526
|
+
const after = proposal.after;
|
|
527
|
+
return Array.isArray(after?.children) ? after.children : [];
|
|
528
|
+
}
|
|
529
|
+
function isValidSteeringChildGoal(value) {
|
|
530
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
531
|
+
return false;
|
|
532
|
+
const candidate = value;
|
|
533
|
+
return typeof candidate.title === 'string'
|
|
534
|
+
&& candidate.title.trim().length > 0
|
|
535
|
+
&& typeof candidate.objective === 'string'
|
|
536
|
+
&& candidate.objective.trim().length > 0;
|
|
537
|
+
}
|
|
538
|
+
function childGoalsFromProposal(proposal) {
|
|
539
|
+
return rawChildGoalsFromProposal(proposal).filter(isValidSteeringChildGoal);
|
|
540
|
+
}
|
|
541
|
+
function pendingOrderFromProposal(proposal) {
|
|
542
|
+
if (proposal.pendingOrder?.length)
|
|
543
|
+
return proposal.pendingOrder;
|
|
544
|
+
const after = proposal.after;
|
|
545
|
+
return Array.isArray(after?.pendingGoalIds) ? after.pendingGoalIds : [];
|
|
546
|
+
}
|
|
547
|
+
function revisedTitleFromProposal(proposal) {
|
|
548
|
+
if (proposal.revisedTitle?.trim())
|
|
549
|
+
return proposal.revisedTitle;
|
|
550
|
+
const after = proposal.after;
|
|
551
|
+
return after?.title ?? proposal.title;
|
|
552
|
+
}
|
|
553
|
+
function revisedObjectiveFromProposal(proposal) {
|
|
554
|
+
if (proposal.revisedObjective?.trim())
|
|
555
|
+
return proposal.revisedObjective;
|
|
556
|
+
const after = proposal.after;
|
|
557
|
+
return after?.objective ?? proposal.objective;
|
|
558
|
+
}
|
|
559
|
+
export function validateUltragoalSteeringProposal(plan, proposal) {
|
|
560
|
+
const rejectedReasons = [];
|
|
561
|
+
const evidenceBackedNecessity = Boolean(proposal.evidence?.trim()) && Boolean(proposal.rationale?.trim());
|
|
562
|
+
if (!ULTRAGOAL_STEERING_MUTATION_KINDS.includes(proposal.kind))
|
|
563
|
+
rejectedReasons.push(`Invalid steering mutation kind: ${String(proposal.kind)}.`);
|
|
564
|
+
if (!ULTRAGOAL_STEERING_SOURCES.includes(proposal.source))
|
|
565
|
+
rejectedReasons.push(`Invalid steering source: ${String(proposal.source)}.`);
|
|
566
|
+
if (!evidenceBackedNecessity)
|
|
567
|
+
rejectedReasons.push('Steering requires non-empty evidence and rationale.');
|
|
568
|
+
if (hasProtectedSteeringPayload(proposal.after))
|
|
569
|
+
rejectedReasons.push('Steering payload must not edit protected objective, constraint, quality gate, or completion fields.');
|
|
570
|
+
if (/\b(?:skip|bypass|weaken|remove)\b.*\b(?:test|tests|review|verification|quality gate|complete|completion)\b|\bauto[- ]?complete\b/.test(protectedIntentText(proposal))) {
|
|
571
|
+
rejectedReasons.push('Steering must not weaken completion, quality gates, tests, reviews, or auto-complete work.');
|
|
572
|
+
}
|
|
573
|
+
if (plan.aggregateCompletion?.status === 'complete')
|
|
574
|
+
rejectedReasons.push('Cannot steer an already completed aggregate ultragoal plan.');
|
|
575
|
+
let targets = [];
|
|
576
|
+
try {
|
|
577
|
+
targets = steeringTargets(plan, proposal);
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
581
|
+
rejectedReasons.push(message.replace(/^Unknown ultragoal id:/, 'unknown ultragoal id:'));
|
|
582
|
+
}
|
|
583
|
+
const target = targets[0];
|
|
584
|
+
if ((proposal.kind === 'split_subgoal' || proposal.kind === 'revise_pending_wording' || proposal.kind === 'mark_blocked_superseded') && !target) {
|
|
585
|
+
rejectedReasons.push(`${proposal.kind} requires a target goal id.`);
|
|
586
|
+
}
|
|
587
|
+
if ((proposal.kind === 'split_subgoal' || proposal.kind === 'revise_pending_wording') && target?.status !== 'pending') {
|
|
588
|
+
rejectedReasons.push(`${proposal.kind} can only target a pending goal.`);
|
|
589
|
+
}
|
|
590
|
+
if (proposal.kind === 'add_subgoal') {
|
|
591
|
+
if (!proposal.title?.trim() || !proposal.objective?.trim())
|
|
592
|
+
rejectedReasons.push('add_subgoal requires title and objective.');
|
|
593
|
+
}
|
|
594
|
+
if (proposal.kind === 'split_subgoal') {
|
|
595
|
+
const rawChildren = rawChildGoalsFromProposal(proposal);
|
|
596
|
+
if (rawChildren.length === 0)
|
|
597
|
+
rejectedReasons.push('split_subgoal requires replacement child goals.');
|
|
598
|
+
if (rawChildren.some((child) => !isValidSteeringChildGoal(child)))
|
|
599
|
+
rejectedReasons.push('split_subgoal children require title and objective.');
|
|
600
|
+
}
|
|
601
|
+
if (proposal.kind === 'mark_blocked_superseded') {
|
|
602
|
+
const rawChildren = rawChildGoalsFromProposal(proposal);
|
|
603
|
+
if (rawChildren.some((child) => !isValidSteeringChildGoal(child)))
|
|
604
|
+
rejectedReasons.push('mark_blocked_superseded replacement children require title and objective.');
|
|
605
|
+
}
|
|
606
|
+
if (proposal.kind === 'reorder_pending') {
|
|
607
|
+
const requested = pendingOrderFromProposal(proposal);
|
|
608
|
+
const pending = plan.goals.filter((goal) => goal.status === 'pending' && isScheduleEligible(goal)).map((goal) => goal.id);
|
|
609
|
+
if (requested.length === 0)
|
|
610
|
+
rejectedReasons.push('reorder_pending requires at least one pending goal id.');
|
|
611
|
+
if (new Set(requested).size !== requested.length)
|
|
612
|
+
rejectedReasons.push('duplicate goal id in pendingOrder.');
|
|
613
|
+
if (requested.some((id) => !pending.includes(id)))
|
|
614
|
+
rejectedReasons.push('pendingOrder contains non-pending or unknown goal.');
|
|
615
|
+
}
|
|
616
|
+
if (proposal.kind === 'revise_pending_wording') {
|
|
617
|
+
if (!revisedTitleFromProposal(proposal)?.trim() && !revisedObjectiveFromProposal(proposal)?.trim())
|
|
618
|
+
rejectedReasons.push('revise_pending_wording requires title or objective.');
|
|
619
|
+
}
|
|
620
|
+
if (proposal.kind === 'annotate_ledger' && !proposal.evidence?.trim())
|
|
621
|
+
rejectedReasons.push('annotate_ledger requires evidence.');
|
|
622
|
+
const accepted = rejectedReasons.length === 0;
|
|
623
|
+
const noEasierCompletion = !mentionsWeakenedCompletion(protectedIntentText(proposal));
|
|
624
|
+
return {
|
|
625
|
+
structuralInvariantAccepted: accepted,
|
|
626
|
+
evidenceBackedNecessity,
|
|
627
|
+
noEasierCompletion,
|
|
628
|
+
accepted,
|
|
629
|
+
rejectedReasons,
|
|
630
|
+
reasons: rejectedReasons,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
export const validateSteeringProposal = validateUltragoalSteeringProposal;
|
|
634
|
+
async function readSteeringLedgerEntries(cwd) {
|
|
635
|
+
try {
|
|
636
|
+
const raw = await readFile(ultragoalLedgerPath(cwd), 'utf-8');
|
|
637
|
+
return raw.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
return [];
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function cloneForAudit(value) {
|
|
644
|
+
return JSON.parse(JSON.stringify(value));
|
|
645
|
+
}
|
|
646
|
+
function moveGoalsAfterTarget(plan, targetId, movedIds) {
|
|
647
|
+
const moved = movedIds.map((id) => plan.goals.find((goal) => goal.id === id)).filter((goal) => Boolean(goal));
|
|
648
|
+
if (moved.length === 0)
|
|
649
|
+
return;
|
|
650
|
+
plan.goals = plan.goals.filter((goal) => !movedIds.includes(goal.id));
|
|
651
|
+
const targetIndex = plan.goals.findIndex((goal) => goal.id === targetId);
|
|
652
|
+
plan.goals.splice(targetIndex >= 0 ? targetIndex + 1 : plan.goals.length, 0, ...moved);
|
|
653
|
+
}
|
|
654
|
+
function applySteeringMutation(plan, proposal, now) {
|
|
655
|
+
const targets = steeringTargets(plan, proposal);
|
|
656
|
+
const target = targets[0];
|
|
657
|
+
if (proposal.kind === 'add_subgoal') {
|
|
658
|
+
const goal = appendGoalToPlan(plan, { title: proposal.title ?? '', objective: proposal.objective ?? '', evidence: proposal.evidence, now: new Date(now) });
|
|
659
|
+
return { before: undefined, after: cloneForAudit(goal) };
|
|
660
|
+
}
|
|
661
|
+
if (proposal.kind === 'split_subgoal') {
|
|
662
|
+
const before = cloneForAudit(target);
|
|
663
|
+
const children = childGoalsFromProposal(proposal).map((child) => appendGoalToPlan(plan, { ...child, evidence: proposal.evidence, now: new Date(now) }));
|
|
664
|
+
target.steeringStatus = 'superseded';
|
|
665
|
+
target.supersededBy = children.map((child) => child.id);
|
|
666
|
+
moveGoalsAfterTarget(plan, target.id, children.map((child) => child.id));
|
|
667
|
+
target.steeringEvidence = proposal.evidence;
|
|
668
|
+
target.steeringRationale = proposal.rationale;
|
|
669
|
+
target.updatedAt = now;
|
|
670
|
+
for (const child of children)
|
|
671
|
+
child.supersedes = [target.id];
|
|
672
|
+
if (plan.activeGoalId === target.id)
|
|
673
|
+
plan.activeGoalId = undefined;
|
|
674
|
+
plan.updatedAt = now;
|
|
675
|
+
return { before, after: { target: cloneForAudit(target), children: cloneForAudit(children) } };
|
|
676
|
+
}
|
|
677
|
+
if (proposal.kind === 'reorder_pending') {
|
|
678
|
+
const before = plan.goals.map((goal) => goal.id);
|
|
679
|
+
const requested = pendingOrderFromProposal(proposal);
|
|
680
|
+
const requestedSet = new Set(requested);
|
|
681
|
+
const requestedGoals = requested.map((id) => plan.goals.find((goal) => goal.id === id)).filter((goal) => Boolean(goal));
|
|
682
|
+
const remaining = plan.goals.filter((goal) => !requestedSet.has(goal.id));
|
|
683
|
+
plan.goals = [...requestedGoals, ...remaining];
|
|
684
|
+
plan.updatedAt = now;
|
|
685
|
+
return { before, after: plan.goals.map((goal) => goal.id) };
|
|
686
|
+
}
|
|
687
|
+
if (proposal.kind === 'revise_pending_wording') {
|
|
688
|
+
const before = cloneForAudit(target);
|
|
689
|
+
const revisedTitle = revisedTitleFromProposal(proposal);
|
|
690
|
+
const revisedObjective = revisedObjectiveFromProposal(proposal);
|
|
691
|
+
if (revisedTitle?.trim())
|
|
692
|
+
target.title = revisedTitle.trim();
|
|
693
|
+
if (revisedObjective?.trim())
|
|
694
|
+
target.objective = revisedObjective.trim();
|
|
695
|
+
target.steeringEvidence = proposal.evidence;
|
|
696
|
+
target.steeringRationale = proposal.rationale;
|
|
697
|
+
target.updatedAt = now;
|
|
698
|
+
plan.updatedAt = now;
|
|
699
|
+
return { before, after: cloneForAudit(target) };
|
|
700
|
+
}
|
|
701
|
+
if (proposal.kind === 'annotate_ledger') {
|
|
702
|
+
return { before: undefined, after: { evidence: proposal.evidence, rationale: proposal.rationale } };
|
|
703
|
+
}
|
|
704
|
+
if (proposal.kind === 'mark_blocked_superseded') {
|
|
705
|
+
const before = cloneForAudit(target);
|
|
706
|
+
const children = childGoalsFromProposal(proposal);
|
|
707
|
+
if (children.length > 0) {
|
|
708
|
+
const replacements = children.map((child) => appendGoalToPlan(plan, { ...child, evidence: proposal.evidence, now: new Date(now) }));
|
|
709
|
+
target.steeringStatus = 'superseded';
|
|
710
|
+
target.supersededBy = replacements.map((child) => child.id);
|
|
711
|
+
moveGoalsAfterTarget(plan, target.id, replacements.map((child) => child.id));
|
|
712
|
+
target.steeringEvidence = proposal.evidence;
|
|
713
|
+
target.steeringRationale = proposal.rationale;
|
|
714
|
+
target.updatedAt = now;
|
|
715
|
+
for (const replacement of replacements)
|
|
716
|
+
replacement.supersedes = [target.id];
|
|
717
|
+
if (plan.activeGoalId === target.id)
|
|
718
|
+
plan.activeGoalId = undefined;
|
|
719
|
+
plan.updatedAt = now;
|
|
720
|
+
return { before, after: { target: cloneForAudit(target), children: cloneForAudit(replacements) } };
|
|
721
|
+
}
|
|
722
|
+
if (plan.activeGoalId === target.id)
|
|
723
|
+
delete plan.activeGoalId;
|
|
724
|
+
target.steeringStatus = 'blocked';
|
|
725
|
+
target.blockedReason = proposal.blockedReason ?? proposal.rationale;
|
|
726
|
+
target.steeringEvidence = proposal.evidence;
|
|
727
|
+
target.steeringRationale = proposal.rationale;
|
|
728
|
+
target.updatedAt = now;
|
|
729
|
+
if (plan.activeGoalId === target.id)
|
|
730
|
+
plan.activeGoalId = undefined;
|
|
731
|
+
plan.updatedAt = now;
|
|
732
|
+
return { before, after: cloneForAudit(target) };
|
|
733
|
+
}
|
|
734
|
+
return {};
|
|
735
|
+
}
|
|
736
|
+
export async function steerUltragoal(cwd, proposal, options = {}) {
|
|
737
|
+
return withUltragoalMutationLock(cwd, async () => {
|
|
738
|
+
const plan = await readUltragoalPlan(cwd);
|
|
739
|
+
const existing = proposal.idempotencyKey
|
|
740
|
+
? (await readSteeringLedgerEntries(cwd)).find((entry) => entry.event === 'steering_accepted' && (entry.idempotencyKey === proposal.idempotencyKey || entry.steering?.idempotencyKey === proposal.idempotencyKey) && entry.steering)
|
|
741
|
+
: undefined;
|
|
742
|
+
if (existing?.steering) {
|
|
743
|
+
return { plan, accepted: true, audit: { ...existing.steering, deduped: true }, rejectedReasons: [], deduped: true };
|
|
744
|
+
}
|
|
745
|
+
let invariant = validateUltragoalSteeringProposal(plan, proposal);
|
|
746
|
+
const now = iso(options.now ?? proposal.now);
|
|
747
|
+
const beforePlan = cloneForAudit(plan);
|
|
748
|
+
let mutation = {};
|
|
749
|
+
if (invariant.accepted) {
|
|
750
|
+
try {
|
|
751
|
+
mutation = applySteeringMutation(plan, proposal, now);
|
|
752
|
+
}
|
|
753
|
+
catch (error) {
|
|
754
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
755
|
+
const rejectedReasons = [...invariant.rejectedReasons, `Steering mutation failed: ${message}`];
|
|
756
|
+
invariant = {
|
|
757
|
+
...invariant,
|
|
758
|
+
accepted: false,
|
|
759
|
+
structuralInvariantAccepted: false,
|
|
760
|
+
rejectedReasons,
|
|
761
|
+
reasons: rejectedReasons,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const audit = {
|
|
766
|
+
kind: proposal.kind,
|
|
767
|
+
source: proposal.source,
|
|
768
|
+
targetGoalIds: proposalTargetIds(proposal),
|
|
769
|
+
before: mutation.before ?? beforePlan,
|
|
770
|
+
after: mutation.after,
|
|
771
|
+
evidence: proposal.evidence,
|
|
772
|
+
rationale: proposal.rationale,
|
|
773
|
+
invariant,
|
|
774
|
+
directiveText: options.directiveText ?? proposal.directiveText,
|
|
775
|
+
promptSignature: proposal.promptSignature,
|
|
776
|
+
idempotencyKey: proposal.idempotencyKey,
|
|
777
|
+
};
|
|
778
|
+
if (invariant.accepted)
|
|
779
|
+
await writePlan(cwd, plan);
|
|
780
|
+
await appendLedger(cwd, {
|
|
781
|
+
ts: now,
|
|
782
|
+
event: invariant.accepted ? 'steering_accepted' : 'steering_rejected',
|
|
783
|
+
goalId: proposalTargetIds(proposal)[0],
|
|
784
|
+
evidence: proposal.evidence,
|
|
785
|
+
message: proposal.rationale,
|
|
786
|
+
steering: audit,
|
|
787
|
+
mutationKind: proposal.kind,
|
|
788
|
+
before: audit.before,
|
|
789
|
+
after: audit.after,
|
|
790
|
+
});
|
|
791
|
+
return { plan, accepted: invariant.accepted, audit, rejectedReasons: invariant.rejectedReasons, deduped: false };
|
|
267
792
|
});
|
|
268
|
-
return { plan, goal };
|
|
269
793
|
}
|
|
270
794
|
function validateQualityGate(value) {
|
|
271
795
|
if (!value || typeof value !== 'object') {
|
|
@@ -301,230 +825,259 @@ function validateQualityGate(value) {
|
|
|
301
825
|
return gate;
|
|
302
826
|
}
|
|
303
827
|
export async function startNextUltragoal(cwd, options = {}) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
next
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
828
|
+
return withUltragoalMutationLock(cwd, async () => {
|
|
829
|
+
const plan = await readUltragoalPlan(cwd);
|
|
830
|
+
const now = iso(options.now);
|
|
831
|
+
if (plan.aggregateCompletion?.status === 'complete')
|
|
832
|
+
return { plan, goal: null, resumed: false, done: true };
|
|
833
|
+
const existing = plan.goals.find((goal) => goal.status === 'in_progress' && isScheduleEligibleGoal(goal));
|
|
834
|
+
if (existing) {
|
|
835
|
+
await appendLedger(cwd, { ts: now, event: 'goal_resumed', goalId: existing.id, status: existing.status, message: 'Resuming active ultragoal' });
|
|
836
|
+
return { plan, goal: existing, resumed: true, done: false };
|
|
837
|
+
}
|
|
838
|
+
let next = plan.goals.find((goal) => goal.status === 'pending' && isScheduleEligible(goal));
|
|
839
|
+
if (!next && options.retryFailed) {
|
|
840
|
+
next = plan.goals.find((goal) => goal.status === 'failed' && !goal.nonRetriable && isScheduleEligible(goal));
|
|
841
|
+
if (next)
|
|
842
|
+
await appendLedger(cwd, { ts: now, event: 'goal_retried', goalId: next.id, status: 'pending', message: next.failureReason });
|
|
843
|
+
}
|
|
844
|
+
if (!next)
|
|
845
|
+
return { plan, goal: null, resumed: false, done: isUltragoalDone(plan) };
|
|
846
|
+
next.status = 'in_progress';
|
|
847
|
+
next.attempt += 1;
|
|
848
|
+
next.startedAt = now;
|
|
849
|
+
next.failedAt = undefined;
|
|
850
|
+
next.failureReason = undefined;
|
|
851
|
+
clearGoalBlockerFields(next);
|
|
852
|
+
next.updatedAt = now;
|
|
853
|
+
plan.activeGoalId = next.id;
|
|
854
|
+
plan.updatedAt = now;
|
|
855
|
+
await writePlan(cwd, plan);
|
|
856
|
+
await appendLedger(cwd, { ts: now, event: 'goal_started', goalId: next.id, status: next.status, message: `Attempt ${next.attempt}` });
|
|
857
|
+
return { plan, goal: next, resumed: false, done: false };
|
|
858
|
+
});
|
|
332
859
|
}
|
|
333
860
|
export async function checkpointUltragoal(cwd, options) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (
|
|
341
|
-
|
|
861
|
+
return withUltragoalMutationLock(cwd, async () => {
|
|
862
|
+
const plan = await readUltragoalPlan(cwd);
|
|
863
|
+
const goal = plan.goals.find((candidate) => candidate.id === options.goalId);
|
|
864
|
+
if (!goal)
|
|
865
|
+
throw new UltragoalError(`Unknown ultragoal id: ${options.goalId}`);
|
|
866
|
+
const now = iso(options.now);
|
|
867
|
+
if (options.status === 'blocked') {
|
|
868
|
+
if (goal.status !== 'in_progress') {
|
|
869
|
+
throw new UltragoalError(`Cannot record a blocked checkpoint for ${goal.id} while it is ${goal.status}; start or resume the ultragoal before recording a non-terminal blocker.`);
|
|
870
|
+
}
|
|
871
|
+
const snapshot = options.codexGoal === undefined ? null : parseCodexGoalSnapshot(options.codexGoal);
|
|
872
|
+
if (!snapshot?.available) {
|
|
873
|
+
throw new UltragoalError('Blocked ultragoal checkpoints require a get_goal snapshot for the completed legacy Codex goal that blocked create_goal; pass --codex-goal-json.');
|
|
874
|
+
}
|
|
875
|
+
if (snapshot.status !== 'complete') {
|
|
876
|
+
throw new UltragoalError(`Cannot record a blocked ultragoal checkpoint while the existing Codex goal is ${snapshot.status ?? 'unknown'}; strict objective mismatch protection remains required for active or incomplete goals.`);
|
|
877
|
+
}
|
|
878
|
+
if (!snapshot.objective) {
|
|
879
|
+
throw new UltragoalError('Blocked ultragoal checkpoint Codex snapshot is missing objective text.');
|
|
880
|
+
}
|
|
881
|
+
if (normalizeObjective(snapshot.objective) === normalizeObjective(expectedCodexObjective(plan, goal))) {
|
|
882
|
+
throw new UltragoalError('Blocked ultragoal checkpoint is only for a different completed legacy Codex goal; complete this ultragoal with --status complete after its audit passes.');
|
|
883
|
+
}
|
|
884
|
+
goal.updatedAt = now;
|
|
885
|
+
plan.activeGoalId = goal.id;
|
|
886
|
+
plan.updatedAt = now;
|
|
887
|
+
await writePlan(cwd, plan);
|
|
888
|
+
await appendLedger(cwd, {
|
|
889
|
+
ts: now,
|
|
890
|
+
event: 'goal_blocked',
|
|
891
|
+
goalId: goal.id,
|
|
892
|
+
status: goal.status,
|
|
893
|
+
evidence: options.evidence,
|
|
894
|
+
codexGoal: options.codexGoal,
|
|
895
|
+
});
|
|
896
|
+
return plan;
|
|
342
897
|
}
|
|
343
|
-
|
|
344
|
-
if (
|
|
345
|
-
|
|
898
|
+
let aggregateCompletion;
|
|
899
|
+
if (options.status === 'complete') {
|
|
900
|
+
const expectedObjective = expectedCodexObjective(plan, goal);
|
|
901
|
+
const aggregateMode = codexGoalMode(plan) === 'aggregate';
|
|
902
|
+
const finalRunCheckpoint = isFinalRunCompletionCandidate(plan, goal);
|
|
903
|
+
const snapshot = options.codexGoal === undefined ? null : parseCodexGoalSnapshot(options.codexGoal);
|
|
904
|
+
const reconciliation = reconcileCodexGoalSnapshot(snapshot, {
|
|
905
|
+
expectedObjective,
|
|
906
|
+
acceptedObjectives: aggregateMode ? compatibleCodexObjectives(plan) : undefined,
|
|
907
|
+
allowedStatuses: aggregateMode
|
|
908
|
+
? (finalRunCheckpoint && !options.allowActiveFinalCodexGoal ? ['complete'] : ['active'])
|
|
909
|
+
: ['complete'],
|
|
910
|
+
requireSnapshot: true,
|
|
911
|
+
requireComplete: !aggregateMode || (finalRunCheckpoint && !options.allowActiveFinalCodexGoal),
|
|
912
|
+
});
|
|
913
|
+
if (!reconciliation.ok) {
|
|
914
|
+
const completedTaskScopedAggregateSnapshot = snapshot?.available
|
|
915
|
+
&& snapshot.status === 'complete'
|
|
916
|
+
&& Boolean(snapshot.objective)
|
|
917
|
+
&& normalizeObjective(snapshot.objective ?? '') !== normalizeObjective(expectedObjective)
|
|
918
|
+
&& await canReconcileCompletedTaskScopedAggregateSnapshot(cwd, plan, goal, snapshot.objective ?? '', options.evidence);
|
|
919
|
+
if (completedTaskScopedAggregateSnapshot) {
|
|
920
|
+
aggregateCompletion = {
|
|
921
|
+
status: 'complete',
|
|
922
|
+
completedAt: now,
|
|
923
|
+
evidence: assertNonEmpty(options.evidence, '--evidence'),
|
|
924
|
+
codexGoal: options.codexGoal,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
const taskScopedRequirement = aggregateMode && snapshot?.status === 'complete' && Boolean(snapshot.objective)
|
|
929
|
+
? ' Completed task-scoped aggregate reconciliation requires the checkpoint goal to be the active in-progress OMX goal, evidence that names that active OMX goal id, names .omx/ultragoal/goals.json or ledger.jsonl, includes completed implementation plus validation/review evidence, and a get_goal objective that maps to the ultragoal brief/artifact.'
|
|
930
|
+
: '';
|
|
931
|
+
const remediation = reconciliation.snapshot.available
|
|
932
|
+
&& reconciliation.snapshot.status === 'complete'
|
|
933
|
+
&& Boolean(reconciliation.snapshot.objective)
|
|
934
|
+
&& normalizeObjective(reconciliation.snapshot.objective ?? '') !== normalizeObjective(expectedObjective)
|
|
935
|
+
? ` ${buildCompletedLegacyGoalRemediation(goal)}`
|
|
936
|
+
: '';
|
|
937
|
+
throw new UltragoalError(`${formatCodexGoalReconciliation(reconciliation)}${taskScopedRequirement}${remediation}`);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
if (finalRunCheckpoint && !options.allowActiveFinalCodexGoal)
|
|
941
|
+
goal.evidence = options.evidence;
|
|
346
942
|
}
|
|
347
|
-
|
|
348
|
-
|
|
943
|
+
const qualityGate = options.status === 'complete' && (aggregateCompletion !== undefined || (isFinalRunCompletionCandidate(plan, goal) && !options.allowActiveFinalCodexGoal))
|
|
944
|
+
? validateQualityGate(options.qualityGate)
|
|
945
|
+
: undefined;
|
|
946
|
+
if (aggregateCompletion) {
|
|
947
|
+
plan.aggregateCompletion = aggregateCompletion;
|
|
948
|
+
if (plan.activeGoalId === goal.id)
|
|
949
|
+
delete plan.activeGoalId;
|
|
950
|
+
plan.updatedAt = now;
|
|
951
|
+
await writePlan(cwd, plan);
|
|
952
|
+
await appendLedger(cwd, {
|
|
953
|
+
ts: now,
|
|
954
|
+
event: 'aggregate_completed',
|
|
955
|
+
goalId: goal.id,
|
|
956
|
+
status: goal.status,
|
|
957
|
+
evidence: options.evidence,
|
|
958
|
+
codexGoal: options.codexGoal,
|
|
959
|
+
qualityGate,
|
|
960
|
+
message: 'Aggregate ultragoal plan completed via task-scoped Codex goal snapshot; microgoal ledger progress remains independent.',
|
|
961
|
+
});
|
|
962
|
+
return plan;
|
|
349
963
|
}
|
|
350
|
-
|
|
351
|
-
|
|
964
|
+
goal.status = options.status;
|
|
965
|
+
goal.updatedAt = now;
|
|
966
|
+
if (options.status === 'complete') {
|
|
967
|
+
goal.completedAt = now;
|
|
968
|
+
goal.evidence = options.evidence;
|
|
969
|
+
goal.failureReason = undefined;
|
|
970
|
+
goal.failedAt = undefined;
|
|
971
|
+
clearGoalBlockerFields(goal);
|
|
972
|
+
if (plan.activeGoalId === goal.id)
|
|
973
|
+
delete plan.activeGoalId;
|
|
352
974
|
}
|
|
353
|
-
|
|
354
|
-
|
|
975
|
+
else {
|
|
976
|
+
const blocker = classifyExternalAuthorizationBlocker(options.evidence);
|
|
977
|
+
const previousEntries = blocker ? await readSteeringLedgerEntries(cwd) : [];
|
|
978
|
+
const occurrenceCount = blocker ? sameBlockerOccurrences(previousEntries, goal.id, blocker.signature) + 1 : 0;
|
|
979
|
+
const shouldCircuitBreak = blocker !== null && occurrenceCount >= 3;
|
|
980
|
+
goal.failedAt = now;
|
|
981
|
+
goal.failureReason = options.evidence;
|
|
982
|
+
goal.blockerSignature = blocker?.signature;
|
|
983
|
+
goal.blockerOccurrenceCount = blocker ? occurrenceCount : undefined;
|
|
984
|
+
goal.requiredExternalDecision = blocker?.requiredDecision;
|
|
985
|
+
goal.nonRetriable = shouldCircuitBreak || undefined;
|
|
986
|
+
if (shouldCircuitBreak) {
|
|
987
|
+
goal.status = 'needs_user_decision';
|
|
988
|
+
goal.blockedReason = options.evidence;
|
|
989
|
+
}
|
|
990
|
+
if (plan.activeGoalId === goal.id)
|
|
991
|
+
delete plan.activeGoalId;
|
|
355
992
|
}
|
|
356
|
-
goal.updatedAt = now;
|
|
357
|
-
plan.activeGoalId = goal.id;
|
|
358
993
|
plan.updatedAt = now;
|
|
359
994
|
await writePlan(cwd, plan);
|
|
995
|
+
const blockerEvent = goal.status === 'needs_user_decision';
|
|
360
996
|
await appendLedger(cwd, {
|
|
361
997
|
ts: now,
|
|
362
|
-
event: '
|
|
998
|
+
event: options.status === 'complete' ? 'goal_completed' : blockerEvent ? 'goal_needs_user_decision' : 'goal_failed',
|
|
363
999
|
goalId: goal.id,
|
|
364
1000
|
status: goal.status,
|
|
365
1001
|
evidence: options.evidence,
|
|
366
1002
|
codexGoal: options.codexGoal,
|
|
1003
|
+
qualityGate,
|
|
1004
|
+
blockerSignature: goal.blockerSignature,
|
|
1005
|
+
blockerOccurrenceCount: goal.blockerOccurrenceCount,
|
|
1006
|
+
requiredExternalDecision: goal.requiredExternalDecision,
|
|
1007
|
+
message: blockerEvent
|
|
1008
|
+
? `Blocked on repeated external authorization. Required decision: ${goal.requiredExternalDecision}.`
|
|
1009
|
+
: undefined,
|
|
367
1010
|
});
|
|
368
1011
|
return plan;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
export async function recordFinalReviewBlockers(cwd, options) {
|
|
1015
|
+
return withUltragoalMutationLock(cwd, async () => {
|
|
1016
|
+
const plan = await readUltragoalPlan(cwd);
|
|
1017
|
+
const goal = plan.goals.find((candidate) => candidate.id === options.goalId);
|
|
1018
|
+
if (!goal)
|
|
1019
|
+
throw new UltragoalError(`Unknown ultragoal id: ${options.goalId}`);
|
|
1020
|
+
assertNonEmpty(options.evidence, '--evidence');
|
|
1021
|
+
if (goal.status !== 'in_progress') {
|
|
1022
|
+
throw new UltragoalError(`Cannot record final review blockers for ${goal.id} while it is ${goal.status}; start or resume the ultragoal first.`);
|
|
1023
|
+
}
|
|
1024
|
+
if (!isFinalRunCompletionCandidate(plan, goal)) {
|
|
1025
|
+
throw new UltragoalError(`Cannot record final review blockers for ${goal.id}; it is not the only unresolved ultragoal story.`);
|
|
1026
|
+
}
|
|
1027
|
+
const now = iso(options.now);
|
|
372
1028
|
const expectedObjective = expectedCodexObjective(plan, goal);
|
|
373
1029
|
const aggregateMode = codexGoalMode(plan) === 'aggregate';
|
|
374
|
-
const
|
|
375
|
-
const snapshot = options.codexGoal === undefined ? null : parseCodexGoalSnapshot(options.codexGoal);
|
|
376
|
-
const reconciliation = reconcileCodexGoalSnapshot(snapshot, {
|
|
1030
|
+
const reconciliation = reconcileCodexGoalSnapshot(options.codexGoal === undefined ? null : parseCodexGoalSnapshot(options.codexGoal), {
|
|
377
1031
|
expectedObjective,
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
: ['complete'],
|
|
1032
|
+
acceptedObjectives: aggregateMode ? compatibleCodexObjectives(plan) : undefined,
|
|
1033
|
+
allowedStatuses: ['active'],
|
|
381
1034
|
requireSnapshot: true,
|
|
382
|
-
requireComplete:
|
|
1035
|
+
requireComplete: false,
|
|
383
1036
|
});
|
|
384
1037
|
if (!reconciliation.ok) {
|
|
385
|
-
|
|
386
|
-
&& snapshot.status === 'complete'
|
|
387
|
-
&& Boolean(snapshot.objective)
|
|
388
|
-
&& normalizeObjective(snapshot.objective ?? '') !== normalizeObjective(expectedObjective)
|
|
389
|
-
&& await canReconcileCompletedTaskScopedAggregateSnapshot(cwd, plan, goal, snapshot.objective ?? '', options.evidence);
|
|
390
|
-
if (completedTaskScopedAggregateSnapshot) {
|
|
391
|
-
aggregateCompletion = {
|
|
392
|
-
status: 'complete',
|
|
393
|
-
completedAt: now,
|
|
394
|
-
evidence: assertNonEmpty(options.evidence, '--evidence'),
|
|
395
|
-
codexGoal: options.codexGoal,
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
else {
|
|
399
|
-
const taskScopedRequirement = aggregateMode && snapshot?.status === 'complete' && Boolean(snapshot.objective)
|
|
400
|
-
? ' Completed task-scoped aggregate reconciliation requires the checkpoint goal to be the active in-progress OMX goal, evidence that names that active OMX goal id, names .omx/ultragoal/goals.json or ledger.jsonl, includes completed implementation plus validation/review evidence, and a get_goal objective that maps to the ultragoal brief/artifact.'
|
|
401
|
-
: '';
|
|
402
|
-
const remediation = reconciliation.snapshot.available
|
|
403
|
-
&& reconciliation.snapshot.status === 'complete'
|
|
404
|
-
&& Boolean(reconciliation.snapshot.objective)
|
|
405
|
-
&& normalizeObjective(reconciliation.snapshot.objective ?? '') !== normalizeObjective(expectedObjective)
|
|
406
|
-
? ` ${buildCompletedLegacyGoalRemediation(goal)}`
|
|
407
|
-
: '';
|
|
408
|
-
throw new UltragoalError(`${formatCodexGoalReconciliation(reconciliation)}${taskScopedRequirement}${remediation}`);
|
|
409
|
-
}
|
|
1038
|
+
throw new UltragoalError(formatCodexGoalReconciliation(reconciliation));
|
|
410
1039
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
1040
|
+
const addedGoal = appendGoalToPlan(plan, { ...options, now: options.now });
|
|
1041
|
+
goal.status = 'review_blocked';
|
|
1042
|
+
goal.reviewBlockedAt = now;
|
|
1043
|
+
goal.updatedAt = now;
|
|
1044
|
+
goal.completedAt = undefined;
|
|
1045
|
+
goal.failedAt = undefined;
|
|
1046
|
+
goal.failureReason = undefined;
|
|
1047
|
+
goal.evidence = options.evidence;
|
|
419
1048
|
if (plan.activeGoalId === goal.id)
|
|
420
1049
|
delete plan.activeGoalId;
|
|
421
1050
|
plan.updatedAt = now;
|
|
422
1051
|
await writePlan(cwd, plan);
|
|
423
1052
|
await appendLedger(cwd, {
|
|
424
1053
|
ts: now,
|
|
425
|
-
event: '
|
|
1054
|
+
event: 'final_review_failed',
|
|
426
1055
|
goalId: goal.id,
|
|
427
1056
|
status: goal.status,
|
|
428
1057
|
evidence: options.evidence,
|
|
429
1058
|
codexGoal: options.codexGoal,
|
|
430
|
-
|
|
431
|
-
|
|
1059
|
+
message: aggregateMode
|
|
1060
|
+
? 'Final aggregate code-review was not clean; blocker story was appended while Codex goal remains active.'
|
|
1061
|
+
: 'Final per-story code-review was not clean; blocker story was appended and may require an available Codex goal context.',
|
|
432
1062
|
});
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
plan.updatedAt = now;
|
|
452
|
-
await writePlan(cwd, plan);
|
|
453
|
-
await appendLedger(cwd, {
|
|
454
|
-
ts: now,
|
|
455
|
-
event: options.status === 'complete' ? 'goal_completed' : 'goal_failed',
|
|
456
|
-
goalId: goal.id,
|
|
457
|
-
status: goal.status,
|
|
458
|
-
evidence: options.evidence,
|
|
459
|
-
codexGoal: options.codexGoal,
|
|
460
|
-
qualityGate,
|
|
461
|
-
});
|
|
462
|
-
return plan;
|
|
463
|
-
}
|
|
464
|
-
export async function recordFinalReviewBlockers(cwd, options) {
|
|
465
|
-
const plan = await readUltragoalPlan(cwd);
|
|
466
|
-
const goal = plan.goals.find((candidate) => candidate.id === options.goalId);
|
|
467
|
-
if (!goal)
|
|
468
|
-
throw new UltragoalError(`Unknown ultragoal id: ${options.goalId}`);
|
|
469
|
-
assertNonEmpty(options.evidence, '--evidence');
|
|
470
|
-
if (goal.status !== 'in_progress') {
|
|
471
|
-
throw new UltragoalError(`Cannot record final review blockers for ${goal.id} while it is ${goal.status}; start or resume the ultragoal first.`);
|
|
472
|
-
}
|
|
473
|
-
if (!isFinalRunCompletionCandidate(plan, goal)) {
|
|
474
|
-
throw new UltragoalError(`Cannot record final review blockers for ${goal.id}; it is not the only unresolved ultragoal story.`);
|
|
475
|
-
}
|
|
476
|
-
const now = iso(options.now);
|
|
477
|
-
const expectedObjective = expectedCodexObjective(plan, goal);
|
|
478
|
-
const aggregateMode = codexGoalMode(plan) === 'aggregate';
|
|
479
|
-
const reconciliation = reconcileCodexGoalSnapshot(options.codexGoal === undefined ? null : parseCodexGoalSnapshot(options.codexGoal), {
|
|
480
|
-
expectedObjective,
|
|
481
|
-
allowedStatuses: ['active'],
|
|
482
|
-
requireSnapshot: true,
|
|
483
|
-
requireComplete: false,
|
|
484
|
-
});
|
|
485
|
-
if (!reconciliation.ok) {
|
|
486
|
-
throw new UltragoalError(formatCodexGoalReconciliation(reconciliation));
|
|
487
|
-
}
|
|
488
|
-
const addedGoal = appendGoalToPlan(plan, options, now);
|
|
489
|
-
goal.status = 'review_blocked';
|
|
490
|
-
goal.reviewBlockedAt = now;
|
|
491
|
-
goal.updatedAt = now;
|
|
492
|
-
goal.completedAt = undefined;
|
|
493
|
-
goal.failedAt = undefined;
|
|
494
|
-
goal.failureReason = undefined;
|
|
495
|
-
goal.evidence = options.evidence;
|
|
496
|
-
if (plan.activeGoalId === goal.id)
|
|
497
|
-
delete plan.activeGoalId;
|
|
498
|
-
plan.updatedAt = now;
|
|
499
|
-
await writePlan(cwd, plan);
|
|
500
|
-
await appendLedger(cwd, {
|
|
501
|
-
ts: now,
|
|
502
|
-
event: 'final_review_failed',
|
|
503
|
-
goalId: goal.id,
|
|
504
|
-
status: goal.status,
|
|
505
|
-
evidence: options.evidence,
|
|
506
|
-
codexGoal: options.codexGoal,
|
|
507
|
-
message: aggregateMode
|
|
508
|
-
? 'Final aggregate code-review was not clean; blocker story was appended while Codex goal remains active.'
|
|
509
|
-
: 'Final per-story code-review was not clean; blocker story was appended and may require a fresh/available Codex goal context.',
|
|
510
|
-
});
|
|
511
|
-
await appendLedger(cwd, {
|
|
512
|
-
ts: now,
|
|
513
|
-
event: 'goal_added',
|
|
514
|
-
goalId: addedGoal.id,
|
|
515
|
-
status: addedGoal.status,
|
|
516
|
-
evidence: options.evidence,
|
|
517
|
-
message: addedGoal.title,
|
|
518
|
-
});
|
|
519
|
-
await appendLedger(cwd, {
|
|
520
|
-
ts: now,
|
|
521
|
-
event: 'goal_review_blocked',
|
|
522
|
-
goalId: goal.id,
|
|
523
|
-
status: goal.status,
|
|
524
|
-
evidence: options.evidence,
|
|
525
|
-
codexGoal: options.codexGoal,
|
|
1063
|
+
await appendLedger(cwd, {
|
|
1064
|
+
ts: now,
|
|
1065
|
+
event: 'goal_added',
|
|
1066
|
+
goalId: addedGoal.id,
|
|
1067
|
+
status: addedGoal.status,
|
|
1068
|
+
evidence: options.evidence,
|
|
1069
|
+
message: addedGoal.title,
|
|
1070
|
+
});
|
|
1071
|
+
await appendLedger(cwd, {
|
|
1072
|
+
ts: now,
|
|
1073
|
+
event: 'goal_review_blocked',
|
|
1074
|
+
goalId: goal.id,
|
|
1075
|
+
status: goal.status,
|
|
1076
|
+
evidence: options.evidence,
|
|
1077
|
+
codexGoal: options.codexGoal,
|
|
1078
|
+
});
|
|
1079
|
+
return { plan, blockedGoal: goal, addedGoal };
|
|
526
1080
|
});
|
|
527
|
-
return { plan, blockedGoal: goal, addedGoal };
|
|
528
1081
|
}
|
|
529
1082
|
export function buildCodexGoalInstruction(goal, plan) {
|
|
530
1083
|
if (codexGoalMode(plan) === 'aggregate')
|
|
@@ -546,7 +1099,8 @@ function buildPerStoryCodexGoalInstruction(goal, plan) {
|
|
|
546
1099
|
'Codex goal integration constraints:',
|
|
547
1100
|
'- First call get_goal. If no active goal exists, call create_goal with the payload below.',
|
|
548
1101
|
'- If a different active Codex goal exists, finish/checkpoint that goal before starting this ultragoal.',
|
|
549
|
-
'-
|
|
1102
|
+
'- Ultragoal cannot call /goal clear from the model/shell tool surface. For another per-story goal in the same session/thread after a completed Codex goal, manually run /goal clear in the Codex UI before creating the next goal.',
|
|
1103
|
+
'- If get_goal returns a different completed legacy/thread goal and create_goal rejects because this thread already has a completed goal, continue only from a Codex goal context with no active/completed conflicting goal in the same repo/worktree and create the payload there.',
|
|
550
1104
|
`- To preserve the durable ledger before switching threads, record the non-terminal blocker without failing this goal: omx ultragoal checkpoint --goal-id ${goal.id} --status blocked --evidence "<completed legacy Codex goal blocks create_goal in this thread>" --codex-goal-json "<get_goal JSON or path>"`,
|
|
551
1105
|
'- Work only this goal until its completion audit passes.',
|
|
552
1106
|
finalStory
|
|
@@ -559,7 +1113,7 @@ function buildPerStoryCodexGoalInstruction(goal, plan) {
|
|
|
559
1113
|
? ` omx ultragoal record-review-blockers --goal-id ${goal.id} --title "Resolve final code-review blockers" --objective "<blocker-resolution objective>" --evidence "<review findings>" --codex-goal-json "<active get_goal JSON or path>"`
|
|
560
1114
|
: ` omx ultragoal checkpoint --goal-id ${goal.id} --status complete --evidence "<tests/files/PR evidence>" --codex-goal-json "<fresh get_goal JSON or path>"`,
|
|
561
1115
|
finalStory
|
|
562
|
-
? '- In legacy per-story mode, the blocker story may require
|
|
1116
|
+
? '- In legacy per-story mode, the blocker story may require an available Codex goal context because this story remains an active incomplete Codex goal; do not claim it is complete.'
|
|
563
1117
|
: null,
|
|
564
1118
|
finalStory
|
|
565
1119
|
? '- If final $code-review is clean (APPROVE + CLEAR), call update_goal({status: "complete"}), call get_goal again, then checkpoint with --quality-gate-json:'
|
|
@@ -592,6 +1146,7 @@ function buildAggregateCodexGoalInstruction(goal, plan) {
|
|
|
592
1146
|
'- First call get_goal. If no active goal exists, call create_goal with the aggregate payload below.',
|
|
593
1147
|
'- If get_goal reports the same aggregate objective as active, continue this OMX story without creating a new Codex goal.',
|
|
594
1148
|
'- If a different active or incomplete Codex goal exists, finish/checkpoint that goal before starting this ultragoal; do not replace hidden Codex state from the shell.',
|
|
1149
|
+
'- Ultragoal does not call /goal clear. After a completed aggregate run, manually run /goal clear in the Codex UI before starting another ultragoal run in the same session/thread.',
|
|
595
1150
|
finalStory
|
|
596
1151
|
? '- This is the final pending story: run the mandatory final ai-slop-cleaner pass, rerun verification, and run $code-review before any update_goal call.'
|
|
597
1152
|
: '- This is not the final story: do not call update_goal yet; the aggregate Codex goal must remain active while later OMX stories remain.',
|