@gajae-code/coding-agent 0.6.5 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/dist/types/async/job-manager.d.ts +3 -1
- package/dist/types/cli/daemon-cli.d.ts +25 -0
- package/dist/types/cli/notify-cli.d.ts +23 -0
- package/dist/types/cli/setup-cli.d.ts +20 -1
- package/dist/types/commands/daemon.d.ts +41 -0
- package/dist/types/commands/notify.d.ts +41 -0
- package/dist/types/config/model-profile-activation.d.ts +12 -0
- package/dist/types/config/model-profiles.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +38 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/daemon/builtin.d.ts +20 -0
- package/dist/types/daemon/control-types.d.ts +57 -0
- package/dist/types/daemon/runtime.d.ts +25 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +2 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +15 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +14 -0
- package/dist/types/modes/components/oauth-selector.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/types.d.ts +7 -1
- package/dist/types/notifications/config-commands.d.ts +26 -0
- package/dist/types/notifications/config.d.ts +61 -0
- package/dist/types/notifications/helpers.d.ts +55 -0
- package/dist/types/notifications/html-format.d.ts +62 -0
- package/dist/types/notifications/index.d.ts +28 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
- package/dist/types/notifications/telegram-cli.d.ts +19 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
- package/dist/types/notifications/telegram-daemon.d.ts +276 -0
- package/dist/types/notifications/telegram-reference.d.ts +111 -0
- package/dist/types/notifications/threaded-inbound.d.ts +58 -0
- package/dist/types/notifications/threaded-render.d.ts +66 -0
- package/dist/types/notifications/topic-registry.d.ts +67 -0
- package/dist/types/rlm/index.d.ts +12 -0
- package/dist/types/session/agent-session.d.ts +39 -2
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/setup/credential-auto-import.d.ts +63 -0
- package/dist/types/setup/credential-import.d.ts +3 -0
- package/dist/types/setup/host-plugin-setup.d.ts +39 -0
- package/dist/types/tools/ask-answer-registry.d.ts +13 -0
- package/dist/types/tools/index.d.ts +18 -0
- package/dist/types/tools/subagent.d.ts +3 -0
- package/package.json +7 -7
- package/scripts/build-binary.ts +3 -0
- package/src/async/job-manager.ts +5 -1
- package/src/cli/daemon-cli.ts +122 -0
- package/src/cli/notify-cli.ts +274 -0
- package/src/cli/setup-cli.ts +173 -84
- package/src/cli.ts +3 -3
- package/src/commands/daemon.ts +47 -0
- package/src/commands/notify.ts +61 -0
- package/src/commands/setup.ts +11 -1
- package/src/config/model-profile-activation.ts +74 -5
- package/src/config/model-profiles.ts +7 -4
- package/src/config/model-registry.ts +6 -3
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +29 -0
- package/src/coordinator/contract.ts +3 -0
- package/src/coordinator-mcp/server.ts +270 -1
- package/src/daemon/builtin.ts +46 -0
- package/src/daemon/control-types.ts +65 -0
- package/src/daemon/runtime.ts +51 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
- package/src/edit/modes/replace.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +12 -4
- package/src/gjc-runtime/launch-tmux.ts +10 -2
- package/src/gjc-runtime/state-runtime.ts +18 -4
- package/src/gjc-runtime/state-writer.ts +8 -8
- package/src/gjc-runtime/tmux-common.ts +8 -0
- package/src/gjc-runtime/tmux-sessions.ts +8 -1
- package/src/gjc-runtime/ultragoal-guard.ts +57 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +105 -19
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -2
- package/src/gjc-runtime/workflow-manifest.ts +11 -1
- package/src/goals/tools/goal-tool.ts +11 -2
- package/src/hashline/hash.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +9 -7
- package/src/main.ts +30 -0
- package/src/modes/acp/acp-event-mapper.ts +1 -0
- package/src/modes/components/hook-editor.ts +7 -2
- package/src/modes/components/oauth-selector.ts +19 -0
- package/src/modes/controllers/event-controller.ts +20 -0
- package/src/modes/controllers/selector-controller.ts +80 -17
- package/src/modes/interactive-mode.ts +6 -2
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/shared/agent-wire/event-contract.ts +1 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
- package/src/modes/shared/agent-wire/event-observation.ts +16 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
- package/src/modes/types.ts +7 -1
- package/src/modes/utils/ui-helpers.ts +23 -0
- package/src/notifications/config-commands.ts +50 -0
- package/src/notifications/config.ts +107 -0
- package/src/notifications/helpers.ts +135 -0
- package/src/notifications/html-format.ts +389 -0
- package/src/notifications/index.ts +700 -0
- package/src/notifications/rate-limit-pool.ts +179 -0
- package/src/notifications/telegram-cli.ts +194 -0
- package/src/notifications/telegram-daemon-cli.ts +74 -0
- package/src/notifications/telegram-daemon-control.ts +370 -0
- package/src/notifications/telegram-daemon.ts +1370 -0
- package/src/notifications/telegram-reference.ts +335 -0
- package/src/notifications/threaded-inbound.ts +80 -0
- package/src/notifications/threaded-render.ts +155 -0
- package/src/notifications/topic-registry.ts +133 -0
- package/src/rlm/index.ts +19 -0
- package/src/sdk.ts +16 -0
- package/src/session/agent-session.ts +113 -3
- package/src/session/auth-storage.ts +3 -0
- package/src/session/session-dump-format.ts +43 -2
- package/src/session/session-manager.ts +39 -5
- package/src/setup/credential-auto-import.ts +258 -0
- package/src/setup/credential-import.ts +17 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
- package/src/setup/host-plugin-setup.ts +142 -0
- package/src/slash-commands/builtin-registry.ts +4 -1
- package/src/task/executor.ts +5 -1
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/ask.ts +77 -6
- package/src/tools/image-gen.ts +5 -8
- package/src/tools/index.ts +19 -0
- package/src/tools/inspect-image.ts +16 -11
- package/src/tools/subagent-render.ts +7 -0
- package/src/tools/subagent.ts +38 -7
|
@@ -32,6 +32,7 @@ import type { PythonResult } from "../../eval/py/executor";
|
|
|
32
32
|
import type { BashResult } from "../../exec/bash-executor";
|
|
33
33
|
import type { ExecOptions, ExecResult } from "../../exec/exec";
|
|
34
34
|
import type { CustomEditor } from "../../modes/components/custom-editor";
|
|
35
|
+
import type { WorkflowGateEmitter } from "../../modes/shared/agent-wire/unattended-session";
|
|
35
36
|
import type { Theme } from "../../modes/theme/theme";
|
|
36
37
|
import type { CustomMessage } from "../../session/messages";
|
|
37
38
|
import type { ReadonlySessionManager, SessionManager } from "../../session/session-manager";
|
|
@@ -310,6 +311,11 @@ export interface ExtensionContext {
|
|
|
310
311
|
getSystemPrompt(): string[];
|
|
311
312
|
/** @deprecated Use hasPendingMessages() instead */
|
|
312
313
|
hasQueuedMessages(): boolean;
|
|
314
|
+
/**
|
|
315
|
+
* Unattended workflow-gate bridge. Present only when the session runs in
|
|
316
|
+
* unattended/RPC mode; `undefined` in interactive/TUI mode (notify-only).
|
|
317
|
+
*/
|
|
318
|
+
workflowGate?: WorkflowGateEmitter;
|
|
313
319
|
}
|
|
314
320
|
|
|
315
321
|
/**
|
|
@@ -1234,6 +1240,8 @@ export interface ExtensionContextActions {
|
|
|
1234
1240
|
getContextUsage: () => ContextUsage | undefined;
|
|
1235
1241
|
compact: (instructionsOrOptions?: string | CompactOptions) => Promise<void>;
|
|
1236
1242
|
getSystemPrompt: () => string[];
|
|
1243
|
+
/** Unattended workflow-gate bridge (present only in unattended/RPC mode). */
|
|
1244
|
+
getWorkflowGate?: () => WorkflowGateEmitter | undefined;
|
|
1237
1245
|
}
|
|
1238
1246
|
|
|
1239
1247
|
/** Actions for ExtensionCommandContext (ctx.* in command handlers). */
|
|
@@ -326,13 +326,21 @@ async function persistEnvelope(
|
|
|
326
326
|
payload.version ??= WORKFLOW_STATE_VERSION;
|
|
327
327
|
payload.active ??= true;
|
|
328
328
|
payload.current_phase ??= "interviewing";
|
|
329
|
-
|
|
329
|
+
const expectedRevision = existingStateRevision(envelope);
|
|
330
|
+
const writeResult = await writeGuardedWorkflowEnvelopeAtomic(statePath, payload, {
|
|
330
331
|
cwd,
|
|
331
332
|
policy: "source",
|
|
332
|
-
expectedRevision
|
|
333
|
+
expectedRevision,
|
|
333
334
|
receipt: { cwd, skill: "deep-interview", owner: "gjc-runtime", command, sessionId, nowIso: now },
|
|
334
335
|
audit: { category: "state", verb: "write", owner: "gjc-runtime", skill: "deep-interview", sessionId },
|
|
335
336
|
});
|
|
337
|
+
// Reflect the freshly written revision back onto the in-memory envelope so a
|
|
338
|
+
// follow-up HUD sync derives its `sourceRevision` from the persisted revision
|
|
339
|
+
// (not the stale pre-write value), otherwise the active-state writer treats the
|
|
340
|
+
// newer HUD as stale and skips it (e.g. dropping the ambiguity chip after scoring).
|
|
341
|
+
if (writeResult.written && typeof expectedRevision === "number") {
|
|
342
|
+
(envelope as Record<string, unknown>).state_revision = expectedRevision + 1;
|
|
343
|
+
}
|
|
336
344
|
await writeSessionActivityMarker(cwd, sessionId, { writer: "deep-interview-recorder", path: statePath });
|
|
337
345
|
}
|
|
338
346
|
|
|
@@ -354,8 +362,8 @@ async function syncRecorderHud(
|
|
|
354
362
|
phase,
|
|
355
363
|
sessionId,
|
|
356
364
|
source: "gjc-runtime-deep-interview-recorder",
|
|
357
|
-
hud: deriveDeepInterviewHud(envelope as Record<string, unknown>, { phase }),
|
|
358
|
-
sourceRevision: existingStateRevision(envelope),
|
|
365
|
+
hud: deriveDeepInterviewHud(normalizeDeepInterviewEnvelope(envelope) as Record<string, unknown>, { phase }),
|
|
366
|
+
sourceRevision: (existingStateRevision(envelope) ?? 0) + 1,
|
|
359
367
|
});
|
|
360
368
|
}
|
|
361
369
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { safeStderrWrite } from "@gajae-code/utils";
|
|
4
|
+
import { VERSION } from "@gajae-code/utils/dirs";
|
|
4
5
|
import type { Args } from "../cli/args";
|
|
5
6
|
import { tmuxRuntimeSessionPath } from "./session-layout";
|
|
6
7
|
import { GJC_COORDINATOR_SESSION_ID_ENV, GJC_COORDINATOR_SESSION_STATE_FILE_ENV } from "./session-state-sidecar";
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
type GjcTmuxProfileCommand,
|
|
17
18
|
resolveGjcTmuxCommand,
|
|
18
19
|
} from "./tmux-common";
|
|
19
|
-
import { findGjcTmuxSessionByName, findGjcTmuxSessionByScope } from "./tmux-sessions";
|
|
20
|
+
import { findGjcTmuxSessionByName, findGjcTmuxSessionByScope, type GjcTmuxSessionStatus } from "./tmux-sessions";
|
|
20
21
|
|
|
21
22
|
export {
|
|
22
23
|
buildGjcTmuxProfileCommands,
|
|
@@ -88,6 +89,9 @@ export interface TmuxLaunchPlan {
|
|
|
88
89
|
function explicitTmuxSessionName(env: NodeJS.ProcessEnv): string | undefined {
|
|
89
90
|
return env.GJC_TMUX_SESSION?.trim() || undefined;
|
|
90
91
|
}
|
|
92
|
+
function hasCurrentGjcVersion(session: GjcTmuxSessionStatus | undefined): boolean {
|
|
93
|
+
return session?.version === VERSION;
|
|
94
|
+
}
|
|
91
95
|
|
|
92
96
|
function findExistingSessionForLaunch(context: {
|
|
93
97
|
env: NodeJS.ProcessEnv;
|
|
@@ -96,7 +100,8 @@ function findExistingSessionForLaunch(context: {
|
|
|
96
100
|
}): string | undefined {
|
|
97
101
|
const explicit = explicitTmuxSessionName(context.env);
|
|
98
102
|
if (explicit) return findGjcTmuxSessionByName(explicit, context.env)?.name;
|
|
99
|
-
|
|
103
|
+
const scoped = findGjcTmuxSessionByScope(context.project, context.branch, context.env);
|
|
104
|
+
return hasCurrentGjcVersion(scoped) ? scoped?.name : undefined;
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
export interface GjcTmuxProfileResult {
|
|
@@ -116,6 +121,7 @@ export interface GjcTmuxProfileContext {
|
|
|
116
121
|
project?: string | null;
|
|
117
122
|
sessionId?: string | null;
|
|
118
123
|
sessionStateFile?: string | null;
|
|
124
|
+
version?: string | null;
|
|
119
125
|
}
|
|
120
126
|
|
|
121
127
|
interface CommandResolutionContext {
|
|
@@ -195,6 +201,7 @@ export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProf
|
|
|
195
201
|
project: context.project ?? null,
|
|
196
202
|
sessionId: context.sessionId ?? env[GJC_COORDINATOR_SESSION_ID_ENV] ?? null,
|
|
197
203
|
sessionStateFile: context.sessionStateFile ?? env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV] ?? null,
|
|
204
|
+
version: context.version ?? null,
|
|
198
205
|
});
|
|
199
206
|
if (commands.length === 0) return { skipped: true, commands: [], failures: [] };
|
|
200
207
|
const spawnSync = context.spawnSync ?? defaultSpawnSync;
|
|
@@ -457,6 +464,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
|
|
|
457
464
|
project: plan.project,
|
|
458
465
|
sessionId: plan.sessionId ?? null,
|
|
459
466
|
sessionStateFile: plan.sessionStateFile ?? null,
|
|
467
|
+
version: VERSION,
|
|
460
468
|
});
|
|
461
469
|
const ownershipFailure = profile.failures.find(item => item.command.args.includes("@gjc-profile"));
|
|
462
470
|
if (ownershipFailure) {
|
|
@@ -818,7 +818,7 @@ async function writeJsonAtomic(
|
|
|
818
818
|
toPhase?: string;
|
|
819
819
|
owner?: WorkflowStateMutationOwner;
|
|
820
820
|
},
|
|
821
|
-
): Promise<{ warning?: string; stamped: Record<string, unknown
|
|
821
|
+
): Promise<{ warning?: string; stamped: Record<string, unknown>; revision: number }> {
|
|
822
822
|
const warning = options?.skill
|
|
823
823
|
? await warnAndAuditOutOfBandIfNeeded(cwd, options.sessionId, filePath, options.skill, {
|
|
824
824
|
mutationId: options.mutationId,
|
|
@@ -832,7 +832,7 @@ async function writeJsonAtomic(
|
|
|
832
832
|
// writer lock; do not enforce an optimistic `expectedRevision` here (tamper
|
|
833
833
|
// detection is handled by warnAndAuditOutOfBandIfNeeded above, and a forced
|
|
834
834
|
// write must succeed over corrupt/missing prior state).
|
|
835
|
-
await writeGuardedWorkflowEnvelopeAtomic(filePath, value, {
|
|
835
|
+
const writeResult = await writeGuardedWorkflowEnvelopeAtomic(filePath, value, {
|
|
836
836
|
cwd,
|
|
837
837
|
policy: "source",
|
|
838
838
|
audit: {
|
|
@@ -847,7 +847,11 @@ async function writeJsonAtomic(
|
|
|
847
847
|
forced: options?.force ?? false,
|
|
848
848
|
},
|
|
849
849
|
});
|
|
850
|
-
|
|
850
|
+
// `writeResult.revision` is computed inside the writer lock, so it is the revision this
|
|
851
|
+
// write actually owns. Prefer it over a post-lock file re-read, which a concurrent writer
|
|
852
|
+
// could have advanced before the read — that race could otherwise let this payload be
|
|
853
|
+
// published with another writer's newer revision.
|
|
854
|
+
return { warning, stamped: (await readJsonFile(filePath)) ?? {}, revision: writeResult.revision };
|
|
851
855
|
}
|
|
852
856
|
|
|
853
857
|
function parseFieldsFlag(args: readonly string[]): StateProjectionField[] | undefined {
|
|
@@ -1399,7 +1403,11 @@ async function handleWrite(
|
|
|
1399
1403
|
const validation = validateWorkflowStateEnvelope(mode, merged);
|
|
1400
1404
|
if (!validation.valid) throw new StateCommandError(2, validation.error ?? `invalid ${mode} state envelope`);
|
|
1401
1405
|
|
|
1402
|
-
const {
|
|
1406
|
+
const {
|
|
1407
|
+
warning: outOfBandWarning,
|
|
1408
|
+
stamped,
|
|
1409
|
+
revision: stampedRevision,
|
|
1410
|
+
} = await writeJsonAtomic(cwd, filePath, merged, "write", {
|
|
1403
1411
|
sessionId,
|
|
1404
1412
|
skill: mode,
|
|
1405
1413
|
mutationId,
|
|
@@ -1411,6 +1419,12 @@ async function handleWrite(
|
|
|
1411
1419
|
|
|
1412
1420
|
const phase = typeof merged.current_phase === "string" ? merged.current_phase : undefined;
|
|
1413
1421
|
const active = merged.active !== false;
|
|
1422
|
+
// Reflect the lock-owned mode-state revision onto the in-memory payload so the active-state/HUD
|
|
1423
|
+
// sync derives a `sourceRevision` from the revision this write actually owns (computed inside the
|
|
1424
|
+
// writer lock), not the stale pre-write value or a post-lock re-read a concurrent writer could
|
|
1425
|
+
// have advanced; otherwise the active-state writer stale-skips the update and the mirror keeps the
|
|
1426
|
+
// prior phase (e.g. staying "interviewing" after a "handoff" write).
|
|
1427
|
+
merged.state_revision = stampedRevision;
|
|
1414
1428
|
await syncWorkflowSkillState({ cwd, mode, sessionId, threadId, turnId, active, phase, payload: merged, receipt });
|
|
1415
1429
|
await touchStateActivityMarker(cwd, sessionId, filePath);
|
|
1416
1430
|
|
|
@@ -99,8 +99,8 @@ export interface GuardedStateWriterOptions extends StateWriterOptions {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
export type GuardedWriteResult =
|
|
102
|
-
| { path: string; written: true }
|
|
103
|
-
| { path: string; written: false; reason: "stale-skip" };
|
|
102
|
+
| { path: string; written: true; revision: number }
|
|
103
|
+
| { path: string; written: false; reason: "stale-skip"; revision: number };
|
|
104
104
|
|
|
105
105
|
export interface StateWriterOptions {
|
|
106
106
|
cwd?: string;
|
|
@@ -511,13 +511,13 @@ async function writeGuardedResolvedJsonAtomic(
|
|
|
511
511
|
const next = stampStateRevision(withWorkflowReceipt(value, buildReceipt(options)), currentRevision + 1);
|
|
512
512
|
await atomicWrite(filePath, jsonText(next));
|
|
513
513
|
await maybeAudit(filePath, options);
|
|
514
|
-
return { path: filePath, written: true };
|
|
514
|
+
return { path: filePath, written: true, revision: currentRevision + 1 };
|
|
515
515
|
}
|
|
516
516
|
|
|
517
517
|
const incomingSourceRevision =
|
|
518
518
|
options.sourceRevision ?? (isPlainObject(value) ? persistedStateRevision(value) : 0);
|
|
519
519
|
if (current !== undefined && incomingSourceRevision <= persistedSourceRevision(current)) {
|
|
520
|
-
return { path: filePath, written: false, reason: "stale-skip" };
|
|
520
|
+
return { path: filePath, written: false, reason: "stale-skip", revision: currentRevision };
|
|
521
521
|
}
|
|
522
522
|
const next = stampStateRevision(
|
|
523
523
|
withWorkflowReceipt(value, buildReceipt(options)),
|
|
@@ -526,7 +526,7 @@ async function writeGuardedResolvedJsonAtomic(
|
|
|
526
526
|
);
|
|
527
527
|
await atomicWrite(filePath, jsonText(next));
|
|
528
528
|
await maybeAudit(filePath, options);
|
|
529
|
-
return { path: filePath, written: true };
|
|
529
|
+
return { path: filePath, written: true, revision: currentRevision + 1 };
|
|
530
530
|
},
|
|
531
531
|
options.lock,
|
|
532
532
|
);
|
|
@@ -574,13 +574,13 @@ export async function writeGuardedWorkflowEnvelopeAtomic(
|
|
|
574
574
|
}
|
|
575
575
|
await atomicWrite(filePath, jsonText(next));
|
|
576
576
|
await maybeAudit(filePath, options);
|
|
577
|
-
return { path: filePath, written: true };
|
|
577
|
+
return { path: filePath, written: true, revision: currentRevision + 1 };
|
|
578
578
|
}
|
|
579
579
|
|
|
580
580
|
const incomingSourceRevision =
|
|
581
581
|
options.sourceRevision ?? (isPlainObject(value) ? persistedStateRevision(value) : 0);
|
|
582
582
|
if (current !== undefined && incomingSourceRevision <= persistedSourceRevision(current)) {
|
|
583
|
-
return { path: filePath, written: false, reason: "stale-skip" };
|
|
583
|
+
return { path: filePath, written: false, reason: "stale-skip", revision: currentRevision };
|
|
584
584
|
}
|
|
585
585
|
const next = stampWorkflowEnvelopeRevisionAndChecksum(
|
|
586
586
|
value,
|
|
@@ -599,7 +599,7 @@ export async function writeGuardedWorkflowEnvelopeAtomic(
|
|
|
599
599
|
}
|
|
600
600
|
await atomicWrite(filePath, jsonText(next));
|
|
601
601
|
await maybeAudit(filePath, options);
|
|
602
|
-
return { path: filePath, written: true };
|
|
602
|
+
return { path: filePath, written: true, revision: currentRevision + 1 };
|
|
603
603
|
},
|
|
604
604
|
options.lock,
|
|
605
605
|
);
|
|
@@ -10,6 +10,7 @@ export const GJC_TMUX_BRANCH_SLUG_OPTION = "@gjc-branch-slug";
|
|
|
10
10
|
export const GJC_TMUX_PROJECT_OPTION = "@gjc-project";
|
|
11
11
|
export const GJC_TMUX_SESSION_ID_OPTION = "@gjc-session-id";
|
|
12
12
|
export const GJC_TMUX_SESSION_STATE_FILE_OPTION = "@gjc-session-state-file";
|
|
13
|
+
export const GJC_TMUX_VERSION_OPTION = "@gjc-version";
|
|
13
14
|
|
|
14
15
|
export interface GjcTmuxProfileCommand {
|
|
15
16
|
description: string;
|
|
@@ -101,6 +102,7 @@ export function buildGjcTmuxRequiredProfileCommands(
|
|
|
101
102
|
project?: string | null;
|
|
102
103
|
sessionId?: string | null;
|
|
103
104
|
sessionStateFile?: string | null;
|
|
105
|
+
version?: string | null;
|
|
104
106
|
} = {},
|
|
105
107
|
): GjcTmuxProfileCommand[] {
|
|
106
108
|
const commands: GjcTmuxProfileCommand[] = [
|
|
@@ -134,6 +136,11 @@ export function buildGjcTmuxRequiredProfileCommands(
|
|
|
134
136
|
description: "record GJC session state marker",
|
|
135
137
|
args: ["set-option", "-t", target, GJC_TMUX_SESSION_STATE_FILE_OPTION, metadata.sessionStateFile],
|
|
136
138
|
});
|
|
139
|
+
if (metadata.version)
|
|
140
|
+
commands.push({
|
|
141
|
+
description: "record GJC version identity",
|
|
142
|
+
args: ["set-option", "-t", target, GJC_TMUX_VERSION_OPTION, metadata.version],
|
|
143
|
+
});
|
|
137
144
|
return commands;
|
|
138
145
|
}
|
|
139
146
|
|
|
@@ -146,6 +153,7 @@ export function buildGjcTmuxProfileCommands(
|
|
|
146
153
|
project?: string | null;
|
|
147
154
|
sessionId?: string | null;
|
|
148
155
|
sessionStateFile?: string | null;
|
|
156
|
+
version?: string | null;
|
|
149
157
|
} = {},
|
|
150
158
|
): GjcTmuxProfileCommand[] {
|
|
151
159
|
const commands = buildGjcTmuxRequiredProfileCommands(target, metadata);
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
GJC_TMUX_PROJECT_OPTION,
|
|
11
11
|
GJC_TMUX_SESSION_ID_OPTION,
|
|
12
12
|
GJC_TMUX_SESSION_STATE_FILE_OPTION,
|
|
13
|
+
GJC_TMUX_VERSION_OPTION,
|
|
13
14
|
normalizeTmuxCreatedAt,
|
|
14
15
|
resolveGjcTmuxCommand,
|
|
15
16
|
} from "./tmux-common";
|
|
@@ -26,6 +27,7 @@ export interface GjcTmuxSessionStatus {
|
|
|
26
27
|
project?: string;
|
|
27
28
|
sessionId?: string;
|
|
28
29
|
sessionStateFile?: string;
|
|
30
|
+
version?: string;
|
|
29
31
|
panePids: number[];
|
|
30
32
|
profile?: string;
|
|
31
33
|
}
|
|
@@ -37,6 +39,7 @@ export interface GjcTmuxSessionTagsForGc {
|
|
|
37
39
|
branchSlug?: string;
|
|
38
40
|
sessionId?: string;
|
|
39
41
|
sessionStateFile?: string;
|
|
42
|
+
version?: string;
|
|
40
43
|
createdAt?: string;
|
|
41
44
|
attached?: boolean;
|
|
42
45
|
panePids?: number[];
|
|
@@ -86,6 +89,7 @@ function parseSessionLine(line: string): GjcTmuxSessionStatus | null {
|
|
|
86
89
|
project = "",
|
|
87
90
|
sessionId = "",
|
|
88
91
|
sessionStateFile = "",
|
|
92
|
+
version = "",
|
|
89
93
|
] = line.split("\t");
|
|
90
94
|
if (!name) return null;
|
|
91
95
|
return {
|
|
@@ -105,6 +109,7 @@ function parseSessionLine(line: string): GjcTmuxSessionStatus | null {
|
|
|
105
109
|
profile: profile || undefined,
|
|
106
110
|
sessionId: sessionId || undefined,
|
|
107
111
|
sessionStateFile: sessionStateFile || undefined,
|
|
112
|
+
version: version || undefined,
|
|
108
113
|
};
|
|
109
114
|
}
|
|
110
115
|
|
|
@@ -131,7 +136,7 @@ function runListSessions(format: string, env: NodeJS.ProcessEnv = process.env):
|
|
|
131
136
|
|
|
132
137
|
function listSessionLines(env: NodeJS.ProcessEnv = process.env): string[] {
|
|
133
138
|
return runListSessions(
|
|
134
|
-
`#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}\t#{${GJC_TMUX_PROFILE_OPTION}}\t#{session_key_table}\t#{session_panes}\t#{pane_pid}\t#{${GJC_TMUX_BRANCH_OPTION}}\t#{${GJC_TMUX_BRANCH_SLUG_OPTION}}\t#{${GJC_TMUX_PROJECT_OPTION}}\t#{${GJC_TMUX_SESSION_ID_OPTION}}\t#{${GJC_TMUX_SESSION_STATE_FILE_OPTION}}`,
|
|
139
|
+
`#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}\t#{${GJC_TMUX_PROFILE_OPTION}}\t#{session_key_table}\t#{session_panes}\t#{pane_pid}\t#{${GJC_TMUX_BRANCH_OPTION}}\t#{${GJC_TMUX_BRANCH_SLUG_OPTION}}\t#{${GJC_TMUX_PROJECT_OPTION}}\t#{${GJC_TMUX_SESSION_ID_OPTION}}\t#{${GJC_TMUX_SESSION_STATE_FILE_OPTION}}\t#{${GJC_TMUX_VERSION_OPTION}}`,
|
|
135
140
|
env,
|
|
136
141
|
);
|
|
137
142
|
}
|
|
@@ -265,6 +270,7 @@ function hydrateSessionFromExactOptions(session: GjcTmuxSessionStatus, env: Node
|
|
|
265
270
|
sessionId: session.sessionId ?? readExactOptionForGc(session.name, GJC_TMUX_SESSION_ID_OPTION, env),
|
|
266
271
|
sessionStateFile:
|
|
267
272
|
session.sessionStateFile ?? readExactOptionForGc(session.name, GJC_TMUX_SESSION_STATE_FILE_OPTION, env),
|
|
273
|
+
version: session.version ?? readExactOptionForGc(session.name, GJC_TMUX_VERSION_OPTION, env),
|
|
268
274
|
};
|
|
269
275
|
}
|
|
270
276
|
|
|
@@ -281,6 +287,7 @@ export function readTmuxSessionTagsForGc(
|
|
|
281
287
|
branchSlug: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_SLUG_OPTION, env),
|
|
282
288
|
sessionId: readExactOptionForGc(sessionName, GJC_TMUX_SESSION_ID_OPTION, env),
|
|
283
289
|
sessionStateFile: readExactOptionForGc(sessionName, GJC_TMUX_SESSION_STATE_FILE_OPTION, env),
|
|
290
|
+
version: readExactOptionForGc(sessionName, GJC_TMUX_VERSION_OPTION, env),
|
|
284
291
|
createdAt: session?.createdAt,
|
|
285
292
|
attached: session?.attached,
|
|
286
293
|
panePids: session?.panePids,
|
|
@@ -288,14 +288,15 @@ export function validateCompletionReceipt(input: {
|
|
|
288
288
|
export async function readUltragoalVerificationState(input: {
|
|
289
289
|
cwd: string;
|
|
290
290
|
currentGoal?: CurrentGoalLike | null;
|
|
291
|
+
sessionId?: string | null;
|
|
291
292
|
}): Promise<UltragoalGuardDiagnostic> {
|
|
292
293
|
const currentObjective = input.currentGoal?.objective?.trim() ?? "";
|
|
293
294
|
if (!currentObjective) return { state: "inactive", message: "No current goal objective is active." };
|
|
294
295
|
let plan: UltragoalPlan | null;
|
|
295
296
|
let ledger: UltragoalLedgerEvent[];
|
|
296
297
|
try {
|
|
297
|
-
plan = await readUltragoalPlan(input.cwd);
|
|
298
|
-
ledger = await readUltragoalLedger(input.cwd);
|
|
298
|
+
plan = await readUltragoalPlan(input.cwd, input.sessionId ?? undefined);
|
|
299
|
+
ledger = await readUltragoalLedger(input.cwd, input.sessionId ?? undefined);
|
|
299
300
|
} catch (error) {
|
|
300
301
|
if (currentObjective === DEFAULT_ULTRAGOAL_OBJECTIVE) {
|
|
301
302
|
return {
|
|
@@ -479,6 +480,7 @@ export async function isUltragoalAskBlocked(cwd: string): Promise<UltragoalAskBl
|
|
|
479
480
|
export async function assertCanCompleteCurrentGoal(input: {
|
|
480
481
|
cwd: string;
|
|
481
482
|
currentGoal?: CurrentGoalLike | null;
|
|
483
|
+
sessionId?: string | null;
|
|
482
484
|
}): Promise<void> {
|
|
483
485
|
if (!input.cwd) return;
|
|
484
486
|
const diagnostic = await readUltragoalVerificationState(input);
|
|
@@ -496,3 +498,56 @@ export function isUltragoalBypassPrompt(prompt: string): boolean {
|
|
|
496
498
|
) || /goal[\s\S]{0,80}complete/i.test(normalized)
|
|
497
499
|
);
|
|
498
500
|
}
|
|
501
|
+
export interface UltragoalPauseBlockDiagnostic {
|
|
502
|
+
blocked: boolean;
|
|
503
|
+
reason: string;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* While an Ultragoal run is active, `goal({"op":"pause"})` is only allowed when the
|
|
508
|
+
* current durable Ultragoal state is readable and the latest durable ledger event
|
|
509
|
+
* classifies the current blocker as `human_blocked`. Resolvable blockers must be
|
|
510
|
+
* worked, not parked. Reads fail closed so unreadable durable state or ledger data
|
|
511
|
+
* blocks pause rather than silently allowing a give-up.
|
|
512
|
+
*/
|
|
513
|
+
export async function isUltragoalPauseBlocked(cwd: string): Promise<UltragoalPauseBlockDiagnostic> {
|
|
514
|
+
if (!cwd) return { blocked: false, reason: "No cwd to resolve durable Ultragoal state." };
|
|
515
|
+
const ask = await isUltragoalAskBlocked(cwd);
|
|
516
|
+
if (ask.source === "durable_state_unreadable") {
|
|
517
|
+
return {
|
|
518
|
+
blocked: true,
|
|
519
|
+
reason: `Unable to verify current durable Ultragoal state for pause: ${ask.reason}`,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
if (!ask.active) return { blocked: false, reason: "No active Ultragoal run." };
|
|
523
|
+
let ledger: UltragoalLedgerEvent[];
|
|
524
|
+
try {
|
|
525
|
+
ledger = await readUltragoalLedger(cwd);
|
|
526
|
+
} catch (error) {
|
|
527
|
+
return {
|
|
528
|
+
blocked: true,
|
|
529
|
+
reason: `Unable to read durable Ultragoal ledger: ${error instanceof Error ? error.message : String(error)}`,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
const latest = ledger.at(-1);
|
|
533
|
+
if (latest?.event === "blocker_classified" && latest.classification === "human_blocked") {
|
|
534
|
+
return { blocked: false, reason: "Latest Ultragoal ledger event classifies the blocker as human_blocked." };
|
|
535
|
+
}
|
|
536
|
+
return {
|
|
537
|
+
blocked: true,
|
|
538
|
+
reason:
|
|
539
|
+
"An Ultragoal run is active. Pausing requires the current blocker to be classified human_blocked as the latest ledger event.",
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export async function assertUltragoalPauseAllowed(cwd: string): Promise<void> {
|
|
544
|
+
const diagnostic = await isUltragoalPauseBlocked(cwd);
|
|
545
|
+
if (!diagnostic.blocked) return;
|
|
546
|
+
throw new Error(
|
|
547
|
+
[
|
|
548
|
+
diagnostic.reason,
|
|
549
|
+
"Resolvable blockers must be worked, not paused: investigate, `gjc ultragoal steer --kind add_subgoal`, delegate an executor, or `gjc ultragoal record-review-blockers`.",
|
|
550
|
+
'If the blocker is genuinely human-only, record `gjc ultragoal classify-blocker --classification human_blocked --evidence "<human-only dependency>"` immediately before pausing.',
|
|
551
|
+
].join("\n"),
|
|
552
|
+
);
|
|
553
|
+
}
|
|
@@ -898,32 +898,29 @@ function categorizeComputerChangePath(value: string): UltragoalChangeCategory {
|
|
|
898
898
|
return "other";
|
|
899
899
|
}
|
|
900
900
|
|
|
901
|
-
function
|
|
901
|
+
function isComputerControlSurfaceCategory(category: UltragoalChangeCategory): boolean {
|
|
902
902
|
return (
|
|
903
|
-
|
|
904
|
-
(row.oldPath ? categorizeComputerChangePath(row.oldPath) !== "other" : false)
|
|
903
|
+
category === "code" || category === "generated-binding" || category === "tool" || category === "settings-registry"
|
|
905
904
|
);
|
|
906
905
|
}
|
|
907
906
|
|
|
908
|
-
function
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
const oldCategory = row.oldPath ? categorizeComputerChangePath(row.oldPath) : category;
|
|
913
|
-
return category === "docs-static" && oldCategory === "docs-static";
|
|
914
|
-
});
|
|
907
|
+
function isComputerControlSurfaceChangePath(row: UltragoalChangeSetPath): boolean {
|
|
908
|
+
const category = row.category ?? categorizeComputerChangePath(row.path);
|
|
909
|
+
const oldCategory = row.oldPath ? categorizeComputerChangePath(row.oldPath) : category;
|
|
910
|
+
return isComputerControlSurfaceCategory(category) || isComputerControlSurfaceCategory(oldCategory);
|
|
915
911
|
}
|
|
916
912
|
|
|
917
913
|
function trustedChangeSetRequiresComputerSuite(changeSet: UltragoalChangeSet | undefined): boolean {
|
|
918
914
|
if (!changeSet?.trusted) return false;
|
|
919
|
-
|
|
920
|
-
return changeSet.paths.some(isComputerChangePath);
|
|
915
|
+
return changeSet.paths.some(isComputerControlSurfaceChangePath);
|
|
921
916
|
}
|
|
922
917
|
|
|
923
918
|
function requiresComputerRedTeamSuite(executorQa: JsonObject, changeSet: UltragoalChangeSet | undefined): boolean {
|
|
924
919
|
if (trustedChangeSetRequiresComputerSuite(changeSet)) return true;
|
|
925
920
|
const declaredPaths = Array.isArray(executorQa.changedPaths) ? executorQa.changedPaths : [];
|
|
926
|
-
return declaredPaths.some(
|
|
921
|
+
return declaredPaths.some(
|
|
922
|
+
value => typeof value === "string" && isComputerControlSurfaceCategory(categorizeComputerChangePath(value)),
|
|
923
|
+
);
|
|
927
924
|
}
|
|
928
925
|
|
|
929
926
|
function normalizeAdversarialCaseId(value: string): string {
|
|
@@ -2853,6 +2850,33 @@ export async function recordUltragoalReviewBlockers(input: {
|
|
|
2853
2850
|
return plan;
|
|
2854
2851
|
}
|
|
2855
2852
|
|
|
2853
|
+
export type UltragoalBlockerClassification = "human_blocked" | "resolvable";
|
|
2854
|
+
|
|
2855
|
+
/**
|
|
2856
|
+
* Record an audited blocker triage classification in the durable ledger. A
|
|
2857
|
+
* `human_blocked` classification is the only thing that authorizes
|
|
2858
|
+
* `goal({"op":"pause"})` while an Ultragoal run is active; `resolvable` is an
|
|
2859
|
+
* audit note and never unblocks pause.
|
|
2860
|
+
*/
|
|
2861
|
+
export async function recordUltragoalBlockerClassification(input: {
|
|
2862
|
+
cwd: string;
|
|
2863
|
+
classification: UltragoalBlockerClassification;
|
|
2864
|
+
evidence: string;
|
|
2865
|
+
goalId?: string;
|
|
2866
|
+
}): Promise<UltragoalLedgerEvent> {
|
|
2867
|
+
const evidence = input.evidence.trim();
|
|
2868
|
+
if (!evidence) throw new Error("classify-blocker --evidence is required");
|
|
2869
|
+
if (input.classification !== "human_blocked" && input.classification !== "resolvable") {
|
|
2870
|
+
throw new Error('classify-blocker --classification must be "human_blocked" or "resolvable"');
|
|
2871
|
+
}
|
|
2872
|
+
return appendLedger(input.cwd, {
|
|
2873
|
+
event: "blocker_classified",
|
|
2874
|
+
classification: input.classification,
|
|
2875
|
+
...(input.goalId?.trim() ? { goalId: input.goalId.trim() } : {}),
|
|
2876
|
+
evidence,
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2856
2880
|
type UltragoalReviewMode = "review-only" | "review-start";
|
|
2857
2881
|
type UltragoalReviewContractStrength = "strong" | "thin-derived";
|
|
2858
2882
|
|
|
@@ -2920,11 +2944,33 @@ async function spawnText(
|
|
|
2920
2944
|
}
|
|
2921
2945
|
}
|
|
2922
2946
|
|
|
2923
|
-
async function resolveGitBase(cwd: string, branch?: string): Promise<string> {
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2947
|
+
export async function resolveGitBase(cwd: string, branch?: string): Promise<string> {
|
|
2948
|
+
if (branch) {
|
|
2949
|
+
const exists = await spawnText(["git", "rev-parse", "--verify", branch], { cwd, timeoutMs: 3000 });
|
|
2950
|
+
if (exists.ok) return branch;
|
|
2951
|
+
} else {
|
|
2952
|
+
// Prefer the NEAREST integration base (the branch this work actually forks
|
|
2953
|
+
// from) rather than always `main`. A branch opened against `dev` must be
|
|
2954
|
+
// scoped to `dev`; using a stale `main` sweeps in unrelated trunk history
|
|
2955
|
+
// and mis-attributes other people's changes to this story (e.g. falsely
|
|
2956
|
+
// tripping change-scoped gates). Among existing candidates, pick the one
|
|
2957
|
+
// whose merge-base with HEAD is closest to HEAD (fewest commits ahead).
|
|
2958
|
+
const candidates = ["origin/dev", "dev", "origin/main", "origin/master", "main", "master"];
|
|
2959
|
+
let best: { ref: string; ahead: number } | undefined;
|
|
2960
|
+
for (const candidate of candidates) {
|
|
2961
|
+
const exists = await spawnText(["git", "rev-parse", "--verify", candidate], { cwd, timeoutMs: 3000 });
|
|
2962
|
+
if (!exists.ok) continue;
|
|
2963
|
+
const mergeBase = await spawnText(["git", "merge-base", "HEAD", candidate], { cwd, timeoutMs: 3000 });
|
|
2964
|
+
if (!mergeBase.ok || !mergeBase.stdout.trim()) continue;
|
|
2965
|
+
const count = await spawnText(["git", "rev-list", "--count", `${mergeBase.stdout.trim()}..HEAD`], {
|
|
2966
|
+
cwd,
|
|
2967
|
+
timeoutMs: 3000,
|
|
2968
|
+
});
|
|
2969
|
+
const ahead = Number.parseInt(count.stdout.trim(), 10);
|
|
2970
|
+
if (!Number.isFinite(ahead)) continue;
|
|
2971
|
+
if (!best || ahead < best.ahead) best = { ref: candidate, ahead };
|
|
2972
|
+
}
|
|
2973
|
+
if (best) return best.ref;
|
|
2928
2974
|
}
|
|
2929
2975
|
const mergeBase = await spawnText(["git", "merge-base", "HEAD", "origin/main"], { cwd, timeoutMs: 3000 });
|
|
2930
2976
|
if (mergeBase.ok && mergeBase.stdout.trim()) return mergeBase.stdout.trim();
|
|
@@ -3245,6 +3291,7 @@ const FLAGS_WITH_VALUES = new Set([
|
|
|
3245
3291
|
"--rationale",
|
|
3246
3292
|
"--replacements-json",
|
|
3247
3293
|
"--order-json",
|
|
3294
|
+
"--classification",
|
|
3248
3295
|
]);
|
|
3249
3296
|
|
|
3250
3297
|
function isHelpArg(arg: string): boolean {
|
|
@@ -3319,6 +3366,25 @@ function renderUltragoalHelp(args: readonly string[]): string | null {
|
|
|
3319
3366
|
"",
|
|
3320
3367
|
].join("\n");
|
|
3321
3368
|
}
|
|
3369
|
+
if (subject === "classify-blocker") {
|
|
3370
|
+
return [
|
|
3371
|
+
"Run native GJC Ultragoal workflow commands",
|
|
3372
|
+
"",
|
|
3373
|
+
"USAGE",
|
|
3374
|
+
" $ gjc ultragoal classify-blocker --classification <human_blocked|resolvable> --evidence <text> [FLAGS]",
|
|
3375
|
+
"",
|
|
3376
|
+
"FLAGS",
|
|
3377
|
+
" --classification=<value> Required. human_blocked authorizes pause only as the latest ledger event; resolvable never authorizes pause",
|
|
3378
|
+
" --evidence=<value> Required. Specific blocker evidence; must name the human-only dependency for human_blocked",
|
|
3379
|
+
" --goal-id=<value> Optional durable .gjc/ultragoal goal id, e.g. G001",
|
|
3380
|
+
" --json Output a machine-readable receipt",
|
|
3381
|
+
"",
|
|
3382
|
+
"EXAMPLES",
|
|
3383
|
+
' $ gjc ultragoal classify-blocker --classification resolvable --evidence "failing test can be fixed autonomously"',
|
|
3384
|
+
' $ gjc ultragoal classify-blocker --classification human_blocked --evidence "user must provide production API credentials" --goal-id G001',
|
|
3385
|
+
"",
|
|
3386
|
+
].join("\n");
|
|
3387
|
+
}
|
|
3322
3388
|
return [
|
|
3323
3389
|
"Run native GJC Ultragoal workflow commands",
|
|
3324
3390
|
"",
|
|
@@ -3333,8 +3399,9 @@ function renderUltragoalHelp(args: readonly string[]): string | null {
|
|
|
3333
3399
|
" review",
|
|
3334
3400
|
" steer",
|
|
3335
3401
|
" record-review-blockers",
|
|
3402
|
+
" classify-blocker",
|
|
3336
3403
|
"",
|
|
3337
|
-
"Run `gjc ultragoal checkpoint --help` or `gjc ultragoal
|
|
3404
|
+
"Run `gjc ultragoal checkpoint --help`, `gjc ultragoal review --help`, or `gjc ultragoal classify-blocker --help` for command-specific requirements.",
|
|
3338
3405
|
"",
|
|
3339
3406
|
].join("\n");
|
|
3340
3407
|
}
|
|
@@ -3653,6 +3720,24 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
|
|
|
3653
3720
|
: "Recorded review blockers.\n",
|
|
3654
3721
|
};
|
|
3655
3722
|
}
|
|
3723
|
+
case "classify-blocker": {
|
|
3724
|
+
const event = await recordUltragoalBlockerClassification({
|
|
3725
|
+
cwd,
|
|
3726
|
+
classification: (flagValue(args, "--classification") ?? "") as UltragoalBlockerClassification,
|
|
3727
|
+
evidence: flagValue(args, "--evidence") ?? "",
|
|
3728
|
+
goalId: flagValue(args, "--goal-id"),
|
|
3729
|
+
});
|
|
3730
|
+
return {
|
|
3731
|
+
status: 0,
|
|
3732
|
+
stdout: json
|
|
3733
|
+
? renderCliWriteReceipt({
|
|
3734
|
+
ok: true,
|
|
3735
|
+
event: "blocker_classified",
|
|
3736
|
+
classification: event.classification,
|
|
3737
|
+
})
|
|
3738
|
+
: `Recorded blocker classification: ${String(event.classification)}.\n`,
|
|
3739
|
+
};
|
|
3740
|
+
}
|
|
3656
3741
|
default:
|
|
3657
3742
|
return { status: 1, stderr: `Unknown gjc ultragoal command: ${command}\n` };
|
|
3658
3743
|
}
|
|
@@ -3670,6 +3755,7 @@ const RECONCILE_COMMANDS = new Set([
|
|
|
3670
3755
|
"steer",
|
|
3671
3756
|
"record-review-blockers",
|
|
3672
3757
|
"review",
|
|
3758
|
+
"classify-blocker",
|
|
3673
3759
|
]);
|
|
3674
3760
|
|
|
3675
3761
|
/**
|
|
@@ -1443,7 +1443,8 @@
|
|
|
1443
1443
|
"appliesToVerbs": [
|
|
1444
1444
|
"checkpoint",
|
|
1445
1445
|
"record-review-blockers",
|
|
1446
|
-
"steer"
|
|
1446
|
+
"steer",
|
|
1447
|
+
"classify-blocker"
|
|
1447
1448
|
],
|
|
1448
1449
|
"name": "evidence",
|
|
1449
1450
|
"required": true,
|
|
@@ -1471,6 +1472,25 @@
|
|
|
1471
1472
|
"name": "goal-id",
|
|
1472
1473
|
"type": "string"
|
|
1473
1474
|
},
|
|
1475
|
+
{
|
|
1476
|
+
"appliesToVerbs": [
|
|
1477
|
+
"classify-blocker"
|
|
1478
|
+
],
|
|
1479
|
+
"name": "goal-id",
|
|
1480
|
+
"type": "string"
|
|
1481
|
+
},
|
|
1482
|
+
{
|
|
1483
|
+
"appliesToVerbs": [
|
|
1484
|
+
"classify-blocker"
|
|
1485
|
+
],
|
|
1486
|
+
"enumValues": [
|
|
1487
|
+
"human_blocked",
|
|
1488
|
+
"resolvable"
|
|
1489
|
+
],
|
|
1490
|
+
"name": "classification",
|
|
1491
|
+
"required": true,
|
|
1492
|
+
"type": "enum"
|
|
1493
|
+
},
|
|
1474
1494
|
{
|
|
1475
1495
|
"appliesToVerbs": [
|
|
1476
1496
|
"steer"
|
|
@@ -1570,7 +1590,8 @@
|
|
|
1570
1590
|
"review",
|
|
1571
1591
|
"checkpoint",
|
|
1572
1592
|
"record-review-blockers",
|
|
1573
|
-
"steer"
|
|
1593
|
+
"steer",
|
|
1594
|
+
"classify-blocker"
|
|
1574
1595
|
],
|
|
1575
1596
|
"name": "json",
|
|
1576
1597
|
"type": "boolean"
|
|
@@ -1651,6 +1672,10 @@
|
|
|
1651
1672
|
"name": "steer",
|
|
1652
1673
|
"surface": "command-positional"
|
|
1653
1674
|
},
|
|
1675
|
+
{
|
|
1676
|
+
"name": "classify-blocker",
|
|
1677
|
+
"surface": "command-positional"
|
|
1678
|
+
},
|
|
1654
1679
|
{
|
|
1655
1680
|
"name": "graph",
|
|
1656
1681
|
"planned": true,
|