@sayknow-cli/coding-agent 0.2.4 → 0.2.6

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.
Files changed (150) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/types/cli/migrate-cli.d.ts +20 -0
  3. package/dist/types/cli/update-cli.d.ts +3 -0
  4. package/dist/types/commands/migrate.d.ts +33 -0
  5. package/dist/types/config/keybindings.d.ts +4 -0
  6. package/dist/types/config/model-registry.d.ts +3 -0
  7. package/dist/types/config/models-config-schema.d.ts +5 -0
  8. package/dist/types/config/settings-schema.d.ts +27 -0
  9. package/dist/types/harness-control-plane/storage.d.ts +2 -1
  10. package/dist/types/hooks/skill-state.d.ts +12 -4
  11. package/dist/types/lsp/startup-events.d.ts +1 -0
  12. package/dist/types/migrate/action-planner.d.ts +11 -0
  13. package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
  14. package/dist/types/migrate/adapters/codex.d.ts +5 -0
  15. package/dist/types/migrate/adapters/index.d.ts +45 -0
  16. package/dist/types/migrate/adapters/opencode.d.ts +2 -0
  17. package/dist/types/migrate/executor.d.ts +2 -0
  18. package/dist/types/migrate/mcp-mapper.d.ts +20 -0
  19. package/dist/types/migrate/report.d.ts +18 -0
  20. package/dist/types/migrate/skill-normalizer.d.ts +27 -0
  21. package/dist/types/migrate/types.d.ts +126 -0
  22. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  23. package/dist/types/modes/components/welcome.d.ts +3 -1
  24. package/dist/types/modes/interactive-mode.d.ts +3 -0
  25. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
  26. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
  27. package/dist/types/research-plan/index.d.ts +1 -0
  28. package/dist/types/research-plan/ledger.d.ts +33 -0
  29. package/dist/types/rlm/artifacts.d.ts +1 -1
  30. package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
  31. package/dist/types/skc-runtime/deep-interview-recorder.d.ts +2 -0
  32. package/dist/types/skc-runtime/deep-interview-runtime.d.ts +2 -2
  33. package/dist/types/skc-runtime/goal-mode-request.d.ts +1 -1
  34. package/dist/types/skc-runtime/session-layout.d.ts +59 -0
  35. package/dist/types/skc-runtime/session-resolution.d.ts +47 -0
  36. package/dist/types/skc-runtime/state-graph.d.ts +1 -1
  37. package/dist/types/skc-runtime/state-runtime.d.ts +5 -4
  38. package/dist/types/skc-runtime/state-schema.d.ts +2 -0
  39. package/dist/types/skc-runtime/state-writer.d.ts +36 -7
  40. package/dist/types/skc-runtime/tmux-sessions.d.ts +2 -0
  41. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +7 -4
  42. package/dist/types/skc-runtime/workflow-command-ref.d.ts +1 -1
  43. package/dist/types/skc-runtime/workflow-manifest.d.ts +1 -1
  44. package/dist/types/skill-state/active-state.d.ts +6 -11
  45. package/dist/types/skill-state/canonical-skills.d.ts +3 -0
  46. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +5 -0
  47. package/dist/types/skill-state/workflow-hud.d.ts +2 -0
  48. package/dist/types/task/spawn-gate.d.ts +1 -10
  49. package/package.json +7 -7
  50. package/scripts/build-binary.ts +0 -7
  51. package/src/cli/migrate-cli.ts +106 -0
  52. package/src/cli/setup-cli.ts +14 -1
  53. package/src/cli/update-cli.ts +53 -3
  54. package/src/cli.ts +1 -0
  55. package/src/commands/deep-interview.ts +2 -2
  56. package/src/commands/launch.ts +1 -1
  57. package/src/commands/migrate.ts +46 -0
  58. package/src/commands/state.ts +2 -1
  59. package/src/commands/team.ts +7 -3
  60. package/src/config/model-registry.ts +9 -2
  61. package/src/config/model-resolver.ts +13 -2
  62. package/src/config/models-config-schema.ts +1 -0
  63. package/src/config/settings-schema.ts +17 -0
  64. package/src/coordinator-mcp/policy.ts +10 -2
  65. package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +0 -1
  66. package/src/defaults/skc/skills/deep-interview/SKILL.md +30 -24
  67. package/src/defaults/skc/skills/ralplan/SKILL.md +10 -4
  68. package/src/defaults/skc/skills/team/SKILL.md +51 -47
  69. package/src/defaults/skc/skills/ultragoal/SKILL.md +17 -13
  70. package/src/exec/bash-executor.ts +3 -1
  71. package/src/extensibility/custom-commands/loader.ts +0 -7
  72. package/src/extensibility/skc-plugins/injection.ts +23 -4
  73. package/src/extensibility/skc-plugins/state.ts +16 -1
  74. package/src/harness-control-plane/storage.ts +14 -4
  75. package/src/hooks/native-skill-hook.ts +38 -12
  76. package/src/hooks/skill-state.ts +178 -83
  77. package/src/internal-urls/docs-index.generated.ts +11 -9
  78. package/src/lsp/startup-events.ts +24 -0
  79. package/src/migrate/action-planner.ts +318 -0
  80. package/src/migrate/adapters/claude-code.ts +39 -0
  81. package/src/migrate/adapters/codex.ts +70 -0
  82. package/src/migrate/adapters/index.ts +277 -0
  83. package/src/migrate/adapters/opencode.ts +52 -0
  84. package/src/migrate/executor.ts +81 -0
  85. package/src/migrate/mcp-mapper.ts +152 -0
  86. package/src/migrate/report.ts +104 -0
  87. package/src/migrate/skill-normalizer.ts +80 -0
  88. package/src/migrate/types.ts +163 -0
  89. package/src/modes/bridge/bridge-mode.ts +2 -2
  90. package/src/modes/components/custom-editor.ts +30 -20
  91. package/src/modes/components/welcome.ts +20 -9
  92. package/src/modes/controllers/input-controller.ts +21 -3
  93. package/src/modes/interactive-mode.ts +27 -19
  94. package/src/modes/prompt-action-autocomplete.ts +11 -1
  95. package/src/modes/rpc/rpc-mode.ts +2 -2
  96. package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
  97. package/src/prompts/agents/init.md +1 -1
  98. package/src/prompts/system/plan-mode-active.md +1 -1
  99. package/src/prompts/tools/ast-grep.md +1 -1
  100. package/src/prompts/tools/search.md +1 -1
  101. package/src/prompts/tools/task.md +1 -2
  102. package/src/research-plan/index.ts +1 -0
  103. package/src/research-plan/ledger.ts +177 -0
  104. package/src/rlm/artifacts.ts +12 -3
  105. package/src/rlm/index.ts +7 -0
  106. package/src/runtime-mcp/config-writer.ts +46 -0
  107. package/src/session/agent-session.ts +43 -41
  108. package/src/session/session-manager.ts +19 -2
  109. package/src/setup/hermes/templates/operator-instructions.v1.md +8 -0
  110. package/src/setup/hermes-setup.ts +1 -1
  111. package/src/skc-runtime/deep-interview-recorder.ts +51 -18
  112. package/src/skc-runtime/deep-interview-runtime.ts +49 -23
  113. package/src/skc-runtime/goal-mode-request.ts +26 -11
  114. package/src/skc-runtime/launch-tmux.ts +68 -15
  115. package/src/skc-runtime/ralplan-runtime.ts +79 -50
  116. package/src/skc-runtime/session-layout.ts +180 -0
  117. package/src/skc-runtime/session-resolution.ts +217 -0
  118. package/src/skc-runtime/state-graph.ts +1 -2
  119. package/src/skc-runtime/state-migrations.ts +1 -0
  120. package/src/skc-runtime/state-runtime.ts +237 -114
  121. package/src/skc-runtime/state-schema.ts +2 -0
  122. package/src/skc-runtime/state-writer.ts +310 -42
  123. package/src/skc-runtime/team-runtime.ts +43 -19
  124. package/src/skc-runtime/tmux-sessions.ts +43 -2
  125. package/src/skc-runtime/ultragoal-guard.ts +45 -2
  126. package/src/skc-runtime/ultragoal-runtime.ts +121 -41
  127. package/src/skc-runtime/workflow-command-ref.ts +1 -2
  128. package/src/skc-runtime/workflow-manifest.ts +1 -2
  129. package/src/skill-state/active-state.ts +116 -129
  130. package/src/skill-state/canonical-skills.ts +4 -0
  131. package/src/skill-state/deep-interview-mutation-guard.ts +238 -111
  132. package/src/skill-state/workflow-hud.ts +4 -2
  133. package/src/skill-state/workflow-state-contract.ts +3 -3
  134. package/src/slash-commands/builtin-registry.ts +8 -4
  135. package/src/system-prompt.ts +11 -9
  136. package/src/task/agents.ts +1 -22
  137. package/src/task/index.ts +1 -41
  138. package/src/task/spawn-gate.ts +1 -38
  139. package/src/task/types.ts +1 -1
  140. package/src/tools/ask.ts +34 -12
  141. package/src/tools/ast-edit.ts +2 -2
  142. package/src/tools/computer.ts +58 -4
  143. package/src/utils/edit-mode.ts +1 -1
  144. package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
  145. package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
  146. package/src/prompts/agents/explore.md +0 -58
  147. package/src/prompts/agents/plan.md +0 -49
  148. package/src/prompts/agents/reviewer.md +0 -141
  149. package/src/prompts/agents/task.md +0 -16
  150. package/src/prompts/review-request.md +0 -70
