@sayknow-cli/coding-agent 0.2.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -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 +71 -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/defaults/skc-defaults.d.ts +13 -2
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/extensibility/plugins/index.d.ts +1 -0
- package/dist/types/extensibility/plugins/security-scanner.d.ts +31 -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/skc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/skc-runtime/state-writer.d.ts +2 -0
- package/dist/types/skc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +15 -0
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +14 -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/plugin-cli.ts +63 -6
- 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 +61 -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/skc/rules/ponytail.md +68 -0
- package/src/defaults/skc/skills/ultragoal/SKILL.md +16 -0
- package/src/defaults/skc-defaults.ts +27 -2
- 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/extensibility/plugins/index.ts +1 -0
- package/src/extensibility/plugins/manager.ts +50 -0
- package/src/extensibility/plugins/marketplace/manager.ts +42 -0
- package/src/extensibility/plugins/security-scanner.ts +477 -0
- package/src/goals/tools/goal-tool.ts +11 -2
- package/src/hashline/hash.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +10 -8
- 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/skc-runtime/deep-interview-recorder.ts +2 -2
- package/src/skc-runtime/launch-tmux.ts +10 -2
- package/src/skc-runtime/state-runtime.ts +18 -10
- package/src/skc-runtime/state-writer.ts +8 -8
- package/src/skc-runtime/tmux-common.ts +8 -0
- package/src/skc-runtime/tmux-sessions.ts +8 -1
- package/src/skc-runtime/ultragoal-guard.ts +57 -2
- package/src/skc-runtime/ultragoal-runtime.ts +105 -19
- package/src/skc-runtime/workflow-manifest.generated.json +27 -2
- package/src/skc-runtime/workflow-manifest.ts +11 -1
- 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
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host plugin setup for `skc setup claude` and `skc setup codex`.
|
|
3
|
+
*
|
|
4
|
+
* Renders install guidance and a fail-closed coordinator MCP config preview for
|
|
5
|
+
* the canonical generated plugin bundle under `plugins/`. This is intentionally
|
|
6
|
+
* render-only and fail-closed: the workdir allowlist is scoped to the project
|
|
7
|
+
* root and no mutation class is enabled until the user opts in.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { getProjectDir } from "@sayknow-cli/utils";
|
|
13
|
+
|
|
14
|
+
export type HostPluginKind = "claude" | "codex";
|
|
15
|
+
|
|
16
|
+
export interface HostPluginSetupFlags {
|
|
17
|
+
json?: boolean;
|
|
18
|
+
check?: boolean;
|
|
19
|
+
root?: string[];
|
|
20
|
+
repo?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface HostPluginSetupResult {
|
|
24
|
+
ok: true;
|
|
25
|
+
host: HostPluginKind;
|
|
26
|
+
mode: "render";
|
|
27
|
+
gated: boolean;
|
|
28
|
+
pluginPath: string;
|
|
29
|
+
manifestPath: string;
|
|
30
|
+
marketplacePath: string;
|
|
31
|
+
installGuidance: string[];
|
|
32
|
+
coordinatorConfigPreview: {
|
|
33
|
+
command: string;
|
|
34
|
+
args: string[];
|
|
35
|
+
env: Record<string, string>;
|
|
36
|
+
};
|
|
37
|
+
mutationPolicy: string;
|
|
38
|
+
notes: string[];
|
|
39
|
+
check?: { ok: boolean; checked: string[]; missing: string[] };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const NAMESPACE_LABEL = "sayknow-cli-plugin";
|
|
43
|
+
|
|
44
|
+
function resolveProjectRoot(flags: HostPluginSetupFlags): string {
|
|
45
|
+
const explicit = flags.root?.find(root => root.trim().length > 0);
|
|
46
|
+
return explicit ? path.resolve(explicit) : getProjectDir();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function verifyBundleFiles(files: string[]): { ok: boolean; checked: string[]; missing: string[] } {
|
|
50
|
+
const missing = files.filter(file => !fs.existsSync(file));
|
|
51
|
+
return { ok: missing.length === 0, checked: files, missing };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildHostPluginSetup(host: HostPluginKind, flags: HostPluginSetupFlags = {}): HostPluginSetupResult {
|
|
55
|
+
const projectRoot = resolveProjectRoot(flags);
|
|
56
|
+
const marketplaceRoot = path.join(projectRoot, "plugins");
|
|
57
|
+
const pluginDir = path.join(marketplaceRoot, "sayknow-cli");
|
|
58
|
+
const repo = flags.repo && flags.repo.trim().length > 0 ? flags.repo.trim() : NAMESPACE_LABEL;
|
|
59
|
+
|
|
60
|
+
// Concrete, fail-closed env: workdir allowlist is the project root, no mutations.
|
|
61
|
+
const env: Record<string, string> = {
|
|
62
|
+
SKC_COORDINATOR_MCP_WORKDIR_ROOTS: projectRoot,
|
|
63
|
+
SKC_COORDINATOR_MCP_REPO: repo,
|
|
64
|
+
SKC_COORDINATOR_MCP_SESSION_COMMAND: "skc --worktree",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (host === "claude") {
|
|
68
|
+
const manifestPath = path.join(pluginDir, ".claude-plugin", "plugin.json");
|
|
69
|
+
const marketplacePath = path.join(marketplaceRoot, ".claude-plugin", "marketplace.json");
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
host,
|
|
73
|
+
mode: "render",
|
|
74
|
+
gated: false,
|
|
75
|
+
pluginPath: marketplaceRoot,
|
|
76
|
+
manifestPath,
|
|
77
|
+
marketplacePath,
|
|
78
|
+
installGuidance: [
|
|
79
|
+
`Add the local marketplace: /plugin marketplace add ${marketplaceRoot}`,
|
|
80
|
+
"Install the plugin: /plugin install sayknow-cli",
|
|
81
|
+
"Then call skc_delegate_plan / skc_delegate_execute / skc_delegate_team from Claude Code.",
|
|
82
|
+
],
|
|
83
|
+
coordinatorConfigPreview: { command: "skc", args: ["mcp-serve", "coordinator"], env },
|
|
84
|
+
mutationPolicy:
|
|
85
|
+
"Fail-closed: delegation is read-only until you set SKC_COORDINATOR_MCP_MUTATIONS=sessions and pass allow_mutation:true per call.",
|
|
86
|
+
notes: [],
|
|
87
|
+
...(flags.check
|
|
88
|
+
? { check: verifyBundleFiles([manifestPath, marketplacePath, path.join(pluginDir, ".mcp.json")]) }
|
|
89
|
+
: {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Codex: verified installable on Codex CLI 0.139.0 via the local marketplace smoke.
|
|
94
|
+
const manifestPath = path.join(pluginDir, ".codex-plugin", "plugin.json");
|
|
95
|
+
const marketplacePath = path.join(marketplaceRoot, ".agents", "plugins", "marketplace.json");
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
host,
|
|
99
|
+
mode: "render",
|
|
100
|
+
gated: false,
|
|
101
|
+
pluginPath: marketplaceRoot,
|
|
102
|
+
manifestPath,
|
|
103
|
+
marketplacePath,
|
|
104
|
+
installGuidance: [
|
|
105
|
+
`Add the local marketplace: codex plugin marketplace add ${marketplaceRoot}`,
|
|
106
|
+
"Install the plugin: codex plugin add sayknow-cli@sayknow-cli-local",
|
|
107
|
+
"Then call skc_delegate_plan / skc_delegate_execute / skc_delegate_team from Codex.",
|
|
108
|
+
],
|
|
109
|
+
coordinatorConfigPreview: { command: "skc", args: ["mcp-serve", "coordinator"], env },
|
|
110
|
+
mutationPolicy:
|
|
111
|
+
"Fail-closed: delegation is read-only until you set SKC_COORDINATOR_MCP_MUTATIONS=sessions and pass allow_mutation:true per call.",
|
|
112
|
+
notes: [
|
|
113
|
+
"Verified on Codex CLI 0.139.0: marketplace add + plugin add install the plugin (enabled) and `codex mcp list` registers skc-coordinator with the fail-closed env.",
|
|
114
|
+
"The bundled .codex.mcp.json workdir root is host-neutral; `skc setup codex` renders a concrete root, and operators should re-run the local marketplace smoke on their target Codex version.",
|
|
115
|
+
],
|
|
116
|
+
...(flags.check
|
|
117
|
+
? {
|
|
118
|
+
check: verifyBundleFiles([
|
|
119
|
+
manifestPath,
|
|
120
|
+
marketplacePath,
|
|
121
|
+
path.join(pluginDir, ".codex.mcp.json"),
|
|
122
|
+
path.join(pluginDir, "skills", "skc-delegation", "SKILL.md"),
|
|
123
|
+
]),
|
|
124
|
+
}
|
|
125
|
+
: {}),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function formatHostPluginSetup(result: HostPluginSetupResult): string {
|
|
130
|
+
const lines: string[] = [];
|
|
131
|
+
lines.push(`host: ${result.host}${result.gated ? " (gated on versioned smoke)" : ""}`);
|
|
132
|
+
lines.push(`plugin: ${result.pluginPath}`);
|
|
133
|
+
lines.push("install:");
|
|
134
|
+
for (const step of result.installGuidance) lines.push(` - ${step}`);
|
|
135
|
+
lines.push(`mcp: ${result.coordinatorConfigPreview.command} ${result.coordinatorConfigPreview.args.join(" ")}`);
|
|
136
|
+
lines.push(
|
|
137
|
+
` SKC_COORDINATOR_MCP_WORKDIR_ROOTS=${result.coordinatorConfigPreview.env.SKC_COORDINATOR_MCP_WORKDIR_ROOTS}`,
|
|
138
|
+
);
|
|
139
|
+
lines.push(result.mutationPolicy);
|
|
140
|
+
for (const note of result.notes) lines.push(`note: ${note}`);
|
|
141
|
+
return lines.join("\n");
|
|
142
|
+
}
|
|
@@ -362,8 +362,8 @@ async function syncRecorderHud(
|
|
|
362
362
|
phase,
|
|
363
363
|
sessionId,
|
|
364
364
|
source: "skc-runtime-deep-interview-recorder",
|
|
365
|
-
hud: deriveDeepInterviewHud(envelope as Record<string, unknown>, { phase }),
|
|
366
|
-
sourceRevision: existingStateRevision(envelope),
|
|
365
|
+
hud: deriveDeepInterviewHud(normalizeDeepInterviewEnvelope(envelope) as Record<string, unknown>, { phase }),
|
|
366
|
+
sourceRevision: (existingStateRevision(envelope) ?? 0) + 1,
|
|
367
367
|
});
|
|
368
368
|
}
|
|
369
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 "@sayknow-cli/utils";
|
|
4
|
+
import { VERSION } from "@sayknow-cli/utils/dirs";
|
|
4
5
|
import type { Args } from "../cli/args";
|
|
5
6
|
import { tmuxRuntimeSessionPath } from "./session-layout";
|
|
6
7
|
import { SKC_COORDINATOR_SESSION_ID_ENV, SKC_COORDINATOR_SESSION_STATE_FILE_ENV } from "./session-state-sidecar";
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
SKC_TMUX_SESSION_PREFIX,
|
|
17
18
|
type SkcTmuxProfileCommand,
|
|
18
19
|
} from "./tmux-common";
|
|
19
|
-
import { findSkcTmuxSessionByName, findSkcTmuxSessionByScope } from "./tmux-sessions";
|
|
20
|
+
import { findSkcTmuxSessionByName, findSkcTmuxSessionByScope, type SkcTmuxSessionStatus } from "./tmux-sessions";
|
|
20
21
|
|
|
21
22
|
export {
|
|
22
23
|
buildSkcTmuxProfileCommands,
|
|
@@ -88,6 +89,9 @@ export interface TmuxLaunchPlan {
|
|
|
88
89
|
function explicitTmuxSessionName(env: NodeJS.ProcessEnv): string | undefined {
|
|
89
90
|
return env.SKC_TMUX_SESSION?.trim() || undefined;
|
|
90
91
|
}
|
|
92
|
+
function hasCurrentSkcVersion(session: SkcTmuxSessionStatus | 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 findSkcTmuxSessionByName(explicit, context.env)?.name;
|
|
99
|
-
|
|
103
|
+
const scoped = findSkcTmuxSessionByScope(context.project, context.branch, context.env);
|
|
104
|
+
return hasCurrentSkcVersion(scoped) ? scoped?.name : undefined;
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
export interface SkcTmuxProfileResult {
|
|
@@ -116,6 +121,7 @@ export interface SkcTmuxProfileContext {
|
|
|
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 applySkcTmuxProfile(context: SkcTmuxProfileContext): SkcTmuxProf
|
|
|
195
201
|
project: context.project ?? null,
|
|
196
202
|
sessionId: context.sessionId ?? env[SKC_COORDINATOR_SESSION_ID_ENV] ?? null,
|
|
197
203
|
sessionStateFile: context.sessionStateFile ?? env[SKC_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("@skc-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,12 +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;
|
|
1414
|
-
// Reflect the
|
|
1415
|
-
//
|
|
1416
|
-
// stale pre-write value
|
|
1417
|
-
//
|
|
1418
|
-
|
|
1419
|
-
|
|
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;
|
|
1420
1428
|
await syncWorkflowSkillState({ cwd, mode, sessionId, threadId, turnId, active, phase, payload: merged, receipt });
|
|
1421
1429
|
await touchStateActivityMarker(cwd, sessionId, filePath);
|
|
1422
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 SKC_TMUX_BRANCH_SLUG_OPTION = "@skc-branch-slug";
|
|
|
10
10
|
export const SKC_TMUX_PROJECT_OPTION = "@skc-project";
|
|
11
11
|
export const SKC_TMUX_SESSION_ID_OPTION = "@skc-session-id";
|
|
12
12
|
export const SKC_TMUX_SESSION_STATE_FILE_OPTION = "@skc-session-state-file";
|
|
13
|
+
export const SKC_TMUX_VERSION_OPTION = "@skc-version";
|
|
13
14
|
|
|
14
15
|
export interface SkcTmuxProfileCommand {
|
|
15
16
|
description: string;
|
|
@@ -101,6 +102,7 @@ export function buildSkcTmuxRequiredProfileCommands(
|
|
|
101
102
|
project?: string | null;
|
|
102
103
|
sessionId?: string | null;
|
|
103
104
|
sessionStateFile?: string | null;
|
|
105
|
+
version?: string | null;
|
|
104
106
|
} = {},
|
|
105
107
|
): SkcTmuxProfileCommand[] {
|
|
106
108
|
const commands: SkcTmuxProfileCommand[] = [
|
|
@@ -134,6 +136,11 @@ export function buildSkcTmuxRequiredProfileCommands(
|
|
|
134
136
|
description: "record SKC session state marker",
|
|
135
137
|
args: ["set-option", "-t", target, SKC_TMUX_SESSION_STATE_FILE_OPTION, metadata.sessionStateFile],
|
|
136
138
|
});
|
|
139
|
+
if (metadata.version)
|
|
140
|
+
commands.push({
|
|
141
|
+
description: "record SKC version identity",
|
|
142
|
+
args: ["set-option", "-t", target, SKC_TMUX_VERSION_OPTION, metadata.version],
|
|
143
|
+
});
|
|
137
144
|
return commands;
|
|
138
145
|
}
|
|
139
146
|
|
|
@@ -146,6 +153,7 @@ export function buildSkcTmuxProfileCommands(
|
|
|
146
153
|
project?: string | null;
|
|
147
154
|
sessionId?: string | null;
|
|
148
155
|
sessionStateFile?: string | null;
|
|
156
|
+
version?: string | null;
|
|
149
157
|
} = {},
|
|
150
158
|
): SkcTmuxProfileCommand[] {
|
|
151
159
|
const commands = buildSkcTmuxRequiredProfileCommands(target, metadata);
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
SKC_TMUX_PROJECT_OPTION,
|
|
13
13
|
SKC_TMUX_SESSION_ID_OPTION,
|
|
14
14
|
SKC_TMUX_SESSION_STATE_FILE_OPTION,
|
|
15
|
+
SKC_TMUX_VERSION_OPTION,
|
|
15
16
|
} from "./tmux-common";
|
|
16
17
|
|
|
17
18
|
export interface SkcTmuxSessionStatus {
|
|
@@ -26,6 +27,7 @@ export interface SkcTmuxSessionStatus {
|
|
|
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 SkcTmuxSessionTagsForGc {
|
|
|
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): SkcTmuxSessionStatus | 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): SkcTmuxSessionStatus | 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#{${SKC_TMUX_PROFILE_OPTION}}\t#{session_key_table}\t#{session_panes}\t#{pane_pid}\t#{${SKC_TMUX_BRANCH_OPTION}}\t#{${SKC_TMUX_BRANCH_SLUG_OPTION}}\t#{${SKC_TMUX_PROJECT_OPTION}}\t#{${SKC_TMUX_SESSION_ID_OPTION}}\t#{${SKC_TMUX_SESSION_STATE_FILE_OPTION}}`,
|
|
139
|
+
`#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}\t#{${SKC_TMUX_PROFILE_OPTION}}\t#{session_key_table}\t#{session_panes}\t#{pane_pid}\t#{${SKC_TMUX_BRANCH_OPTION}}\t#{${SKC_TMUX_BRANCH_SLUG_OPTION}}\t#{${SKC_TMUX_PROJECT_OPTION}}\t#{${SKC_TMUX_SESSION_ID_OPTION}}\t#{${SKC_TMUX_SESSION_STATE_FILE_OPTION}}\t#{${SKC_TMUX_VERSION_OPTION}}`,
|
|
135
140
|
env,
|
|
136
141
|
);
|
|
137
142
|
}
|
|
@@ -265,6 +270,7 @@ function hydrateSessionFromExactOptions(session: SkcTmuxSessionStatus, env: Node
|
|
|
265
270
|
sessionId: session.sessionId ?? readExactOptionForGc(session.name, SKC_TMUX_SESSION_ID_OPTION, env),
|
|
266
271
|
sessionStateFile:
|
|
267
272
|
session.sessionStateFile ?? readExactOptionForGc(session.name, SKC_TMUX_SESSION_STATE_FILE_OPTION, env),
|
|
273
|
+
version: session.version ?? readExactOptionForGc(session.name, SKC_TMUX_VERSION_OPTION, env),
|
|
268
274
|
};
|
|
269
275
|
}
|
|
270
276
|
|
|
@@ -281,6 +287,7 @@ export function readTmuxSessionTagsForGc(
|
|
|
281
287
|
branchSlug: readExactOptionForGc(sessionName, SKC_TMUX_BRANCH_SLUG_OPTION, env),
|
|
282
288
|
sessionId: readExactOptionForGc(sessionName, SKC_TMUX_SESSION_ID_OPTION, env),
|
|
283
289
|
sessionStateFile: readExactOptionForGc(sessionName, SKC_TMUX_SESSION_STATE_FILE_OPTION, env),
|
|
290
|
+
version: readExactOptionForGc(sessionName, SKC_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, `skc ultragoal steer --kind add_subgoal`, delegate an executor, or `skc ultragoal record-review-blockers`.",
|
|
550
|
+
'If the blocker is genuinely human-only, record `skc ultragoal classify-blocker --classification human_blocked --evidence "<human-only dependency>"` immediately before pausing.',
|
|
551
|
+
].join("\n"),
|
|
552
|
+
);
|
|
553
|
+
}
|