@sayknow-cli/coding-agent 0.5.1 → 0.5.2
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 +132 -0
- package/dist/types/commands/session.d.ts +7 -0
- package/dist/types/config/telegram-autostart.d.ts +9 -1
- package/dist/types/modes/components/pet-capability.d.ts +8 -7
- package/dist/types/modes/components/pet-selector.d.ts +1 -1
- package/dist/types/modes/components/sayknow-pet-widget.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +7 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +1 -0
- package/dist/types/skc-runtime/boot-generation.d.ts +59 -0
- package/dist/types/skc-runtime/launch-tmux.d.ts +10 -2
- package/dist/types/skc-runtime/session-restore-runtime.d.ts +41 -0
- package/dist/types/skc-runtime/session-restore.d.ts +99 -0
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +160 -0
- package/dist/types/skc-runtime/tmux-sessions.d.ts +26 -1
- package/dist/types/tools/ask.d.ts +164 -4
- package/package.json +10 -7
- package/src/commands/session.ts +88 -2
- package/src/config/model-registry.ts +12 -0
- package/src/config/telegram-autostart.ts +11 -4
- package/src/defaults/skc/skills/deep-interview/SKILL.md +29 -3
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/main.ts +1 -1
- package/src/modes/components/pet-capability.ts +22 -13
- package/src/modes/components/pet-selector.ts +1 -1
- package/src/modes/components/sayknow-pet-widget.ts +41 -7
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/shared/agent-wire/unattended-session.ts +40 -9
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -0
- package/src/notifications/lifecycle-control-runtime.ts +258 -179
- package/src/prompts/system/eager-todo.md +2 -0
- package/src/prompts/system/plan-mode-approved.md +1 -1
- package/src/prompts/system/system-prompt.md +4 -2
- package/src/sdk/bus/lifecycle-control-runtime.ts +189 -110
- package/src/session/agent-session.ts +31 -11
- package/src/skc-runtime/boot-generation.ts +172 -0
- package/src/skc-runtime/launch-tmux.ts +219 -41
- package/src/skc-runtime/session-restore-runtime.ts +120 -0
- package/src/skc-runtime/session-restore.ts +296 -0
- package/src/skc-runtime/session-state-sidecar.ts +41 -0
- package/src/skc-runtime/tmux-owner-isolation.ts +665 -0
- package/src/skc-runtime/tmux-sessions.ts +284 -108
- package/src/slash-commands/builtin-registry.ts +9 -4
- package/src/tools/ask.ts +183 -10
- package/src/tools/eval.ts +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type OwnerIsolationProbeSync, type PlanResponse, type TmuxOwnerIsolationExecutionDependencies, type TmuxOwnerIsolationExecutionResult } from "./tmux-owner-isolation";
|
|
1
|
+
import { type AbandonedIdentityCensus, type IdentityCreateKey, type IdentityCreateReservationResult, type OwnerIsolationProbeSync, type PlanResponse, type TmuxOwnerIsolationExecutionDependencies, type TmuxOwnerIsolationExecutionResult } from "./tmux-owner-isolation";
|
|
2
2
|
export interface SkcTmuxSessionStatus {
|
|
3
3
|
name: string;
|
|
4
4
|
attached: boolean;
|
|
@@ -67,6 +67,17 @@ export interface ForceCloseOwnerDependencies {
|
|
|
67
67
|
}
|
|
68
68
|
export interface CreateSkcTmuxSessionOptions {
|
|
69
69
|
platform?: NodeJS.Platform;
|
|
70
|
+
/**
|
|
71
|
+
* Restore only: run the child in the session's recorded directory instead of
|
|
72
|
+
* this process's cwd.
|
|
73
|
+
*/
|
|
74
|
+
cwd?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Restore only: argv appended to the `skc` child, e.g. `["--resume", "<id>"]`.
|
|
77
|
+
* Restore reuses this creator verbatim so it inherits the same identity fence
|
|
78
|
+
* and owner-isolation proof as an ordinary create.
|
|
79
|
+
*/
|
|
80
|
+
childArgs?: string[];
|
|
70
81
|
}
|
|
71
82
|
export type CreateOwnerIsolationTestDependencies = {
|
|
72
83
|
probe?: Partial<OwnerIsolationProbeSync>;
|
|
@@ -83,6 +94,20 @@ export declare function findSkcTmuxSessionByBranch(branch: string, env?: NodeJS.
|
|
|
83
94
|
export declare function findSkcTmuxSessionByName(sessionName: string, env?: NodeJS.ProcessEnv): SkcTmuxSessionStatus | undefined;
|
|
84
95
|
export declare function findSkcTmuxSessionByScope(project: string, branch: string | null | undefined, env?: NodeJS.ProcessEnv): SkcTmuxSessionStatus | undefined;
|
|
85
96
|
export declare function statusSkcTmuxSession(sessionName: string, env?: NodeJS.ProcessEnv): SkcTmuxSessionStatus;
|
|
97
|
+
/**
|
|
98
|
+
* Live-tmux census for authority-first recovery of an abandoned reservation.
|
|
99
|
+
*
|
|
100
|
+
* Authority is the CURRENT published generation on disk compared against the
|
|
101
|
+
* live session's canonical tags — never the reservation's own `phase`, because a
|
|
102
|
+
* creator can publish and then die before recording that it published.
|
|
103
|
+
*/
|
|
104
|
+
/**
|
|
105
|
+
* Resolves a reservation refused because its previous owner died after reaching
|
|
106
|
+
* the helper. Shared by every producer so none of them can brick an identity by
|
|
107
|
+
* treating an abandoned row as a permanent collision.
|
|
108
|
+
*/
|
|
109
|
+
export declare function settleAbandonedIdentity(reserved: IdentityCreateReservationResult, identityKey: IdentityCreateKey, census: AbandonedIdentityCensus): IdentityCreateReservationResult;
|
|
110
|
+
export declare function abandonedIdentityCensus(tmuxCommand: string, env: NodeJS.ProcessEnv, stateFile: string): AbandonedIdentityCensus;
|
|
86
111
|
export declare function createSkcTmuxSession(env?: NodeJS.ProcessEnv, options?: CreateSkcTmuxSessionOptions): SkcTmuxSessionStatus;
|
|
87
112
|
/** Proves a managed reusable name still resolves to one immutable session on one server. */
|
|
88
113
|
export declare function proveSkcTmuxSessionMutationTarget(sessionName: string, env?: NodeJS.ProcessEnv): ProvenTmuxSessionIdentity;
|
|
@@ -29,13 +29,92 @@ export declare const askSchema: z.ZodObject<{
|
|
|
29
29
|
}, z.core.$strip>>;
|
|
30
30
|
multi: z.ZodOptional<z.ZodBoolean>;
|
|
31
31
|
recommended: z.ZodOptional<z.ZodNumber>;
|
|
32
|
-
|
|
32
|
+
workflowGate: z.ZodOptional<z.ZodObject<{
|
|
33
|
+
stage: z.ZodEnum<{
|
|
34
|
+
"deep-interview": "deep-interview";
|
|
35
|
+
ralplan: "ralplan";
|
|
36
|
+
ultragoal: "ultragoal";
|
|
37
|
+
}>;
|
|
38
|
+
kind: z.ZodEnum<{
|
|
39
|
+
question: "question";
|
|
40
|
+
approval: "approval";
|
|
41
|
+
execution: "execution";
|
|
42
|
+
}>;
|
|
43
|
+
}, z.core.$strip>>;
|
|
44
|
+
deepInterview: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
45
|
+
round: z.ZodLiteral<0>;
|
|
46
|
+
component: z.ZodLiteral<"review-topology">;
|
|
47
|
+
intent_contract: z.ZodObject<{
|
|
48
|
+
items: z.ZodArray<z.ZodObject<{
|
|
49
|
+
id: z.ZodString;
|
|
50
|
+
category: z.ZodEnum<{
|
|
51
|
+
surface: "surface";
|
|
52
|
+
artifact: "artifact";
|
|
53
|
+
integration: "integration";
|
|
54
|
+
constraint: "constraint";
|
|
55
|
+
}>;
|
|
56
|
+
statement: z.ZodString;
|
|
57
|
+
}, z.core.$strict>>;
|
|
58
|
+
confirmation_options: z.ZodArray<z.ZodString>;
|
|
59
|
+
}, z.core.$strict>;
|
|
33
60
|
round_id: z.ZodOptional<z.ZodString>;
|
|
61
|
+
dimension: z.ZodString;
|
|
62
|
+
ambiguity: z.ZodNumber;
|
|
63
|
+
confused_terms: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
64
|
+
references: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
65
|
+
reference_id: z.ZodString;
|
|
66
|
+
label: z.ZodString;
|
|
67
|
+
origin: z.ZodString;
|
|
68
|
+
url: z.ZodOptional<z.ZodString>;
|
|
69
|
+
excerpt: z.ZodOptional<z.ZodString>;
|
|
70
|
+
}, z.core.$strict>>>;
|
|
71
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
34
72
|
round: z.ZodNumber;
|
|
73
|
+
intent_review: z.ZodObject<{
|
|
74
|
+
observed_items: z.ZodArray<z.ZodObject<{
|
|
75
|
+
id: z.ZodString;
|
|
76
|
+
category: z.ZodEnum<{
|
|
77
|
+
surface: "surface";
|
|
78
|
+
artifact: "artifact";
|
|
79
|
+
integration: "integration";
|
|
80
|
+
constraint: "constraint";
|
|
81
|
+
}>;
|
|
82
|
+
statement: z.ZodString;
|
|
83
|
+
}, z.core.$strict>>;
|
|
84
|
+
supporting_substitutions: z.ZodArray<z.ZodObject<{
|
|
85
|
+
removed_id: z.ZodString;
|
|
86
|
+
replacement_ids: z.ZodArray<z.ZodString>;
|
|
87
|
+
rationale: z.ZodString;
|
|
88
|
+
}, z.core.$strict>>;
|
|
89
|
+
approval_options: z.ZodArray<z.ZodString>;
|
|
90
|
+
}, z.core.$strict>;
|
|
91
|
+
round_id: z.ZodOptional<z.ZodString>;
|
|
35
92
|
component: z.ZodString;
|
|
36
93
|
dimension: z.ZodString;
|
|
37
94
|
ambiguity: z.ZodNumber;
|
|
38
|
-
|
|
95
|
+
confused_terms: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
96
|
+
references: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
97
|
+
reference_id: z.ZodString;
|
|
98
|
+
label: z.ZodString;
|
|
99
|
+
origin: z.ZodString;
|
|
100
|
+
url: z.ZodOptional<z.ZodString>;
|
|
101
|
+
excerpt: z.ZodOptional<z.ZodString>;
|
|
102
|
+
}, z.core.$strict>>>;
|
|
103
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
104
|
+
round_id: z.ZodOptional<z.ZodString>;
|
|
105
|
+
round: z.ZodNumber;
|
|
106
|
+
component: z.ZodString;
|
|
107
|
+
dimension: z.ZodString;
|
|
108
|
+
ambiguity: z.ZodNumber;
|
|
109
|
+
confused_terms: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
110
|
+
references: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
111
|
+
reference_id: z.ZodString;
|
|
112
|
+
label: z.ZodString;
|
|
113
|
+
origin: z.ZodString;
|
|
114
|
+
url: z.ZodOptional<z.ZodString>;
|
|
115
|
+
excerpt: z.ZodOptional<z.ZodString>;
|
|
116
|
+
}, z.core.$strict>>>;
|
|
117
|
+
}, z.core.$strict>]>>;
|
|
39
118
|
}, z.core.$strip>>;
|
|
40
119
|
}, z.core.$strip>;
|
|
41
120
|
export type AskToolInput = z.infer<typeof askSchema>;
|
|
@@ -54,6 +133,8 @@ export interface AskToolDetails {
|
|
|
54
133
|
multi?: boolean;
|
|
55
134
|
selectedOptions?: string[];
|
|
56
135
|
customInput?: string;
|
|
136
|
+
/** Set when the user answered with a clarification question instead of a choice. */
|
|
137
|
+
clarificationQuestion?: string;
|
|
57
138
|
/** Multi-part question mode */
|
|
58
139
|
results?: QuestionResult[];
|
|
59
140
|
}
|
|
@@ -80,13 +161,92 @@ export declare class AskTool implements AgentTool<typeof askSchema, AskToolDetai
|
|
|
80
161
|
}, z.core.$strip>>;
|
|
81
162
|
multi: z.ZodOptional<z.ZodBoolean>;
|
|
82
163
|
recommended: z.ZodOptional<z.ZodNumber>;
|
|
83
|
-
|
|
164
|
+
workflowGate: z.ZodOptional<z.ZodObject<{
|
|
165
|
+
stage: z.ZodEnum<{
|
|
166
|
+
"deep-interview": "deep-interview";
|
|
167
|
+
ralplan: "ralplan";
|
|
168
|
+
ultragoal: "ultragoal";
|
|
169
|
+
}>;
|
|
170
|
+
kind: z.ZodEnum<{
|
|
171
|
+
question: "question";
|
|
172
|
+
approval: "approval";
|
|
173
|
+
execution: "execution";
|
|
174
|
+
}>;
|
|
175
|
+
}, z.core.$strip>>;
|
|
176
|
+
deepInterview: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
177
|
+
round: z.ZodLiteral<0>;
|
|
178
|
+
component: z.ZodLiteral<"review-topology">;
|
|
179
|
+
intent_contract: z.ZodObject<{
|
|
180
|
+
items: z.ZodArray<z.ZodObject<{
|
|
181
|
+
id: z.ZodString;
|
|
182
|
+
category: z.ZodEnum<{
|
|
183
|
+
surface: "surface";
|
|
184
|
+
artifact: "artifact";
|
|
185
|
+
integration: "integration";
|
|
186
|
+
constraint: "constraint";
|
|
187
|
+
}>;
|
|
188
|
+
statement: z.ZodString;
|
|
189
|
+
}, z.core.$strict>>;
|
|
190
|
+
confirmation_options: z.ZodArray<z.ZodString>;
|
|
191
|
+
}, z.core.$strict>;
|
|
84
192
|
round_id: z.ZodOptional<z.ZodString>;
|
|
193
|
+
dimension: z.ZodString;
|
|
194
|
+
ambiguity: z.ZodNumber;
|
|
195
|
+
confused_terms: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
196
|
+
references: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
197
|
+
reference_id: z.ZodString;
|
|
198
|
+
label: z.ZodString;
|
|
199
|
+
origin: z.ZodString;
|
|
200
|
+
url: z.ZodOptional<z.ZodString>;
|
|
201
|
+
excerpt: z.ZodOptional<z.ZodString>;
|
|
202
|
+
}, z.core.$strict>>>;
|
|
203
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
85
204
|
round: z.ZodNumber;
|
|
205
|
+
intent_review: z.ZodObject<{
|
|
206
|
+
observed_items: z.ZodArray<z.ZodObject<{
|
|
207
|
+
id: z.ZodString;
|
|
208
|
+
category: z.ZodEnum<{
|
|
209
|
+
surface: "surface";
|
|
210
|
+
artifact: "artifact";
|
|
211
|
+
integration: "integration";
|
|
212
|
+
constraint: "constraint";
|
|
213
|
+
}>;
|
|
214
|
+
statement: z.ZodString;
|
|
215
|
+
}, z.core.$strict>>;
|
|
216
|
+
supporting_substitutions: z.ZodArray<z.ZodObject<{
|
|
217
|
+
removed_id: z.ZodString;
|
|
218
|
+
replacement_ids: z.ZodArray<z.ZodString>;
|
|
219
|
+
rationale: z.ZodString;
|
|
220
|
+
}, z.core.$strict>>;
|
|
221
|
+
approval_options: z.ZodArray<z.ZodString>;
|
|
222
|
+
}, z.core.$strict>;
|
|
223
|
+
round_id: z.ZodOptional<z.ZodString>;
|
|
86
224
|
component: z.ZodString;
|
|
87
225
|
dimension: z.ZodString;
|
|
88
226
|
ambiguity: z.ZodNumber;
|
|
89
|
-
|
|
227
|
+
confused_terms: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
228
|
+
references: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
229
|
+
reference_id: z.ZodString;
|
|
230
|
+
label: z.ZodString;
|
|
231
|
+
origin: z.ZodString;
|
|
232
|
+
url: z.ZodOptional<z.ZodString>;
|
|
233
|
+
excerpt: z.ZodOptional<z.ZodString>;
|
|
234
|
+
}, z.core.$strict>>>;
|
|
235
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
236
|
+
round_id: z.ZodOptional<z.ZodString>;
|
|
237
|
+
round: z.ZodNumber;
|
|
238
|
+
component: z.ZodString;
|
|
239
|
+
dimension: z.ZodString;
|
|
240
|
+
ambiguity: z.ZodNumber;
|
|
241
|
+
confused_terms: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
242
|
+
references: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
243
|
+
reference_id: z.ZodString;
|
|
244
|
+
label: z.ZodString;
|
|
245
|
+
origin: z.ZodString;
|
|
246
|
+
url: z.ZodOptional<z.ZodString>;
|
|
247
|
+
excerpt: z.ZodOptional<z.ZodString>;
|
|
248
|
+
}, z.core.$strict>>>;
|
|
249
|
+
}, z.core.$strict>]>>;
|
|
90
250
|
}, z.core.$strip>>;
|
|
91
251
|
}, z.core.$strip>;
|
|
92
252
|
readonly strict = true;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@sayknow-cli/coding-agent",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.2",
|
|
5
5
|
"description": "Sayknow-CLI CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://sayknow-cli.com",
|
|
7
7
|
"author": "jaybeyond",
|
|
@@ -52,12 +52,12 @@
|
|
|
52
52
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
53
53
|
"@babel/parser": "^7.29.3",
|
|
54
54
|
"@mozilla/readability": "^0.6.0",
|
|
55
|
-
"@sayknow-cli/stats": "0.5.
|
|
56
|
-
"@sayknow-cli/agent-core": "0.5.
|
|
57
|
-
"@sayknow-cli/ai": "0.5.
|
|
58
|
-
"@sayknow-cli/natives": "0.5.
|
|
59
|
-
"@sayknow-cli/tui": "0.5.
|
|
60
|
-
"@sayknow-cli/utils": "0.5.
|
|
55
|
+
"@sayknow-cli/stats": "0.5.2",
|
|
56
|
+
"@sayknow-cli/agent-core": "0.5.2",
|
|
57
|
+
"@sayknow-cli/ai": "0.5.2",
|
|
58
|
+
"@sayknow-cli/natives": "0.5.2",
|
|
59
|
+
"@sayknow-cli/tui": "0.5.2",
|
|
60
|
+
"@sayknow-cli/utils": "0.5.2",
|
|
61
61
|
"@puppeteer/browsers": "^2.13.0",
|
|
62
62
|
"@types/turndown": "5.0.6",
|
|
63
63
|
"@xterm/headless": "^6.0.0",
|
|
@@ -124,6 +124,9 @@
|
|
|
124
124
|
"./commands/skc-runtime-bridge": null,
|
|
125
125
|
"./capability/mcp": null,
|
|
126
126
|
"./config/mcp-schema": null,
|
|
127
|
+
"./config/model-auth": null,
|
|
128
|
+
"./config/model-bindings-applier": null,
|
|
129
|
+
"./config/model-discovery-manager": null,
|
|
127
130
|
"./discovery/mcp-json": null,
|
|
128
131
|
"./exa": null,
|
|
129
132
|
"./exa/*": null,
|
package/src/commands/session.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { Args, Command, Flags } from "@sayknow-cli/utils/cli";
|
|
2
|
+
import { readBootGeneration } from "../skc-runtime/boot-generation";
|
|
3
|
+
import {
|
|
4
|
+
decodeRestoreReference,
|
|
5
|
+
encodeRestoreReference,
|
|
6
|
+
evaluateRestoreCandidates,
|
|
7
|
+
listRestorePointers,
|
|
8
|
+
type RestoreCandidateVerdict,
|
|
9
|
+
} from "../skc-runtime/session-restore";
|
|
10
|
+
import { buildRestoreCandidateDeps, restoreSession } from "../skc-runtime/session-restore-runtime";
|
|
2
11
|
import {
|
|
3
12
|
attachSkcTmuxSession,
|
|
4
13
|
createSkcTmuxSession,
|
|
@@ -45,12 +54,13 @@ function sessionJson(session: SessionJsonDto): SessionJsonDto {
|
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
export default class Session extends Command {
|
|
48
|
-
static description =
|
|
57
|
+
static description =
|
|
58
|
+
"List, inspect, attach, and remove tagged SKC-managed tmux sessions. `restore` is a manual, opt-in recovery step that only ever restores sessions this machine can prove died in a REBOOT: nothing starts automatically at login, a session that is still live is never duplicated, and running tools or background work are not recovered — only the tmux session, its directory, and its conversation.";
|
|
49
59
|
static strict = false;
|
|
50
60
|
|
|
51
61
|
static args = {
|
|
52
62
|
action: Args.string({
|
|
53
|
-
description: "list (default), status, create, attach, or
|
|
63
|
+
description: "list (default), status, create, attach, remove, or restore",
|
|
54
64
|
required: false,
|
|
55
65
|
}),
|
|
56
66
|
session: Args.string({
|
|
@@ -67,6 +77,15 @@ export default class Session extends Command {
|
|
|
67
77
|
"state-file": Flags.string({
|
|
68
78
|
description: "Expected @skc-session-state-file tag for force-close (defense-in-depth match)",
|
|
69
79
|
}),
|
|
80
|
+
"dry-run": Flags.boolean({
|
|
81
|
+
description:
|
|
82
|
+
"restore: list candidates and their verdicts. Changes nothing at all (no tmux command, no lock, no database, no pointer write). This is the default when no --reference is given.",
|
|
83
|
+
default: false,
|
|
84
|
+
}),
|
|
85
|
+
reference: Flags.string({
|
|
86
|
+
description:
|
|
87
|
+
"restore: restore exactly one candidate by the opaque reference shown in --dry-run output. Refused unless that candidate is still reboot-eligible at this moment.",
|
|
88
|
+
}),
|
|
70
89
|
};
|
|
71
90
|
|
|
72
91
|
static examples = [
|
|
@@ -76,6 +95,8 @@ export default class Session extends Command {
|
|
|
76
95
|
"skc session attach <session>",
|
|
77
96
|
"skc session remove <session>",
|
|
78
97
|
"skc session force-close <session> --session-id <id>",
|
|
98
|
+
"skc session restore --dry-run # list what a reboot left behind",
|
|
99
|
+
"skc session restore --reference <ref> # restore exactly that one",
|
|
79
100
|
];
|
|
80
101
|
|
|
81
102
|
async run(): Promise<void> {
|
|
@@ -105,6 +126,71 @@ export default class Session extends Command {
|
|
|
105
126
|
return;
|
|
106
127
|
}
|
|
107
128
|
|
|
129
|
+
if (action === "restore") {
|
|
130
|
+
// Manual, reboot-proven, opt-in. Nothing here starts automatically at
|
|
131
|
+
// login, and a session is only ever restored when this host can prove
|
|
132
|
+
// the machine rebooted since that session was recorded.
|
|
133
|
+
const reference = flags.reference;
|
|
134
|
+
const decoded = reference ? decodeRestoreReference(reference) : null;
|
|
135
|
+
if (reference && !decoded) throw new Error("invalid_restore_reference");
|
|
136
|
+
const pointers = listRestorePointers().filter(
|
|
137
|
+
pointer =>
|
|
138
|
+
!decoded ||
|
|
139
|
+
(pointer.coordinator_session_id === decoded.coordinatorSessionId &&
|
|
140
|
+
pointer.state_file === decoded.stateFile),
|
|
141
|
+
);
|
|
142
|
+
if (reference && pointers.length === 0) throw new Error("restore_reference_not_found");
|
|
143
|
+
const verdicts = evaluateRestoreCandidates(pointers, buildRestoreCandidateDeps(readBootGeneration()));
|
|
144
|
+
const describe = (verdict: RestoreCandidateVerdict) => ({
|
|
145
|
+
reference: encodeRestoreReference(verdict.pointer.coordinator_session_id, verdict.pointer.state_file),
|
|
146
|
+
sessionId: verdict.pointer.skc_session_id,
|
|
147
|
+
cwd: verdict.pointer.cwd,
|
|
148
|
+
branch: verdict.pointer.branch,
|
|
149
|
+
eligible: verdict.eligible,
|
|
150
|
+
...(verdict.eligible ? {} : { reason: verdict.reason }),
|
|
151
|
+
});
|
|
152
|
+
// Dry run is the default reporting surface and performs zero mutation:
|
|
153
|
+
// no pointer write, no lock, no SQLite, no tmux command.
|
|
154
|
+
if (flags["dry-run"] || !reference) {
|
|
155
|
+
const rows = verdicts.map(describe);
|
|
156
|
+
if (json) {
|
|
157
|
+
writeJson({ ok: true, dryRun: true, candidates: rows });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
writeText(
|
|
161
|
+
rows.length === 0
|
|
162
|
+
? ["no restore candidates"]
|
|
163
|
+
: rows.map(row =>
|
|
164
|
+
[
|
|
165
|
+
row.eligible ? "restorable" : `skipped(${row.reason})`,
|
|
166
|
+
row.sessionId,
|
|
167
|
+
row.cwd,
|
|
168
|
+
row.reference,
|
|
169
|
+
].join("\t"),
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const verdict = verdicts[0];
|
|
175
|
+
if (!verdict) throw new Error("restore_reference_not_found");
|
|
176
|
+
if (!verdict.eligible) throw new Error(`restore_ineligible_${verdict.reason}`);
|
|
177
|
+
const outcome = restoreSession(verdict.pointer);
|
|
178
|
+
if (!outcome.ok) throw new Error(outcome.detail);
|
|
179
|
+
if (json) {
|
|
180
|
+
writeJson({
|
|
181
|
+
ok: true,
|
|
182
|
+
restored: {
|
|
183
|
+
sessionId: outcome.pointer.skc_session_id,
|
|
184
|
+
cwd: outcome.pointer.cwd,
|
|
185
|
+
tmuxSession: outcome.tmuxSession,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
writeText([`restored: ${outcome.tmuxSession} (${outcome.pointer.skc_session_id})`]);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
108
194
|
if (action === "create") {
|
|
109
195
|
const session = createSkcTmuxSession();
|
|
110
196
|
if (json) {
|
|
@@ -1032,6 +1032,13 @@ export class ModelRegistry {
|
|
|
1032
1032
|
#lastStaticLoadMtime: number | null = null;
|
|
1033
1033
|
#registeredProviderSources: Set<string> = new Set();
|
|
1034
1034
|
#providerDiscoveryStates: Map<string, ProviderDiscoveryState> = new Map();
|
|
1035
|
+
/**
|
|
1036
|
+
* Monotonic per-provider discovery sequence. Overlapping refreshes for the
|
|
1037
|
+
* same provider can complete out of order; only the newest-started refresh
|
|
1038
|
+
* may publish discovery state or contribute models, so a slow stale fetch
|
|
1039
|
+
* never overwrites a fresher result.
|
|
1040
|
+
*/
|
|
1041
|
+
#providerDiscoverySequences: Map<string, number> = new Map();
|
|
1035
1042
|
#cacheDbPath?: string;
|
|
1036
1043
|
#suppressedSelectors: Map<string, number> = new Map();
|
|
1037
1044
|
#backgroundRefresh?: Promise<void>;
|
|
@@ -1815,11 +1822,15 @@ export class ModelRegistry {
|
|
|
1815
1822
|
providerConfig: DiscoveryProviderConfig,
|
|
1816
1823
|
strategy: ModelRefreshStrategy,
|
|
1817
1824
|
): Promise<Model<Api>[]> {
|
|
1825
|
+
const sequence = (this.#providerDiscoverySequences.get(providerConfig.provider) ?? 0) + 1;
|
|
1826
|
+
this.#providerDiscoverySequences.set(providerConfig.provider, sequence);
|
|
1827
|
+
const isCurrentDiscovery = () => this.#providerDiscoverySequences.get(providerConfig.provider) === sequence;
|
|
1818
1828
|
const cached = readModelCache<Api>(providerConfig.provider, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
|
|
1819
1829
|
const requiresAuth = !this.#keylessProviders.has(providerConfig.provider);
|
|
1820
1830
|
if (requiresAuth) {
|
|
1821
1831
|
const apiKey = await this.#peekApiKeyForProvider(providerConfig.provider);
|
|
1822
1832
|
if (!isAuthenticated(apiKey)) {
|
|
1833
|
+
if (!isCurrentDiscovery()) return [];
|
|
1823
1834
|
this.#providerDiscoveryStates.set(providerConfig.provider, {
|
|
1824
1835
|
provider: providerConfig.provider,
|
|
1825
1836
|
status: "unauthenticated",
|
|
@@ -1865,6 +1876,7 @@ export class ModelRegistry {
|
|
|
1865
1876
|
: result.models.length > 0
|
|
1866
1877
|
? "ok"
|
|
1867
1878
|
: "empty";
|
|
1879
|
+
if (!isCurrentDiscovery()) return [];
|
|
1868
1880
|
this.#providerDiscoveryStates.set(providerId, {
|
|
1869
1881
|
provider: providerId,
|
|
1870
1882
|
status,
|
|
@@ -10,11 +10,18 @@
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import { getConfigRootDir, isCompiledBinary, logger } from "@sayknow-cli/utils";
|
|
13
|
-
import { settings } from "./settings";
|
|
13
|
+
import { settings as globalSettings, type Settings } from "./settings";
|
|
14
14
|
import type { TelegramSettings } from "./settings-schema";
|
|
15
15
|
import { telegramSettingsToEnv, validateTelegramSettings } from "./telegram-env-bridge";
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The startup command runs against an explicit Settings instance (tests and
|
|
19
|
+
* embedders inject one), so read through it instead of the global proxy —
|
|
20
|
+
* which may not be initialized at all on those paths.
|
|
21
|
+
*/
|
|
22
|
+
type TelegramSettingsSource = Pick<Settings, "get">;
|
|
23
|
+
|
|
24
|
+
function readTelegramSettings(settings: TelegramSettingsSource): TelegramSettings {
|
|
18
25
|
return {
|
|
19
26
|
enabled: settings.get("telegram.enabled"),
|
|
20
27
|
botToken: settings.get("telegram.botToken"),
|
|
@@ -69,10 +76,10 @@ function isAlreadyRunning(pidFile: string): boolean {
|
|
|
69
76
|
* it in the background. Never throws — failures are logged as warnings so the
|
|
70
77
|
* main skc session is unaffected.
|
|
71
78
|
*/
|
|
72
|
-
export async function maybeAutostartTelegramRemote(): Promise<void> {
|
|
79
|
+
export async function maybeAutostartTelegramRemote(settings: TelegramSettingsSource = globalSettings): Promise<void> {
|
|
73
80
|
if (!settings.get("telegram.enabled")) return;
|
|
74
81
|
|
|
75
|
-
const config = readTelegramSettings();
|
|
82
|
+
const config = readTelegramSettings(settings);
|
|
76
83
|
const errors = validateTelegramSettings(config);
|
|
77
84
|
if (errors.length > 0) {
|
|
78
85
|
logger.warn("Telegram Remote enabled but misconfigured, skipping autostart", { errors });
|
|
@@ -26,7 +26,7 @@ Deep Interview implements Ouroboros-inspired Socratic questioning with mathemati
|
|
|
26
26
|
<Do_Not_Use_When>
|
|
27
27
|
- User has a detailed, specific request with file paths, function names, or acceptance criteria -- execute directly
|
|
28
28
|
- User wants to explore options or brainstorm -- use `ralplan` skill instead
|
|
29
|
-
- User wants a quick fix or single change -- delegate to executor or execution
|
|
29
|
+
- User wants a quick fix or single change -- delegate to executor or direct execution
|
|
30
30
|
- User says "just do it" or "skip the questions" without an explicit execution path -- respect their intent by ending interview and writing a `pending approval` spec, not by mutating files or delegating execution
|
|
31
31
|
- User already has a PRD or plan file and explicitly asks to execute it -- use the requested execution skill with that plan
|
|
32
32
|
</Do_Not_Use_When>
|
|
@@ -128,6 +128,12 @@ If the user request appended after this skill as the final `User:` line is alrea
|
|
|
128
128
|
|
|
129
129
|
This gate exists to prevent deep-interview from making easy problems harder. A small verification need does not make a request interview-worthy.
|
|
130
130
|
|
|
131
|
+
**Implementation wording is not execution approval.** When the user says `implementation`, "implementation plan", Korean `구현`, or "구현 계획", they are describing the eventual target, not permission to implement now. Interpreting that wording as approval is the single most likely way this skill breaks its own boundary.
|
|
132
|
+
|
|
133
|
+
On that wording: do not implement, edit/write code, launch implementation workers, or start task/skill/ultragoal implementation. Say plainly — "I can interview for an implementation plan, but I won't implement during deep-interview." — and continue clarifying scope, risks, acceptance criteria, and unknowns.
|
|
134
|
+
|
|
135
|
+
Implementation requires an explicit phase transition/approval after the interview: the workflow phase must explicitly transition out of deep-interview, and execution approval must be captured by a downstream execution path (Phase 5's bridge into `/skill:ralplan`, `/skill:ultragoal`, or `/skill:team`). Wanting an implementation plan and approving implementation are two different consents.
|
|
136
|
+
|
|
131
137
|
## Phase 0.75: Optional Trace Pre-Step
|
|
132
138
|
|
|
133
139
|
Run this phase only when the active deep-interview state or invocation indicates `--trace` / `state.trace.enabled === true`. It is a pre-interview research step, not an implementation phase.
|
|
@@ -326,6 +332,20 @@ If any prompt input is too large, summarize it first and then continue from the
|
|
|
326
332
|
| Context Clarity (brownfield) | "How does this fit?" | "I found JWT auth middleware in `src/auth/` (pattern: passport + JWT). Should this feature extend that path or intentionally diverge from it?" |
|
|
327
333
|
| Scope-fuzzy / ontology stress | "What IS the core thing here?" | "You have named Tasks, Projects, and Workspaces across the last rounds. Which one is the core entity, and which are supporting views or containers?" |
|
|
328
334
|
|
|
335
|
+
**Attach your own guess to every question.** The question exposes the *user's* assumptions; the guess exposes *yours*. Without it the interview only ever audits one side, and the agent's unexamined read of the ask silently steers the next round's targeting.
|
|
336
|
+
|
|
337
|
+
Every generated question MUST carry a one-line `GUESS:` — your hypothesis for the answer, the reason you hold it, and what changes if it is wrong:
|
|
338
|
+
|
|
339
|
+
```
|
|
340
|
+
{question}
|
|
341
|
+
GUESS: {your hypothesis} — {why you hold it}. If it is {the other reading} instead, {what changes}.
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
- Reacting is cheaper than generating: a terse "no, the second one" still carries a full correction, so low-effort answers stop being low-information rounds.
|
|
345
|
+
- A visible guess is falsifiable. Being wrong out loud is the point; guess in the direction you expect pushback when the reading is genuinely contested.
|
|
346
|
+
- A guess is NOT an auto-answer. It never resolves the round, never feeds scoring, and never increments `auto_answer_streak` — only the user's answer does.
|
|
347
|
+
- Skip only when a guess would be pure noise: Round 0 topology confirmation, the Phase 4b restate gate, and clarification re-asks.
|
|
348
|
+
|
|
329
349
|
### Step 2a′: Auto-Research Greenfield Questions
|
|
330
350
|
|
|
331
351
|
When the next question is for a greenfield interview and is tagged `research: true`, load `auto-research-greenfield.md` as an internal `kind: "skill-fragment"` prompt for a fork-context architect before Step 2b. Pass only the tagged question, locked topology summary, prompt-safe initial idea, trimmed prior decisions/gaps, and relevant constraints. The architect must return 2-3 ranked candidates with rationale, confidence, and fallback notes. Validate the shape before use; if valid, incorporate the candidates as concise answer options or context for the single user-facing question and append the round number to `auto_researched_rounds`. If invalid or unavailable, fall back silently to the normal generated question and increment `architect_failures`.
|
|
@@ -340,14 +360,19 @@ Use the `ask` tool with the generated question. When a question has options, you
|
|
|
340
360
|
Round {n} | Component: {target_component_name} | Targeting: {weakest_dimension} | Why now: {one_sentence_targeting_rationale} | Ambiguity: {score}%
|
|
341
361
|
|
|
342
362
|
{question}
|
|
363
|
+
GUESS: {your hypothesis} — {why you hold it}. If it is {the other reading} instead, {what changes}.
|
|
343
364
|
```
|
|
344
365
|
|
|
345
366
|
Options should include contextually relevant choices plus free-text, translated/localized according to `language.instruction` when present.
|
|
346
367
|
|
|
347
|
-
After applying `language.instruction` to the visible question, options, and generated rationale, apply the self-proofread once to new prose only; preserve only the Round/Component/Targeting/Ambiguity line structure, fixed labels, numeric ambiguity value, component/target identifiers, and `deepInterview.*` metadata keys. Do not exempt generated natural-language rationale such as Why now.
|
|
368
|
+
After applying `language.instruction` to the visible question, options, and generated rationale, apply the self-proofread once to new prose only; preserve only the Round/Component/Targeting/Ambiguity line structure, fixed labels (including the `GUESS:` marker), numeric ambiguity value, component/target identifiers, and `deepInterview.*` metadata keys. Do not exempt generated natural-language rationale such as Why now or the guess text that follows the `GUESS:` marker.
|
|
348
369
|
|
|
349
370
|
When calling `ask`, SHOULD include optional structured metadata so the runtime can record the round without manual state writes: `deepInterview.round_id?`, `deepInterview.round`, `deepInterview.component`, `deepInterview.dimension`, and `deepInterview.ambiguity`. Keep this metadata aligned with the visible Round/Component/Targeting/Ambiguity line; if metadata cannot be supplied, the legacy formatted question text remains the fallback.
|
|
350
371
|
|
|
372
|
+
**Non-behavioral adapter context (`confused_terms`, `references`).** `deepInterview.confused_terms` and `deepInterview.references` carry vocabulary and citations queued at interview start. They are inert: they MUST NOT alter the first question, are never inferred from vocabulary density, and a reference's `url`/`excerpt` are strings that are **never auto-fetched**. Bounds enforced at the ask boundary: at most 32 terms and 32 references, adapter strings ≤ 256 characters, `url`/`excerpt` ≤ 2048, and core round metadata (`round_id`, `component`, `dimension`) ≤ 128 — all counted in Unicode code points, so emoji-padded input hits the same ceiling as ASCII.
|
|
373
|
+
|
|
374
|
+
**Free-text vs structural input (`FREETEXT_FIELDS`).** The runtime keeps an allowlist of fields that legitimately carry prose — `initial_context`, `initial_idea`, `initial_context_summary`, `user_response`, `answer`, `goal`, `objective`, `prompt`, `description`, `statement`, `restated_goal`, `evidence`, `excerpt`. Inside those fields, shell metacharacters (`;`, `|`, `&`, backticks, `$()`) are valid prose and MUST NOT be rejected as structural injection; a user is allowed to describe a shell command. Structural fields (ids, categories, hashes) stay strictly validated by their own guards. Size is capped by character-count, not byte length: 50,000 for initial context and 10,000 for a single user response.
|
|
375
|
+
|
|
351
376
|
If the `ask` tool returns `clarificationQuestion`, treat it as a non-answer about the displayed choices. Answer the clarification briefly from the current interview context, then call `ask` again with the exact original question, options, and `deepInterview.*` metadata. A clarification bypasses Step 2b′ auto-answer, Step 2b″ free-text refine, Step 2c ambiguity scoring, Step 2d progress reporting, and Step 2e state updates; it must not be recorded as a round answer. This does not violate the one-question-per-round rule because the round remains unresolved until the user submits a real listed option or `Other` answer.
|
|
352
377
|
|
|
353
378
|
### Step 2b′: Auto-Answer Opted-Out Questions
|
|
@@ -504,7 +529,7 @@ Round {n} complete.
|
|
|
504
529
|
|
|
505
530
|
Apply `language.instruction` when present before showing this progress report so status text, gaps, and next-target phrasing stay in the preserved session language.
|
|
506
531
|
|
|
507
|
-
Then apply the self-proofread once
|
|
532
|
+
Then apply the self-proofread once to narrative status text, generated prose cells, gaps, and next-target phrasing; preserve only table structure, fixed status labels, scores, weights, component ids, and trigger tokens.
|
|
508
533
|
|
|
509
534
|
### Step 2e: Update State
|
|
510
535
|
|
|
@@ -927,6 +952,7 @@ Why bad: 45% ambiguity means nearly half the requirements are unclear. The mathe
|
|
|
927
952
|
- [ ] Oversized initial context/history summarized before scoring, question generation, spec generation, or handoff
|
|
928
953
|
- [ ] Round 0 topology gate completed before scoring; `topology.confirmed_at` persisted
|
|
929
954
|
- [ ] Ambiguity scored and displayed every round, naming the weakest component/dimension target (rotating across active components when N > 1)
|
|
955
|
+
- [ ] Every asked question carried a `GUESS:` line (except Round 0 topology, the Restate gate, and clarification re-asks), and no guess was recorded as an answer or counted toward `auto_answer_streak`
|
|
930
956
|
- [ ] Lateral panel convened at milestone transitions (and before synthesizing agent-supplied answers) with parallel read-only personas
|
|
931
957
|
- [ ] Free-text answers passed the Refine gate; dialectic rhythm guard forced a user question after 3 agent-resolved answers; any auto-answer threshold crossing explicitly confirmed
|
|
932
958
|
- [ ] Closure / Acceptance Guard and the one-sentence Restate gate both passed before crystallization
|