@@ -11,7 +11,8 @@ import {
11
11
  normalizeDeepInterviewEnvelope,
12
12
  questionHash,
13
13
  } from "./deep-interview-state";
14
- import { readExistingStateForMutation, writeWorkflowEnvelopeAtomic } from "./state-writer";
14
+ import { writeSessionActivityMarker } from "./session-resolution";
15
+ import { readExistingStateForMutation, writeGuardedWorkflowEnvelopeAtomic } from "./state-writer";
15
16
 
16
17
  export * from "./deep-interview-state";
17
18
 
@@ -298,6 +299,12 @@ async function readEnvelope(statePath: string): Promise<DeepInterviewStateEnvelo
298
299
  return ensureDeepInterviewStateShape(undefined);
299
300
  }
300
301
 
302
+ function existingStateRevision(value: unknown): number | undefined {
303
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
304
+ const revision = (value as Record<string, unknown>).state_revision;
305
+ return typeof revision === "number" && Number.isFinite(revision) ? revision : 0;
306
+ }
307
+
301
308
  function interviewIdOf(envelope: DeepInterviewStateEnvelope): string | undefined {
302
309
  const inner = (envelope.state ?? {}) as Record<string, unknown>;
303
310
  return typeof inner.interview_id === "string" ? inner.interview_id : undefined;
@@ -310,6 +317,7 @@ async function persistEnvelope(
310
317
  sessionId: string | undefined,
311
318
  command: string,
312
319
  ): Promise<void> {
320
+ if (!sessionId) throw new Error("deep-interview recorder requires a session id");
313
321
  const now = new Date().toISOString();
314
322
  const payload: Record<string, unknown> = { ...normalizeDeepInterviewEnvelope(envelope), updated_at: now };
315
323
  // Guarantee RequiredOnWriteEnvelopeSchema fields for the fresh/absent fallback;
@@ -318,11 +326,22 @@ async function persistEnvelope(
318
326
  payload.version ??= WORKFLOW_STATE_VERSION;
319
327
  payload.active ??= true;
320
328
  payload.current_phase ??= "interviewing";
321
- await writeWorkflowEnvelopeAtomic(statePath, payload, {
329
+ const expectedRevision = existingStateRevision(envelope);
330
+ const writeResult = await writeGuardedWorkflowEnvelopeAtomic(statePath, payload, {
322
331
  cwd,
332
+ policy: "source",
333
+ expectedRevision,
323
334
  receipt: { cwd, skill: "deep-interview", owner: "skc-runtime", command, sessionId, nowIso: now },
324
- audit: { category: "state", verb: "write", owner: "skc-runtime", skill: "deep-interview" },
335
+ audit: { category: "state", verb: "write", owner: "skc-runtime", skill: "deep-interview", sessionId },
325
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
+ }
344
+ await writeSessionActivityMarker(cwd, sessionId, { writer: "deep-interview-recorder", path: statePath });
326
345
  }
327
346
 
328
347
  /**
@@ -335,20 +354,17 @@ async function syncRecorderHud(
335
354
  envelope: DeepInterviewStateEnvelope,
336
355
  sessionId: string | undefined,
337
356
  ): Promise<void> {
338
- try {
339
- const phase = typeof envelope.current_phase === "string" ? envelope.current_phase : "interviewing";
340
- await syncSkillActiveState({
341
- cwd,
342
- skill: "deep-interview",
343
- active: phase !== "complete",
344
- phase,
345
- sessionId,
346
- source: "skc-runtime-deep-interview-recorder",
347
- hud: deriveDeepInterviewHud(envelope as Record<string, unknown>, { phase }),
348
- });
349
- } catch {
350
- // HUD sync is best-effort cache maintenance and must not change record semantics.
351
- }
357
+ const phase = typeof envelope.current_phase === "string" ? envelope.current_phase : "interviewing";
358
+ await syncSkillActiveState({
359
+ cwd,
360
+ skill: "deep-interview",
361
+ active: phase !== "complete",
362
+ phase,
363
+ sessionId,
364
+ source: "skc-runtime-deep-interview-recorder",
365
+ hud: deriveDeepInterviewHud(envelope as Record<string, unknown>, { phase }),
366
+ sourceRevision: existingStateRevision(envelope),
367
+ });
352
368
  }
353
369
 
354
370
  /**
@@ -360,6 +376,19 @@ async function repairRecorderHudFromPersisted(
360
376
  cwd: string,
361
377
  statePath: string,
362
378
  sessionId: string | undefined,
379
+ ): Promise<void> {
380
+ try {
381
+ await syncDeepInterviewRecorderHud(cwd, statePath, sessionId);
382
+ } catch {
383
+ // HUD sync is best-effort cache maintenance and must not change record semantics.
384
+ }
385
+ }
386
+
387
+ /** Refresh the best-effort HUD cache from persisted deep-interview state. */
388
+ export async function syncDeepInterviewRecorderHud(
389
+ cwd: string,
390
+ statePath: string,
391
+ sessionId: string | undefined,
363
392
  ): Promise<void> {
364
393
  const read = await readExistingStateForMutation(statePath);
365
394
  if (read.kind !== "valid") return;
@@ -384,7 +413,11 @@ export async function appendOrMergeDeepInterviewRound(
384
413
  }
385
414
  (envelope.state as Record<string, unknown>).rounds = result.rounds;
386
415
  await persistEnvelope(cwd, statePath, envelope, options.sessionId, "skc deep-interview record-answer");
387
- await syncRecorderHud(cwd, envelope, options.sessionId);
416
+ try {
417
+ await syncRecorderHud(cwd, envelope, options.sessionId);
418
+ } catch {
419
+ // HUD sync is best-effort cache maintenance and must not change record semantics.
420
+ }
388
421
  return { action: result.action, record: result.record };
389
422
  }
390
423
 
@@ -7,6 +7,8 @@ import { deriveDeepInterviewHud } from "../skill-state/workflow-hud";
7
7
  import { WORKFLOW_STATE_VERSION } from "../skill-state/workflow-state-contract";
8
8
  import { normalizeDeepInterviewEnvelope } from "./deep-interview-state";
9
9
  import { runNativeRalplanCommand } from "./ralplan-runtime";
10
+ import { modeStatePath, sessionSpecsDir } from "./session-layout";
11
+ import { resolveSkcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
10
12
  import { runNativeStateCommand } from "./state-runtime";
11
13
  import { appendJsonl, readExistingStateForMutation, writeArtifact, writeWorkflowEnvelopeAtomic } from "./state-writer";
12
14
 
@@ -76,10 +78,6 @@ function assertSafePathComponent(value: string, label: string): void {
76
78
  }
77
79
  }
78
80
 
79
- function encodeSessionSegment(value: string): string {
80
- return encodeURIComponent(value).replaceAll(".", "%2E");
81
- }
82
-
83
81
  function defaultSpecSlug(now: Date = new Date()): string {
84
82
  const yyyy = now.getUTCFullYear().toString().padStart(4, "0");
85
83
  const mm = (now.getUTCMonth() + 1).toString().padStart(2, "0");
@@ -89,14 +87,10 @@ function defaultSpecSlug(now: Date = new Date()): string {
89
87
  return `${yyyy}-${mm}-${dd}-${hh}${min}-${randomBytes(2).toString("hex")}`;
90
88
  }
91
89
 
92
- function stateDirFor(cwd: string, sessionId: string | undefined): string {
93
- return sessionId
94
- ? path.join(cwd, ".skc", "state", "sessions", encodeSessionSegment(sessionId))
95
- : path.join(cwd, ".skc", "state");
96
- }
97
-
98
- export function deepInterviewStatePath(cwd: string, sessionId: string | undefined): string {
99
- return path.join(stateDirFor(cwd, sessionId), "deep-interview-state.json");
90
+ export function deepInterviewStatePath(cwd: string, sessionId?: string): string {
91
+ const resolvedSessionId = sessionId?.trim() || process.env.SKC_SESSION_ID?.trim();
92
+ if (!resolvedSessionId) throw new Error("deep-interview state path requires a session id");
93
+ return modeStatePath(cwd, resolvedSessionId, "deep-interview");
100
94
  }
101
95
 
102
96
  async function resolveSpecContent(rawSpec: string, cwd: string): Promise<string> {
@@ -117,7 +111,7 @@ interface ResolvedDeepInterviewArgs {
117
111
  resolution: DeepInterviewResolution;
118
112
  threshold: number;
119
113
  thresholdSource: string;
120
- sessionId?: string;
114
+ sessionId: string;
121
115
  idea: string;
122
116
  language?: DeepInterviewLanguagePreference;
123
117
  json: boolean;
@@ -134,7 +128,7 @@ export interface ResolvedDeepInterviewSpecWriteArgs {
134
128
  stage: "final";
135
129
  slug: string;
136
130
  spec: string;
137
- sessionId?: string;
131
+ sessionId: string;
138
132
  json: boolean;
139
133
  deliberate: boolean;
140
134
  handoff?: "ralplan";
@@ -277,8 +271,12 @@ async function resolveSpecWriteArgs(args: readonly string[], cwd: string): Promi
277
271
  throw new DeepInterviewCommandError(2, "--spec is required for deep-interview --write");
278
272
  }
279
273
 
280
- const sessionId = flagValue(args, "--session-id")?.trim() || undefined;
281
- if (sessionId) assertSafePathComponent(sessionId, "session-id");
274
+ const session = resolveSkcSessionForWrite(cwd, {
275
+ flagValue: flagValue(args, "--session-id"),
276
+ envSessionId: process.env.SKC_SESSION_ID,
277
+ });
278
+ const sessionId = session.skcSessionId;
279
+ assertSafePathComponent(sessionId, "session-id");
282
280
 
283
281
  const rawHandoff = flagValue(args, "--handoff")?.trim() || undefined;
284
282
  if (rawHandoff && rawHandoff !== "ralplan") {
@@ -324,8 +322,12 @@ async function resolveSpecWriteArgs(args: readonly string[], cwd: string): Promi
324
322
  }
325
323
 
326
324
  async function resolveDeepInterviewArgs(args: readonly string[], cwd: string): Promise<ResolvedDeepInterviewArgs> {
327
- const sessionId = flagValue(args, "--session-id")?.trim() || undefined;
328
- if (sessionId) assertSafePathComponent(sessionId, "session-id");
325
+ const session = resolveSkcSessionForWrite(cwd, {
326
+ flagValue: flagValue(args, "--session-id"),
327
+ envSessionId: process.env.SKC_SESSION_ID,
328
+ });
329
+ const sessionId = session.skcSessionId;
330
+ assertSafePathComponent(sessionId, "session-id");
329
331
 
330
332
  const explicitResolutions = (["quick", "standard", "deep"] as const).filter(name => hasFlag(args, `--${name}`));
331
333
  if (explicitResolutions.length > 1) {
@@ -402,19 +404,34 @@ export async function persistDeepInterviewSpec(
402
404
  }
403
405
  const existing = existingRead.kind === "valid" ? existingRead.value : {};
404
406
 
405
- const specPath = path.join(cwd, ".skc", "specs", `deep-interview-${resolved.slug}.md`);
407
+ const specPath = path.join(sessionSpecsDir(cwd, resolved.sessionId), `deep-interview-${resolved.slug}.md`);
406
408
  const content = resolved.spec.endsWith("\n") ? resolved.spec : `${resolved.spec}\n`;
407
409
  await writeArtifact(specPath, content, {
408
410
  cwd,
409
- audit: { category: "artifact", verb: "write", owner: "skc-runtime", skill: "deep-interview" },
411
+ audit: {
412
+ category: "artifact",
413
+ verb: "write",
414
+ owner: "skc-runtime",
415
+ skill: "deep-interview",
416
+ sessionId: resolved.sessionId,
417
+ },
410
418
  });
411
419
 
412
420
  const sha256 = createHash("sha256").update(content).digest("hex");
413
421
  const createdAt = new Date().toISOString();
414
422
  await appendJsonl(
415
- path.join(cwd, ".skc", "specs", "deep-interview-index.jsonl"),
423
+ path.join(sessionSpecsDir(cwd, resolved.sessionId), "deep-interview-index.jsonl"),
416
424
  { slug: resolved.slug, stage: resolved.stage, path: specPath, created_at: createdAt, sha256 },
417
- { cwd, audit: { category: "ledger", verb: "append", owner: "skc-runtime", skill: "deep-interview" } },
425
+ {
426
+ cwd,
427
+ audit: {
428
+ category: "ledger",
429
+ verb: "append",
430
+ owner: "skc-runtime",
431
+ skill: "deep-interview",
432
+ sessionId: resolved.sessionId,
433
+ },
434
+ },
418
435
  );
419
436
 
420
437
  const payload = normalizeDeepInterviewEnvelope({
@@ -446,9 +463,11 @@ export async function persistDeepInterviewSpec(
446
463
  verb: "write",
447
464
  owner: "skc-runtime",
448
465
  skill: "deep-interview",
466
+ sessionId: resolved.sessionId,
449
467
  forced: resolved.force,
450
468
  },
451
469
  });
470
+ await writeSessionActivityMarker(cwd, resolved.sessionId, { writer: "deep-interview-runtime", path: statePath });
452
471
  await syncDeepInterviewHud({
453
472
  cwd,
454
473
  sessionId: resolved.sessionId,
@@ -503,8 +522,15 @@ async function seedDeepInterviewState(cwd: string, resolved: ResolvedDeepIntervi
503
522
  sessionId: resolved.sessionId,
504
523
  nowIso: now,
505
524
  },
506
- audit: { category: "state", verb: "write", owner: "skc-runtime", skill: "deep-interview" },
525
+ audit: {
526
+ category: "state",
527
+ verb: "write",
528
+ owner: "skc-runtime",
529
+ skill: "deep-interview",
530
+ sessionId: resolved.sessionId,
531
+ },
507
532
  });
533
+ await writeSessionActivityMarker(cwd, resolved.sessionId, { writer: "deep-interview-runtime", path: statePath });
508
534
  await syncDeepInterviewHud({ cwd, sessionId: resolved.sessionId, payload, phase: "interviewing" });
509
535
  return statePath;
510
536
  }
@@ -8,6 +8,8 @@ import {
8
8
  type ModeChangeEntry,
9
9
  type SessionEntry,
10
10
  } from "../session/session-manager";
11
+ import { sessionStateDir, sessionUltragoalDir } from "./session-layout";
12
+ import { resolveSkcSessionForRead, resolveSkcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
11
13
  import { removeFileAudited, writeJsonAtomic } from "./state-writer";
12
14
 
13
15
  export const SKC_SESSION_FILE_ENV = "SKC_SESSION_FILE";
@@ -48,12 +50,12 @@ function isEnoent(error: unknown): boolean {
48
50
  );
49
51
  }
50
52
 
51
- function requestPath(cwd: string): string {
52
- return path.join(cwd, ".skc", "state", "goal-mode-request.json");
53
+ function requestPath(cwd: string, sessionId: string): string {
54
+ return path.join(sessionStateDir(cwd, sessionId), "goal-mode-request.json");
53
55
  }
54
56
 
55
- function ultragoalGoalsPath(cwd: string): string {
56
- return path.join(cwd, ".skc", "ultragoal", "goals.json");
57
+ function ultragoalGoalsPath(cwd: string, sessionId: string): string {
58
+ return path.join(sessionUltragoalDir(cwd, sessionId), "goals.json");
57
59
  }
58
60
 
59
61
  function isCreateGoalsArg(value: string): boolean {
@@ -65,8 +67,14 @@ export function isUltragoalCreateGoalsInvocation(args: readonly string[]): boole
65
67
  return command !== undefined && isCreateGoalsArg(command);
66
68
  }
67
69
 
68
- export async function readUltragoalSkcObjective(cwd: string): Promise<{ objective: string; goalsPath: string }> {
69
- const goalsPath = ultragoalGoalsPath(cwd);
70
+ export async function readUltragoalSkcObjective(
71
+ cwd: string,
72
+ sessionId?: string | null,
73
+ ): Promise<{ objective: string; goalsPath: string }> {
74
+ const session = sessionId?.trim()
75
+ ? { skcSessionId: sessionId.trim() }
76
+ : await resolveSkcSessionForRead(cwd, { envSessionId: process.env.SKC_SESSION_ID });
77
+ const goalsPath = ultragoalGoalsPath(cwd, session.skcSessionId);
70
78
  try {
71
79
  const plan = (await Bun.file(goalsPath).json()) as UltragoalPlanShape;
72
80
  const objective = typeof plan.skcObjective === "string" ? plan.skcObjective.trim() : "";
@@ -87,7 +95,10 @@ export async function writePendingGoalModeRequest(input: {
87
95
  }): Promise<PendingGoalModeRequest> {
88
96
  const objective = input.objective.trim();
89
97
  if (!objective) throw new Error("goal objective is required");
90
- const sessionId = input.sessionId?.trim();
98
+ const resolvedSessionId =
99
+ input.sessionId?.trim() ||
100
+ resolveSkcSessionForWrite(input.cwd, { envSessionId: process.env.SKC_SESSION_ID }).skcSessionId;
101
+ const sessionId = resolvedSessionId;
91
102
  const request: PendingGoalModeRequest = {
92
103
  version: REQUEST_VERSION,
93
104
  kind: "goal_mode_request",
@@ -97,11 +108,12 @@ export async function writePendingGoalModeRequest(input: {
97
108
  goalsPath: input.goalsPath,
98
109
  ...(sessionId ? { sessionId } : {}),
99
110
  };
100
- const filePath = requestPath(input.cwd);
111
+ const filePath = requestPath(input.cwd, sessionId);
101
112
  await writeJsonAtomic(filePath, request, {
102
113
  cwd: input.cwd,
103
- audit: { category: "state", verb: "write", owner: "skc-runtime" },
114
+ audit: { category: "state", verb: "write", owner: "skc-runtime", sessionId },
104
115
  });
116
+ await writeSessionActivityMarker(input.cwd, sessionId, { writer: "goal-mode-request", path: filePath });
105
117
  return request;
106
118
  }
107
119
 
@@ -175,7 +187,10 @@ export async function consumePendingGoalModeRequest(
175
187
  cwd: string,
176
188
  currentSessionId?: string | null,
177
189
  ): Promise<PendingGoalModeRequest | null> {
178
- const filePath = requestPath(cwd);
190
+ const session = currentSessionId?.trim()
191
+ ? { skcSessionId: currentSessionId.trim() }
192
+ : await resolveSkcSessionForRead(cwd, { envSessionId: process.env.SKC_SESSION_ID });
193
+ const filePath = requestPath(cwd, session.skcSessionId);
179
194
  let raw: unknown;
180
195
  try {
181
196
  raw = await Bun.file(filePath).json();
@@ -203,7 +218,7 @@ export async function consumePendingGoalModeRequest(
203
218
  }
204
219
  await removeFileAudited(filePath, {
205
220
  cwd,
206
- audit: { category: "prune", verb: "remove", owner: "skc-runtime" },
221
+ audit: { category: "prune", verb: "remove", owner: "skc-runtime", sessionId: session.skcSessionId },
207
222
  }).catch(error => {
208
223
  if (!isEnoent(error)) throw error;
209
224
  });
@@ -1,6 +1,8 @@
1
+ import { Buffer } from "node:buffer";
1
2
  import * as path from "node:path";
2
3
  import { safeStderrWrite } from "@sayknow-cli/utils";
3
4
  import type { Args } from "../cli/args";
5
+ import { tmuxRuntimeSessionPath } from "./session-layout";
4
6
  import { SKC_COORDINATOR_SESSION_ID_ENV, SKC_COORDINATOR_SESSION_STATE_FILE_ENV } from "./session-state-sidecar";
5
7
  import {
6
8
  buildSkcTmuxProfileCommands,
@@ -14,7 +16,7 @@ import {
14
16
  SKC_TMUX_SESSION_PREFIX,
15
17
  type SkcTmuxProfileCommand,
16
18
  } from "./tmux-common";
17
- import { findSkcTmuxSessionByBranch } from "./tmux-sessions";
19
+ import { findSkcTmuxSessionByName, findSkcTmuxSessionByScope } from "./tmux-sessions";
18
20
 
19
21
  export {
20
22
  buildSkcTmuxProfileCommands,
@@ -83,6 +85,20 @@ export interface TmuxLaunchPlan {
83
85
  sessionStateFile?: string | null;
84
86
  }
85
87
 
88
+ function explicitTmuxSessionName(env: NodeJS.ProcessEnv): string | undefined {
89
+ return env.SKC_TMUX_SESSION?.trim() || undefined;
90
+ }
91
+
92
+ function findExistingSessionForLaunch(context: {
93
+ env: NodeJS.ProcessEnv;
94
+ project: string;
95
+ branch?: string | null;
96
+ }): string | undefined {
97
+ const explicit = explicitTmuxSessionName(context.env);
98
+ if (explicit) return findSkcTmuxSessionByName(explicit, context.env)?.name;
99
+ return findSkcTmuxSessionByScope(context.project, context.branch, context.env)?.name;
100
+ }
101
+
86
102
  export interface SkcTmuxProfileResult {
87
103
  skipped: boolean;
88
104
  commands: SkcTmuxProfileCommand[];
@@ -107,6 +123,7 @@ interface CommandResolutionContext {
107
123
  argv: string[];
108
124
  execPath: string;
109
125
  extraEnv?: Record<string, string>;
126
+ platform?: NodeJS.Platform;
110
127
  }
111
128
 
112
129
  function parseLaunchPolicy(env: NodeJS.ProcessEnv): LaunchPolicy {
@@ -148,6 +165,26 @@ function buildEnvAssignments(values: Record<string, string> | undefined): string
148
165
  const entries = Object.entries(values ?? {});
149
166
  return entries.length === 0 ? "" : ` ${entries.map(([key, value]) => `${key}=${shellQuote(value)}`).join(" ")}`;
150
167
  }
168
+ function powershellQuote(value: string): string {
169
+ return `'${value.replace(/'/g, "''")}'`;
170
+ }
171
+ function stripRootTmuxFlag(rawArgs: string[]): string[] {
172
+ return rawArgs.filter(arg => arg !== "--tmux");
173
+ }
174
+
175
+ function buildWindowsPowerShellInnerCommand(context: CommandResolutionContext, rawArgs: string[]): string {
176
+ const command = resolveCurrentSkcCommand(context);
177
+ const envLines = Object.entries({ [SKC_TMUX_LAUNCHED_ENV]: "1", ...(context.extraEnv ?? {}) }).map(
178
+ ([key, value]) => `$env:${key} = ${powershellQuote(value)}`,
179
+ );
180
+ const invocation = ["&", ...command.map(powershellQuote), ...stripRootTmuxFlag(rawArgs).map(powershellQuote)].join(
181
+ " ",
182
+ );
183
+ const exitLine = "if ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE } else { exit 1 }";
184
+ const script = [...envLines, invocation, exitLine].join("\n");
185
+ const encodedCommand = Buffer.from(script, "utf16le").toString("base64");
186
+ return `pwsh -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encodedCommand}`;
187
+ }
151
188
 
152
189
  export function applySkcTmuxProfile(context: SkcTmuxProfileContext): SkcTmuxProfileResult {
153
190
  const env = context.env ?? process.env;
@@ -177,16 +214,26 @@ function resolveCurrentSkcCommand(context: CommandResolutionContext): string[] {
177
214
  if (isBunVirtualPath(entrypoint)) {
178
215
  return isBunVirtualPath(context.execPath) ? ["skc"] : [context.execPath];
179
216
  }
180
- const resolvedEntrypoint = path.isAbsolute(entrypoint) ? entrypoint : path.resolve(context.cwd, entrypoint);
217
+ const pathModule = pathModuleForPlatform(context.platform);
218
+ const resolvedEntrypoint = pathModule.isAbsolute(entrypoint)
219
+ ? entrypoint
220
+ : pathModule.resolve(context.cwd, entrypoint);
181
221
  if (entrypoint.endsWith(".ts") || entrypoint.endsWith(".js") || entrypoint.endsWith(".mjs")) {
182
222
  return [context.execPath, resolvedEntrypoint];
183
223
  }
184
224
  return [resolvedEntrypoint];
185
225
  }
226
+ function isWindowsPlatform(platform: NodeJS.Platform | undefined): boolean {
227
+ return platform === "win32";
228
+ }
229
+ function pathModuleForPlatform(platform: NodeJS.Platform | undefined): typeof path.win32 | typeof path {
230
+ return isWindowsPlatform(platform) ? path.win32 : path;
231
+ }
186
232
 
187
233
  function buildInnerCommand(context: CommandResolutionContext, rawArgs: string[]): string {
234
+ if (isWindowsPlatform(context.platform)) return buildWindowsPowerShellInnerCommand(context, rawArgs);
188
235
  const command = resolveCurrentSkcCommand(context);
189
- const quoted = [...command, ...rawArgs].map(shellQuote).join(" ");
236
+ const quoted = [...command, ...stripRootTmuxFlag(rawArgs)].map(shellQuote).join(" ");
190
237
  return `exec env ${SKC_TMUX_LAUNCHED_ENV}=1${buildEnvAssignments(context.extraEnv)} ${quoted}`;
191
238
  }
192
239
 
@@ -305,7 +352,6 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
305
352
  if (!context.parsed.tmux || policy === "direct") return undefined;
306
353
  if (env.TMUX || env[SKC_TMUX_LAUNCHED_ENV] === "1") return undefined;
307
354
  const platform = context.platform ?? process.platform;
308
- if (platform === "win32") return undefined;
309
355
  const tty = context.tty ?? { stdin: Boolean(process.stdin.isTTY), stdout: Boolean(process.stdout.isTTY) };
310
356
  if (policy === "tmux" && !isInteractiveRootLaunch(context.parsed, tty)) return undefined;
311
357
 
@@ -315,17 +361,23 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
315
361
  const sessionName = buildSkcTmuxSessionName(env, { branch });
316
362
  const tmuxCommand = resolveSkcTmuxCommand(env);
317
363
  const sessionId = env[SKC_COORDINATOR_SESSION_ID_ENV]?.trim() || sessionName;
364
+ // The session ROOT is keyed by the active SKC session (SKC_SESSION_ID), NOT the
365
+ // coordinator/tmux identity. Fall back to the coordinator id only for standalone
366
+ // tmux launches with no SKC session context.
367
+ const skcSessionId = env.SKC_SESSION_ID?.trim() || sessionId;
318
368
  const sessionStateFile =
319
369
  env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim() ||
320
- path.join(cwd, ".skc", "runtime", "tmux-sessions", `${buildSkcTmuxSessionSlug(sessionName)}.json`);
370
+ tmuxRuntimeSessionPath(cwd, skcSessionId, buildSkcTmuxSessionSlug(sessionName));
321
371
  const tmuxAvailable = context.tmuxAvailable ?? Bun.which(tmuxCommand) !== null;
322
372
  if (!tmuxAvailable) return undefined;
323
- const existingBranchSessionName =
373
+ const existingSessionName =
324
374
  "existingBranchSessionName" in context
325
375
  ? (context.existingBranchSessionName ?? undefined)
326
- : context.worktreeBranch
327
- ? findSkcTmuxSessionByBranch(context.worktreeBranch, env, project)?.name
328
- : undefined;
376
+ : findExistingSessionForLaunch({
377
+ env,
378
+ project,
379
+ branch,
380
+ });
329
381
  const innerCommand = buildInnerCommand(
330
382
  {
331
383
  cwd,
@@ -335,6 +387,7 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
335
387
  [SKC_COORDINATOR_SESSION_ID_ENV]: sessionId,
336
388
  [SKC_COORDINATOR_SESSION_STATE_FILE_ENV]: sessionStateFile,
337
389
  },
390
+ platform,
338
391
  },
339
392
  context.rawArgs,
340
393
  );
@@ -348,7 +401,7 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
348
401
  project,
349
402
  sessionId,
350
403
  sessionStateFile,
351
- attachSessionName: existingBranchSessionName,
404
+ attachSessionName: existingSessionName,
352
405
  };
353
406
  }
354
407
 
@@ -405,19 +458,19 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
405
458
  sessionId: plan.sessionId ?? null,
406
459
  sessionStateFile: plan.sessionStateFile ?? null,
407
460
  });
408
- if (profile.failures.length > 0) {
461
+ const ownershipFailure = profile.failures.find(item => item.command.args.includes("@skc-profile"));
462
+ if (ownershipFailure) {
409
463
  cleanupCreatedTmuxSession(plan, spawnSync, options);
410
- const failure =
411
- profile.failures.find(item => item.command.args.includes("@skc-profile")) ?? profile.failures[0];
412
464
  (context.diagnosticWriter ?? safeStderrWrite)(
413
- formatTmuxLaunchDiagnostic("profile tagging failed", failure?.stderr),
465
+ formatTmuxLaunchDiagnostic("profile tagging failed", ownershipFailure.stderr),
414
466
  );
415
467
  return true;
416
468
  }
417
469
  }
418
470
  if (created.exitCode !== 0) return false;
419
- const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", plan.sessionName], options);
471
+ const attached = spawnSync(plan.tmuxCommand, ["attach-session", "-t", `=${plan.sessionName}`], options);
420
472
  if (attached.exitCode === 0) return true;
473
+ cleanupCreatedTmuxSession(plan, spawnSync, options);
421
474
  (context.diagnosticWriter ?? safeStderrWrite)(formatTmuxLaunchDiagnostic("attach failed", attached.stderr));
422
475
  return true;
423
476
  }