@veewo/claw 0.1.77 → 0.1.79

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/dist/cli.js CHANGED
@@ -5,14 +5,18 @@ import path from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { createHash } from "node:crypto";
7
7
  import { spawn, spawnSync } from "node:child_process";
8
- import { buildDirectWorkflowGuidance, checkProjectProtocol, ClawError, buildPlanWorkflowGuidance, buildMemoryIndex, buildSessionStartDefaultPrompt, buildSessionStartRecoveredPrompt, editPlan, ensureProjectProtocol, enforceTaskRetention, ingestTruth, initProject, getTemplateTaskDoneChoices, resolvePlanTemplateFile, resolveProjectContext, resolveSessionBoundPlan, resolveContext, resolveSeedPlanTemplate, searchMemory, showPlan, createSubplan, switchTask, tryCaptureKnowledgeStop, claimKnowledgeFinalizationJob, listRetryableKnowledgeFinalizationJobs, normalizeTruthMarkdownEncoding, tryCleanupKnowledgeFinalizationReport, writeKnowledgeFinalizationJob, unbindSession, writePlan, } from "@veewo/claw-core";
8
+ import { buildDirectWorkflowGuidance, checkProjectProtocol, ClawError, buildPlanWorkflowGuidance, buildMemoryIndex, buildSessionStartDefaultPrompt, buildSessionStartRecoveredPrompt, editPlan, ensureProjectProtocol, enforceTaskRetention, ingestTruth, initProject, getTemplateTaskDoneChoices, resolvePlanTemplateFile, resolveProjectContext, resolveWorkflowProjectContext, resolveSessionWorkflowContext, deleteSessionWorkflow, sweepExpiredSessionWorkflows, resolveSessionBoundPlan, resolveContext, resolveSeedPlanTemplate, searchMemory, showPlan, createSubplan, switchTask, tryCaptureKnowledgeStop, claimKnowledgeFinalizationJob, listRetryableKnowledgeFinalizationJobs, normalizeTruthMarkdownEncoding, tryCleanupKnowledgeFinalizationReport, writeKnowledgeFinalizationJob, unbindSession, writePlan, } from "@veewo/claw-core";
9
9
  import { buildCodexDriverEnvelope } from "./codex-driver.js";
10
10
  import { checkCodexRuntime, resolveCodexSdkEntryPath } from "./codex-runtime.js";
11
11
  import { extractLatestFinalAssistantMessage } from "./codex-transcript.js";
12
+ import { consumeBufferedHookInput } from "./knowledge-hook-preflight.js";
13
+ import { resolveInvocationHost, withoutInvocationHost } from "./invocation-host.js";
14
+ import { runOpencodeKnowledgeWriter } from "./opencode-runner.js";
12
15
  const CLI_VERSION = readCliVersion();
13
16
  const TOP_LEVEL_COMMANDS = [
14
17
  { name: "init [options]", summary: "Initialize and normalize the .claw project surface." },
15
18
  { name: "context [--task <name>]", summary: "Resolve project context, auto-initializing or correcting .claw state." },
19
+ { name: "session clean [--expired]", summary: "Remove current or expired session workflow state." },
16
20
  { name: "check", summary: "Check and auto-correct .claw project protocol fields." },
17
21
  { name: "plan <subcommand> [options]", summary: "Plan lifecycle: create, start, edit, remove, wait, resume, show, done." },
18
22
  { name: "codex driver", summary: "Return the versioned code-mode driver used by the Codex adapter." },
@@ -33,7 +37,7 @@ const COMMAND_HELP = {
33
37
  { flag: "--name <project-name>", detail: "Human-readable project name." },
34
38
  { flag: "--context-path <file>", detail: "Extra context path to track (repeatable)." },
35
39
  { flag: "--ext-path <path>", detail: "External doc path to index (repeatable)." },
36
- { flag: "--external-writer-skill <skill>", detail: "Skill id for the combined knowledge writer." },
40
+ { flag: "--external-writer-skill <skill>", detail: "Skill id override for the combined knowledge-writer pass." },
37
41
  { flag: "--planning true|false", detail: "Enable planning-aware default template behavior (default true)." },
38
42
  { flag: "--external-planning-skill <skill>", detail: "Skill id for an external planning skill." },
39
43
  { flag: "--gitnexus true|false", detail: "Enable GitNexus integration (default false)." },
@@ -48,6 +52,10 @@ const COMMAND_HELP = {
48
52
  { flag: "--task <name>", detail: "Resolve context scoped to a specific task." },
49
53
  ],
50
54
  },
55
+ session: {
56
+ usage: ["{script} session clean", "{script} session clean --expired"],
57
+ description: "Clean ephemeral session-scoped workflow state without touching a project .claw directory.",
58
+ },
51
59
  check: {
52
60
  usage: ["{script} check"],
53
61
  description: "Check the .claw project protocol and auto-correct any missing or malformed fields in project.json. Returns issues found and the paths that were fixed.",
@@ -58,14 +66,15 @@ const COMMAND_HELP = {
58
66
  subcommands: {
59
67
  create: {
60
68
  usage: [
61
- "{script} plan create \"<title>\" [--goal <text>]",
62
- "{script} plan create --title <text> [--goal <text>] [--template <name>]",
69
+ "{script} plan create \"<title>\" [--goal <text>] [--scope session]",
70
+ "{script} plan create --title <text> [--goal <text>] [--template <name>] [--scope session]",
63
71
  ],
64
72
  description: "Create the task scope and initial plan from a template. Uses explicit `--template` first, otherwise the project's configured `defaultPlanTemplate`, and finally falls back to the built-in `default`; planning-enabled projects start in process.discussing with the default planning bridge tasks, while planning-disabled projects start directly in process.active with one executable task.",
65
73
  summary: "Create the task scope and initial plan.",
66
74
  options: [
67
75
  { flag: "--title <text>", detail: "Task title (required unless a positional title is given)." },
68
76
  { flag: "--goal <text>", detail: "Optional goal text." },
77
+ { flag: "--scope session", detail: "Use ephemeral per-session storage and disable project knowledge side effects." },
69
78
  { flag: "--template <name>", detail: "Optional plan template name. Overrides the project's configured default template." },
70
79
  ],
71
80
  },
@@ -328,19 +337,20 @@ const COMMAND_HELP = {
328
337
  },
329
338
  "internal-knowledge-finalize": {
330
339
  usage: ["{script} internal-knowledge-finalize --job <path>"],
331
- description: "Internal: runs one queued knowledge deposition job through the Codex SDK.",
340
+ description: "Internal: runs one queued knowledge deposition job through the host-aware finalization runner (Codex SDK for codex host, opencode run for opencode host).",
332
341
  options: [{ flag: "--job <path>", detail: "(required) Finalization job JSON path." }],
333
342
  },
334
343
  };
335
344
  async function main() {
336
345
  const args = process.argv.slice(2);
337
346
  const explicitHost = readOptionalFlag(args, "--host");
338
- if (explicitHost) {
339
- if (!new Set(["codex", "opencode"]).has(explicitHost)) {
340
- handleError(new ClawError("PROJECT_CONFIG_INVALID", `Unsupported host "${explicitHost}".`));
341
- return;
342
- }
343
- process.env.CLAW_HOST = explicitHost;
347
+ let effectiveHost;
348
+ try {
349
+ effectiveHost = resolveInvocationHost(explicitHost, process.env.CLAW_HOST);
350
+ }
351
+ catch (error) {
352
+ handleError(error);
353
+ return;
344
354
  }
345
355
  const command = args.shift();
346
356
  if (command === "--help" || command === "-h") {
@@ -382,7 +392,10 @@ async function main() {
382
392
  printJson(initProject(initInput));
383
393
  return;
384
394
  case "context":
385
- printJson(buildPublicContextOutput(await runContextCommand(args)));
395
+ printJson(buildPublicContextOutput(await runContextCommand(args, process.cwd(), resolveOwnerSessionKey(), effectiveHost)));
396
+ return;
397
+ case "session":
398
+ runSession(args);
386
399
  return;
387
400
  case "check":
388
401
  const checkResult = ensureProjectProtocol(process.cwd());
@@ -397,7 +410,7 @@ async function main() {
397
410
  });
398
411
  return;
399
412
  case "plan":
400
- await runPlan(args);
413
+ await runPlan(args, effectiveHost);
401
414
  return;
402
415
  case "codex":
403
416
  runCodex(args);
@@ -406,10 +419,10 @@ async function main() {
406
419
  await runTemplate(args);
407
420
  return;
408
421
  case "task":
409
- await runTask(args);
422
+ await runTask(args, effectiveHost);
410
423
  return;
411
424
  case "subplan":
412
- await runSubplan(args);
425
+ await runSubplan(args, effectiveHost);
413
426
  return;
414
427
  case "switch-task":
415
428
  printJson(switchTask({
@@ -425,13 +438,13 @@ async function main() {
425
438
  runSearch(args);
426
439
  return;
427
440
  case "direct":
428
- runDirect(args);
441
+ runDirect(args, effectiveHost);
429
442
  return;
430
443
  case "truth":
431
444
  runTruth(args);
432
445
  return;
433
446
  case "hook":
434
- await runHook(args);
447
+ await runHook(args, effectiveHost);
435
448
  return;
436
449
  case "help":
437
450
  printHelp(args);
@@ -459,13 +472,36 @@ function runCodex(args) {
459
472
  assertNoRemainingArgs(args, "codex driver");
460
473
  printJson(buildCodexDriverEnvelope(CLI_VERSION));
461
474
  }
462
- async function runPlan(args) {
475
+ function runSession(args) {
476
+ const subcommand = args.shift();
477
+ if (subcommand !== "clean") {
478
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown session subcommand "${subcommand ?? ""}".`);
479
+ }
480
+ const expired = readBooleanFlag(args, "--expired");
481
+ assertNoRemainingArgs(args, "session clean");
482
+ if (expired) {
483
+ const removed = sweepExpiredSessionWorkflows();
484
+ printJson({ ok: true, command: "session.clean", expired: true, removedCount: removed.length, removed });
485
+ return;
486
+ }
487
+ const ownerSessionKey = resolveOwnerSessionKey();
488
+ if (!ownerSessionKey) {
489
+ throw new ClawError("PROJECT_CONFIG_INVALID", "session clean requires a platform session id.");
490
+ }
491
+ printJson({
492
+ ok: true,
493
+ command: "session.clean",
494
+ removed: deleteSessionWorkflow(ownerSessionKey),
495
+ });
496
+ }
497
+ async function runPlan(args, effectiveHost) {
463
498
  const subcommand = args.shift();
464
499
  switch (subcommand) {
465
500
  case "create":
466
501
  rejectFlags(args, ["--task", "--plan", "--content", "--status", "--parent-task-id", "--description"]);
467
502
  const explicitTitle = readOptionalFlag(args, "--title");
468
503
  const explicitTemplate = readOptionalFlag(args, "--template");
504
+ const scope = readWorkflowScope(args);
469
505
  const title = explicitTitle ?? readOptionalPositionalArg(args);
470
506
  const templateName = explicitTemplate;
471
507
  if (!title) {
@@ -473,14 +509,15 @@ async function runPlan(args) {
473
509
  }
474
510
  const result = await writePlan({
475
511
  cwd: process.cwd(),
512
+ scope,
476
513
  templateName,
477
514
  title,
478
515
  goalText: readOptionalFlag(args, "--goal"),
479
516
  ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
480
- host: process.env.CLAW_HOST ?? undefined,
517
+ host: effectiveHost,
481
518
  });
482
519
  assertNoRemainingArgs(args, "plan create");
483
- printJson(compactPlanCommandResult("plan.create", result));
520
+ printJson(compactPlanCommandResult("plan.create", result, effectiveHost));
484
521
  return;
485
522
  case "edit": {
486
523
  const target = readPlanMutationTarget(args);
@@ -493,10 +530,10 @@ async function runPlan(args) {
493
530
  ...target,
494
531
  operations,
495
532
  commandSource: "plan.edit",
496
- host: process.env.CLAW_HOST ?? undefined,
533
+ host: effectiveHost,
497
534
  ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
498
535
  });
499
- printJson(compactPlanCommandResult("plan.edit", result));
536
+ printJson(compactPlanCommandResult("plan.edit", result, effectiveHost));
500
537
  if (result.operationChain?.status === "partial")
501
538
  process.exitCode = 1;
502
539
  return;
@@ -513,17 +550,17 @@ async function runPlan(args) {
513
550
  ...target,
514
551
  updates,
515
552
  commandSource: "plan.edit",
516
- host: process.env.CLAW_HOST ?? undefined,
553
+ host: effectiveHost,
517
554
  ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
518
555
  });
519
- printJson(compactPlanCommandResult("plan.remove", result));
556
+ printJson(compactPlanCommandResult("plan.remove", result, effectiveHost));
520
557
  return;
521
558
  }
522
559
  case "wait":
523
- await runPlanStatusAlias(args, "process.wait", "plan.wait");
560
+ await runPlanStatusAlias(args, "process.wait", "plan.wait", effectiveHost);
524
561
  return;
525
562
  case "resume":
526
- await runPlanStatusAlias(args, "process.active", "plan.resume");
563
+ await runPlanStatusAlias(args, "process.active", "plan.resume", effectiveHost);
527
564
  return;
528
565
  case "start": {
529
566
  const updates = readPlanFieldUpdates(args);
@@ -541,10 +578,10 @@ async function runPlan(args) {
541
578
  planStatus: "process.active",
542
579
  completeLifecycleBridge: true,
543
580
  commandSource: "plan.start",
544
- host: process.env.CLAW_HOST ?? undefined,
581
+ host: effectiveHost,
545
582
  ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
546
583
  });
547
- printJson(compactPlanCommandResult("plan.start", result));
584
+ printJson(compactPlanCommandResult("plan.start", result, effectiveHost));
548
585
  return;
549
586
  }
550
587
  case "done": {
@@ -561,22 +598,28 @@ async function runPlan(args) {
561
598
  };
562
599
  const target = readPlanMutationTarget(args);
563
600
  assertNoRemainingArgs(args, "plan done");
564
- const gitNexusPreflightAnalyzed = ensureGitNexusReadyForPlanDone(process.cwd());
601
+ const ownerSessionKey = resolveOwnerSessionKey() ?? undefined;
602
+ const workflowProject = resolveWorkflowProjectContext(process.cwd(), ownerSessionKey);
603
+ const gitNexusPreflightAnalyzed = workflowProject.scope === "project"
604
+ ? ensureGitNexusReadyForPlanDone(process.cwd())
605
+ : false;
565
606
  const result = await editPlan({
566
607
  cwd: process.cwd(),
567
608
  ...target,
568
609
  updates,
569
610
  planStatus: "end.completed",
570
611
  commandSource: "plan.done",
571
- host: process.env.CLAW_HOST ?? undefined,
572
- ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
573
- });
574
- const completionRefresh = queueCompletionRefresh({
575
- cwd: process.cwd(),
576
- taskName: result.taskName,
577
- skipGitNexusRefresh: gitNexusPreflightAnalyzed,
612
+ host: effectiveHost,
613
+ ownerSessionKey,
578
614
  });
579
- printJson(compactPlanCommandResult("plan.done", result, completionRefresh));
615
+ const completionRefresh = workflowProject.scope === "project"
616
+ ? queueCompletionRefresh({
617
+ cwd: process.cwd(),
618
+ taskName: result.taskName,
619
+ skipGitNexusRefresh: gitNexusPreflightAnalyzed,
620
+ })
621
+ : undefined;
622
+ printJson(compactPlanCommandResult("plan.done", result, effectiveHost, completionRefresh));
580
623
  return;
581
624
  }
582
625
  case "show": {
@@ -585,6 +628,7 @@ async function runPlan(args) {
585
628
  const result = showPlan({
586
629
  cwd: process.cwd(),
587
630
  ...target,
631
+ ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
588
632
  });
589
633
  printJson({
590
634
  ok: true,
@@ -602,17 +646,17 @@ async function runPlan(args) {
602
646
  throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown plan subcommand "${subcommand ?? ""}".`);
603
647
  }
604
648
  }
605
- async function runPlanStatusAlias(args, planStatus, command) {
649
+ async function runPlanStatusAlias(args, planStatus, command, effectiveHost) {
606
650
  const result = await editPlan({
607
651
  cwd: process.cwd(),
608
652
  ...readPlanMutationTarget(args),
609
653
  planStatus,
610
654
  commandSource: "plan.edit",
611
- host: process.env.CLAW_HOST ?? undefined,
655
+ host: effectiveHost,
612
656
  ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
613
657
  });
614
658
  assertNoRemainingArgs(args, command);
615
- printJson(compactPlanCommandResult(command, result));
659
+ printJson(compactPlanCommandResult(command, result, effectiveHost));
616
660
  }
617
661
  async function runTemplate(args) {
618
662
  const subcommand = args.shift();
@@ -642,6 +686,7 @@ async function runTemplate(args) {
642
686
  command: "template.validate",
643
687
  ok: true,
644
688
  templateId: template.id,
689
+ ...(template.scope ? { scope: template.scope } : {}),
645
690
  source: template.source,
646
691
  ...(template.templatePath ? { templatePath: template.templatePath } : {}),
647
692
  status: template.status,
@@ -659,7 +704,7 @@ async function runTemplate(args) {
659
704
  });
660
705
  }
661
706
  }
662
- async function runTask(args) {
707
+ async function runTask(args, effectiveHost) {
663
708
  const subcommand = args.shift();
664
709
  switch (subcommand) {
665
710
  case "add": {
@@ -670,10 +715,10 @@ async function runTask(args) {
670
715
  ...target,
671
716
  operations,
672
717
  commandSource: "plan.edit",
673
- host: process.env.CLAW_HOST ?? undefined,
718
+ host: effectiveHost,
674
719
  ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
675
720
  });
676
- printJson(compactPlanCommandResult("task.add", result));
721
+ printJson(compactPlanCommandResult("task.add", result, effectiveHost));
677
722
  if (result.operationChain?.status === "partial")
678
723
  process.exitCode = 1;
679
724
  return;
@@ -686,10 +731,10 @@ async function runTask(args) {
686
731
  ...target,
687
732
  operations,
688
733
  commandSource: "plan.edit",
689
- host: process.env.CLAW_HOST ?? undefined,
734
+ host: effectiveHost,
690
735
  ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
691
736
  });
692
- printJson(compactPlanCommandResult("task.edit", result));
737
+ printJson(compactPlanCommandResult("task.edit", result, effectiveHost));
693
738
  if (result.operationChain?.status === "partial")
694
739
  process.exitCode = 1;
695
740
  return;
@@ -702,10 +747,10 @@ async function runTask(args) {
702
747
  ...target,
703
748
  operations,
704
749
  commandSource: "plan.edit",
705
- host: process.env.CLAW_HOST ?? undefined,
750
+ host: effectiveHost,
706
751
  ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
707
752
  });
708
- printJson(compactPlanCommandResult("task.remove", result));
753
+ printJson(compactPlanCommandResult("task.remove", result, effectiveHost));
709
754
  if (result.operationChain?.status === "partial")
710
755
  process.exitCode = 1;
711
756
  return;
@@ -717,10 +762,10 @@ async function runTask(args) {
717
762
  cwd: process.cwd(),
718
763
  ...target,
719
764
  operations,
720
- host: process.env.CLAW_HOST ?? undefined,
765
+ host: effectiveHost,
721
766
  ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
722
767
  });
723
- printJson(compactPlanCommandResult("task.done", result));
768
+ printJson(compactPlanCommandResult("task.done", result, effectiveHost));
724
769
  if (result.operationChain?.status === "partial")
725
770
  process.exitCode = 1;
726
771
  return;
@@ -762,7 +807,7 @@ function runSearch(args) {
762
807
  }),
763
808
  });
764
809
  }
765
- function runDirect(args) {
810
+ function runDirect(args, effectiveHost) {
766
811
  assertNoRemainingArgs(args, "direct");
767
812
  const completionRefresh = queueCompletionRefresh({
768
813
  cwd: process.cwd(),
@@ -773,14 +818,29 @@ function runDirect(args) {
773
818
  });
774
819
  printJson(compactDirectCommandResult("direct", buildDirectWorkflowGuidance({
775
820
  projectConfig: resolveProjectContext(process.cwd()).projectConfig,
776
- host: process.env.CLAW_HOST ?? undefined,
821
+ host: effectiveHost,
777
822
  }), completionRefresh));
778
823
  }
779
- async function runContextCommand(args, cwd = process.cwd(), ownerSessionKey = resolveOwnerSessionKey()) {
824
+ async function runContextCommand(args, cwd = process.cwd(), ownerSessionKey = resolveOwnerSessionKey(), effectiveHost) {
780
825
  const taskName = readOptionalFlag(args, "--task");
781
826
  let initialized = false;
782
827
  let corrected = false;
783
828
  let fixedPaths = [];
829
+ const sessionProject = resolveSessionWorkflowContext(ownerSessionKey ?? undefined);
830
+ if (sessionProject) {
831
+ const activeWorkflow = !taskName && ownerSessionKey
832
+ ? await tryResolveActiveWorkflowSnapshot(cwd, ownerSessionKey, effectiveHost)
833
+ : null;
834
+ const codexRuntime = effectiveHost === "codex" ? checkCodexRuntime() : null;
835
+ const codexRuntimeError = codexRuntime && !codexRuntime.ok
836
+ ? buildCodexRuntimeError(codexRuntime.detail)
837
+ : null;
838
+ return {
839
+ project: sessionProject,
840
+ ...(activeWorkflow ? { activeWorkflow } : {}),
841
+ ...(codexRuntimeError ? { error: codexRuntimeError } : {}),
842
+ };
843
+ }
784
844
  try {
785
845
  const ensureResult = ensureProjectProtocol(cwd);
786
846
  corrected = ensureResult.changed;
@@ -803,9 +863,9 @@ async function runContextCommand(args, cwd = process.cwd(), ownerSessionKey = re
803
863
  resolved = resolveContext(cwd, taskName);
804
864
  }
805
865
  const activeWorkflow = !taskName && ownerSessionKey
806
- ? await tryResolveActiveWorkflowSnapshot(cwd, ownerSessionKey)
866
+ ? await tryResolveActiveWorkflowSnapshot(cwd, ownerSessionKey, effectiveHost)
807
867
  : null;
808
- const codexRuntime = process.env.CLAW_HOST === "codex" ? checkCodexRuntime() : null;
868
+ const codexRuntime = effectiveHost === "codex" ? checkCodexRuntime() : null;
809
869
  const codexRuntimeError = codexRuntime && !codexRuntime.ok
810
870
  ? buildCodexRuntimeError(codexRuntime.detail)
811
871
  : null;
@@ -839,6 +899,7 @@ function buildPublicContextOutput(context) {
839
899
  const output = {};
840
900
  if (project) {
841
901
  output.project = {
902
+ ...(project.scope === "session" ? { scope: "session" } : {}),
842
903
  projectRoot: project.projectRoot,
843
904
  clawDir: project.clawDir,
844
905
  projectId: project.projectId,
@@ -853,6 +914,12 @@ function buildPublicContextOutput(context) {
853
914
  if (context.activeWorkflow !== undefined) {
854
915
  output.activeWorkflow = context.activeWorkflow;
855
916
  }
917
+ else {
918
+ output.session = {
919
+ boundPlan: false,
920
+ note: "No plan is bound to this session yet. Ask the user for the task scope, or run `claw plan create` when ready to start one.",
921
+ };
922
+ }
856
923
  if (context.error !== undefined) {
857
924
  output.error = context.error;
858
925
  }
@@ -1010,17 +1077,17 @@ function runTruth(args) {
1010
1077
  throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown truth subcommand "${subcommand ?? ""}".`);
1011
1078
  }
1012
1079
  }
1013
- async function runHook(args) {
1080
+ async function runHook(args, effectiveHost) {
1014
1081
  const eventName = args.shift();
1015
1082
  if (!eventName) {
1016
1083
  throw new ClawError("PROJECT_CONFIG_INVALID", "claw hook requires an event name.");
1017
1084
  }
1018
1085
  if (eventName === "SessionStart" || eventName === "auto-claw") {
1019
- await runSessionStartHook();
1086
+ await runSessionStartHook(effectiveHost);
1020
1087
  return;
1021
1088
  }
1022
1089
  if (eventName === "Stop" || eventName === "auto-doc") {
1023
- await runStopHook();
1090
+ await runStopHook(effectiveHost);
1024
1091
  return;
1025
1092
  }
1026
1093
  const project = tryResolveHookProject(process.cwd());
@@ -1057,7 +1124,7 @@ async function runHook(args) {
1057
1124
  logPath,
1058
1125
  });
1059
1126
  }
1060
- async function runStopHook() {
1127
+ async function runStopHook(effectiveHost) {
1061
1128
  if (process.env.CLAW_KNOWLEDGE_FINALIZER === "1") {
1062
1129
  return;
1063
1130
  }
@@ -1066,12 +1133,15 @@ async function runStopHook() {
1066
1133
  const sessionId = resolveOwnerSessionKey(payload);
1067
1134
  const turnId = readHookString(payload, "turn_id");
1068
1135
  const transcriptPath = readHookString(payload, "transcript_path");
1069
- if (!hookCwd || !sessionId || !turnId || !transcriptPath || !containsClawDir(hookCwd)) {
1136
+ const payloadMessage = readHookString(payload, "message");
1137
+ if (!hookCwd || !sessionId || !turnId || !containsClawDir(hookCwd)) {
1070
1138
  return;
1071
1139
  }
1072
1140
  try {
1073
1141
  const project = resolveProjectContext(hookCwd);
1074
- const message = extractLatestFinalAssistantMessage(transcriptPath);
1142
+ // Codex passes a transcript_path; opencode and other hosts without a file
1143
+ // transcript pass the final assistant message inline. Either source is valid.
1144
+ const message = payloadMessage ?? (transcriptPath ? extractLatestFinalAssistantMessage(transcriptPath) : null);
1075
1145
  if (!message) {
1076
1146
  return;
1077
1147
  }
@@ -1080,6 +1150,7 @@ async function runStopHook() {
1080
1150
  sessionId,
1081
1151
  turnId,
1082
1152
  message,
1153
+ host: effectiveHost,
1083
1154
  });
1084
1155
  if (result.ok && result.jobPath && process.env.CLAW_KNOWLEDGE_FINALIZER_DISABLE_LAUNCH !== "1") {
1085
1156
  launchKnowledgeFinalizationWorker(result.jobPath, project.projectRoot);
@@ -1096,27 +1167,8 @@ async function runInternalKnowledgeFinalize(args) {
1096
1167
  return;
1097
1168
  }
1098
1169
  try {
1099
- const sdk = await import(pathToFileURL(resolveCodexSdkEntryPath()).href);
1100
- const Codex = sdk.Codex;
1101
- const codex = new Codex({
1102
- env: knowledgeFinalizerEnvironment(),
1103
- ...(process.env.CLAW_CODEX_PATH_OVERRIDE
1104
- ? { codexPathOverride: process.env.CLAW_CODEX_PATH_OVERRIDE }
1105
- : {}),
1106
- });
1107
- const writer = running.writer ?? { model: null, reasoningEffort: "medium" };
1108
- const thread = codex.startThread({
1109
- workingDirectory: running.projectRoot,
1110
- sandboxMode: "workspace-write",
1111
- approvalPolicy: "never",
1112
- networkAccessEnabled: false,
1113
- ...(writer.model ? { model: writer.model } : {}),
1114
- ...(writer.reasoningEffort
1115
- ? { modelReasoningEffort: writer.reasoningEffort }
1116
- : {}),
1117
- });
1118
- const turn = await thread.run(buildKnowledgeWriterPrompt(running));
1119
1170
  const project = resolveProjectContext(running.projectRoot);
1171
+ const writerRun = await runKnowledgeWriterForJob(running);
1120
1172
  const truthEncoding = normalizeTruthMarkdownEncoding(project);
1121
1173
  queueCompletionRefresh({
1122
1174
  cwd: running.projectRoot,
@@ -1130,8 +1182,8 @@ async function runInternalKnowledgeFinalize(args) {
1130
1182
  ...running,
1131
1183
  status: "succeeded",
1132
1184
  finishedAt: new Date().toISOString(),
1133
- ...(thread.id ? { sdkThreadId: thread.id } : {}),
1134
- finalResponse: turn.finalResponse,
1185
+ ...(writerRun.threadId ? { sdkThreadId: writerRun.threadId } : {}),
1186
+ finalResponse: writerRun.finalResponse,
1135
1187
  truthEncoding,
1136
1188
  });
1137
1189
  tryCleanupKnowledgeFinalizationReport(project, running.reportPath);
@@ -1149,9 +1201,57 @@ async function runInternalKnowledgeFinalize(args) {
1149
1201
  }
1150
1202
  }
1151
1203
  }
1204
+ /**
1205
+ * Pick the host-aware finalization runner. The opencode host never assumes a Codex SDK
1206
+ * runtime is installed and runs the writer through `opencode run`; the Codex host and
1207
+ * legacy jobs without a host field keep using the versioned Codex SDK runtime.
1208
+ */
1209
+ async function runKnowledgeWriterForJob(running) {
1210
+ if (running.host === "opencode") {
1211
+ const result = runOpencodeKnowledgeWriter({
1212
+ prompt: buildKnowledgeWriterPrompt(running),
1213
+ projectRoot: running.projectRoot,
1214
+ writer: running.writer ?? null,
1215
+ });
1216
+ return {
1217
+ finalResponse: result.finalResponse,
1218
+ ...(result.threadId ? { threadId: result.threadId } : {}),
1219
+ };
1220
+ }
1221
+ if (running.host !== undefined && running.host !== null && running.host !== "codex") {
1222
+ throw new Error(`Unsupported knowledge finalization job host "${String(running.host)}".`);
1223
+ }
1224
+ return runCodexSdkWriter(running);
1225
+ }
1226
+ async function runCodexSdkWriter(running) {
1227
+ const sdk = await import(pathToFileURL(resolveCodexSdkEntryPath()).href);
1228
+ const Codex = sdk.Codex;
1229
+ const codex = new Codex({
1230
+ env: knowledgeFinalizerEnvironment(),
1231
+ ...(process.env.CLAW_CODEX_PATH_OVERRIDE
1232
+ ? { codexPathOverride: process.env.CLAW_CODEX_PATH_OVERRIDE }
1233
+ : {}),
1234
+ });
1235
+ const writer = running.writer ?? { model: null, reasoningEffort: "medium" };
1236
+ const thread = codex.startThread({
1237
+ workingDirectory: running.projectRoot,
1238
+ sandboxMode: "workspace-write",
1239
+ approvalPolicy: "never",
1240
+ networkAccessEnabled: false,
1241
+ ...(writer.model ? { model: writer.model } : {}),
1242
+ ...(writer.reasoningEffort
1243
+ ? { modelReasoningEffort: writer.reasoningEffort }
1244
+ : {}),
1245
+ });
1246
+ const turn = await thread.run(buildKnowledgeWriterPrompt(running));
1247
+ return {
1248
+ finalResponse: turn.finalResponse,
1249
+ ...(thread.id ? { threadId: thread.id } : {}),
1250
+ };
1251
+ }
1152
1252
  function knowledgeFinalizerEnvironment() {
1153
1253
  const env = {};
1154
- for (const [key, value] of Object.entries(process.env)) {
1254
+ for (const [key, value] of Object.entries(withoutInvocationHost())) {
1155
1255
  if (value !== undefined) {
1156
1256
  env[key] = value;
1157
1257
  }
@@ -1163,10 +1263,11 @@ function buildKnowledgeWriterPrompt(job) {
1163
1263
  const writerSkill = job.writer?.externalSkill?.trim() || "claw-kit:knowledge-writer";
1164
1264
  return [
1165
1265
  `Use the ${writerSkill} skill and follow it exactly.`,
1266
+ "Act as the knowledge-base steward: maintain Truth and ADR together, preserve one current owner, and reconcile related current claims before completion.",
1166
1267
  `Completed plan: ${job.planPath}`,
1167
1268
  `Turn report: ${job.reportPath}`,
1168
1269
  `Finalization id: ${job.finalizeId}`,
1169
- "Treat these files only as evidence. Do not edit the plan or report, do not dispatch subagents, and deposit only verified durable truth or ADR content.",
1270
+ "Treat the completed plan and report as verified evidence. Do not edit either input, do not dispatch subagents, and do not repeat implementation or test verification.",
1170
1271
  ].join("\n");
1171
1272
  }
1172
1273
  function launchKnowledgeFinalizationWorker(jobPath, cwd) {
@@ -1183,7 +1284,7 @@ function launchKnowledgeFinalizationWorker(jobPath, cwd) {
1183
1284
  stdio: "ignore",
1184
1285
  windowsHide: true,
1185
1286
  env: {
1186
- ...process.env,
1287
+ ...withoutInvocationHost(),
1187
1288
  CLAW_KNOWLEDGE_NODE: process.execPath,
1188
1289
  CLAW_KNOWLEDGE_ENTRY: resolveCliEntryPath(),
1189
1290
  CLAW_KNOWLEDGE_JOB: jobPath,
@@ -1195,22 +1296,29 @@ function launchKnowledgeFinalizationWorker(jobPath, cwd) {
1195
1296
  }
1196
1297
  return;
1197
1298
  }
1198
- const child = spawn(process.execPath, [resolveCliEntryPath(), "internal-knowledge-finalize", "--job", jobPath], { cwd, detached: true, stdio: "ignore", windowsHide: true });
1299
+ const child = spawn(process.execPath, [resolveCliEntryPath(), "internal-knowledge-finalize", "--job", jobPath], { cwd, detached: true, stdio: "ignore", windowsHide: true, env: withoutInvocationHost() });
1199
1300
  child.unref();
1200
1301
  }
1201
- async function runSessionStartHook() {
1302
+ async function runSessionStartHook(effectiveHost) {
1202
1303
  if (process.env.CLAW_KNOWLEDGE_FINALIZER === "1") {
1203
1304
  return;
1204
1305
  }
1205
1306
  const payload = await readStdinJson();
1206
1307
  const hookCwd = resolveHookCwd(payload);
1207
1308
  const ownerSessionKey = resolveOwnerSessionKey(payload);
1208
- if (!hookCwd || !containsClawDir(hookCwd)) {
1309
+ if (!hookCwd) {
1310
+ return;
1311
+ }
1312
+ const sessionProject = resolveSessionWorkflowContext(ownerSessionKey ?? undefined);
1313
+ if (!containsClawDir(hookCwd) && !sessionProject) {
1209
1314
  return;
1210
1315
  }
1211
1316
  try {
1212
- const context = await runContextCommand([], hookCwd, ownerSessionKey);
1213
- if (!context.error && process.env.CLAW_KNOWLEDGE_FINALIZER_DISABLE_LAUNCH !== "1") {
1317
+ const context = await runContextCommand([], hookCwd, ownerSessionKey, effectiveHost);
1318
+ const contextProject = asJsonRecord(context.project);
1319
+ if (contextProject?.scope !== "session"
1320
+ && !context.error
1321
+ && process.env.CLAW_KNOWLEDGE_FINALIZER_DISABLE_LAUNCH !== "1") {
1214
1322
  const project = resolveProjectContext(hookCwd);
1215
1323
  for (const jobPath of listRetryableKnowledgeFinalizationJobs(project)) {
1216
1324
  try {
@@ -1349,7 +1457,6 @@ function buildRecoveredWorkflowAdditionalContext(activeWorkflow, versionSyncProm
1349
1457
  const workflowGuidance = activeWorkflow.workflowGuidance;
1350
1458
  const nextsteps = toStringList(workflowGuidance?.nextsteps);
1351
1459
  const recommendedCommands = toStringList(workflowGuidance?.recommendedCommands);
1352
- const delegateSubagents = toDelegateNames(workflowGuidance?.delegateSubagents);
1353
1460
  const notes = typeof workflowGuidance?.notes === "string" ? workflowGuidance.notes.trim() : "";
1354
1461
  const askUser = summarizeAskUser(workflowGuidance?.askUser);
1355
1462
  const goalMode = summarizeGoalMode(workflowGuidance?.goalMode);
@@ -1361,7 +1468,6 @@ function buildRecoveredWorkflowAdditionalContext(activeWorkflow, versionSyncProm
1361
1468
  planSummary,
1362
1469
  nextsteps,
1363
1470
  recommendedCommands,
1364
- delegateSubagents,
1365
1471
  notes,
1366
1472
  askUser: askUser ?? "",
1367
1473
  goalMode: goalMode ?? "",
@@ -1470,8 +1576,8 @@ function summarizeRecoveredPlanContent(planContent) {
1470
1576
  }
1471
1577
  return lines.length > 0 ? lines : ["- plan content present in activeWorkflow.planContent JSON."];
1472
1578
  }
1473
- async function tryResolveActiveWorkflowSnapshot(cwd, ownerSessionKey) {
1474
- const project = resolveProjectContext(cwd);
1579
+ async function tryResolveActiveWorkflowSnapshot(cwd, ownerSessionKey, effectiveHost) {
1580
+ const project = resolveWorkflowProjectContext(cwd, ownerSessionKey);
1475
1581
  const planPath = resolveSessionBoundPlan(project, ownerSessionKey);
1476
1582
  if (!planPath) {
1477
1583
  return null;
@@ -1489,6 +1595,7 @@ async function tryResolveActiveWorkflowSnapshot(cwd, ownerSessionKey) {
1489
1595
  cwd,
1490
1596
  taskName,
1491
1597
  planFile,
1598
+ ownerSessionKey,
1492
1599
  });
1493
1600
  if (result.plan.status.startsWith("end.")) {
1494
1601
  unbindSession(project, ownerSessionKey);
@@ -1507,6 +1614,7 @@ async function tryResolveActiveWorkflowSnapshot(cwd, ownerSessionKey) {
1507
1614
  plan: result.plan,
1508
1615
  projectRoot: project.projectRoot,
1509
1616
  projectConfig: project.projectConfig,
1617
+ host: effectiveHost,
1510
1618
  }),
1511
1619
  };
1512
1620
  }
@@ -1515,7 +1623,7 @@ async function tryResolveActiveWorkflowSnapshot(cwd, ownerSessionKey) {
1515
1623
  return null;
1516
1624
  }
1517
1625
  }
1518
- async function runSubplan(args) {
1626
+ async function runSubplan(args, effectiveHost) {
1519
1627
  const subcommand = args.shift();
1520
1628
  switch (subcommand) {
1521
1629
  case "create": {
@@ -1525,9 +1633,10 @@ async function runSubplan(args) {
1525
1633
  parentTaskId: readOptionalNumber(args, "--task-id") ?? failMissingNumericFlag("--task-id"),
1526
1634
  templateName: readOptionalFlag(args, "--template") ?? undefined,
1527
1635
  ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
1636
+ host: effectiveHost,
1528
1637
  });
1529
1638
  assertNoRemainingArgs(args, "subplan create");
1530
- printJson(compactPlanCommandResult("subplan.create", result));
1639
+ printJson(compactPlanCommandResult("subplan.create", result, effectiveHost));
1531
1640
  return;
1532
1641
  }
1533
1642
  default:
@@ -1559,16 +1668,6 @@ function toStringList(value) {
1559
1668
  .map((item) => item.trim())
1560
1669
  : [];
1561
1670
  }
1562
- function toDelegateNames(value) {
1563
- if (!Array.isArray(value)) {
1564
- return [];
1565
- }
1566
- return value
1567
- .map((entry) => (entry && typeof entry === "object" && typeof entry.name === "string"
1568
- ? entry.name.trim()
1569
- : ""))
1570
- .filter((entry) => Boolean(entry));
1571
- }
1572
1671
  function summarizeAskUser(value) {
1573
1672
  if (!value || typeof value.reason !== "string" || !value.reason.trim()) {
1574
1673
  return null;
@@ -1582,11 +1681,14 @@ function summarizeGoalMode(value) {
1582
1681
  return value.recommendedObjective.trim();
1583
1682
  }
1584
1683
  async function readStdinJson() {
1684
+ const bufferedInput = consumeBufferedHookInput();
1585
1685
  const chunks = [];
1586
- for await (const chunk of process.stdin) {
1587
- chunks.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
1686
+ if (bufferedInput === null) {
1687
+ for await (const chunk of process.stdin) {
1688
+ chunks.push(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
1689
+ }
1588
1690
  }
1589
- const raw = chunks.join("").trim();
1691
+ const raw = (bufferedInput ?? chunks.join("")).trim();
1590
1692
  if (!raw) {
1591
1693
  return null;
1592
1694
  }
@@ -1603,14 +1705,14 @@ function readJson(filePath) {
1603
1705
  function stripBom(content) {
1604
1706
  return content.charCodeAt(0) === 0xfeff ? content.slice(1) : content;
1605
1707
  }
1606
- function compactPlanCommandResult(command, result, completionRefresh) {
1708
+ function compactPlanCommandResult(command, result, effectiveHost, completionRefresh) {
1607
1709
  const archivedPlanPath = completionRefresh?.taskRetention.archivedCurrentTask?.taskName === result.taskName &&
1608
1710
  completionRefresh.taskRetention.archivedCurrentTask.archivedPlanPath
1609
1711
  ? completionRefresh.taskRetention.archivedCurrentTask.archivedPlanPath
1610
1712
  : undefined;
1611
1713
  const resolvedPlanPath = archivedPlanPath ?? result.planPath;
1612
- const hostActions = buildHostActions(result);
1613
- const codexResult = process.env.CLAW_HOST === "codex";
1714
+ const codexResult = effectiveHost === "codex";
1715
+ const hostActions = codexResult ? buildHostActions(result) : [];
1614
1716
  return {
1615
1717
  ok: true,
1616
1718
  command,
@@ -1618,17 +1720,12 @@ function compactPlanCommandResult(command, result, completionRefresh) {
1618
1720
  ...(archivedPlanPath ? { archivedPlanPath } : {}),
1619
1721
  planStatus: result.planStatus,
1620
1722
  ...(!codexResult && result.previousPlanStatus ? { previousPlanStatus: result.previousPlanStatus } : {}),
1621
- ...(!codexResult && result.emittedEvents?.length ? { emittedEvents: result.emittedEvents } : {}),
1622
- ...(!codexResult && result.events?.length ? { events: result.events } : {}),
1623
1723
  ...(hostActions.length ? { hostActions } : {}),
1624
1724
  ...(!codexResult && result.changedTaskIds?.length ? { changedTaskIds: result.changedTaskIds } : {}),
1625
1725
  ...(!codexResult && result.appendedTaskIds?.length ? { appendedTaskIds: result.appendedTaskIds } : {}),
1626
1726
  ...(codexResult ? { stage: result.workflowGuidance.stage } : {}),
1627
1727
  ...(!codexResult ? { nextsteps: result.workflowGuidance.nextsteps } : {}),
1628
1728
  ...(result.workflowGuidance.nextTask ? { nextTask: result.workflowGuidance.nextTask } : {}),
1629
- ...(result.workflowGuidance.delegateSubagents?.length
1630
- ? { delegateSubagents: result.workflowGuidance.delegateSubagents }
1631
- : {}),
1632
1729
  ...(result.workflowGuidance.notes?.trim() && !codexResult
1633
1730
  ? { notes: result.workflowGuidance.notes }
1634
1731
  : {}),
@@ -1730,9 +1827,6 @@ function compactDirectCommandResult(command, workflowGuidance, completionRefresh
1730
1827
  command,
1731
1828
  summary: workflowGuidance.summary,
1732
1829
  nextsteps: workflowGuidance.nextsteps,
1733
- ...(workflowGuidance.delegateSubagents?.length
1734
- ? { delegateSubagents: workflowGuidance.delegateSubagents }
1735
- : {}),
1736
1830
  ...(workflowGuidance.notes?.trim() ? { notes: workflowGuidance.notes } : {}),
1737
1831
  ...(workflowGuidance.recommendedCommands?.length
1738
1832
  ? { recommendedCommands: workflowGuidance.recommendedCommands }
@@ -1931,7 +2025,7 @@ function readPlanMutationTarget(args) {
1931
2025
  ...(explicitPlanFile ? { planFile: explicitPlanFile } : {}),
1932
2026
  };
1933
2027
  }
1934
- const project = resolveProjectContext(process.cwd());
2028
+ const project = resolveWorkflowProjectContext(process.cwd(), resolveOwnerSessionKey() ?? undefined);
1935
2029
  const boundPlanPath = resolveSessionBoundPlan(project, resolveOwnerSessionKey() ?? undefined);
1936
2030
  if (!boundPlanPath) {
1937
2031
  throw new ClawError("PROJECT_CONFIG_INVALID", "No plan is bound to the current session. Create or recover a plan first, or use --task-name and optional --plan-file as an advanced override.");
@@ -2085,7 +2179,7 @@ function launchCompletionRefreshWorker(input) {
2085
2179
  stdio: "ignore",
2086
2180
  windowsHide: true,
2087
2181
  env: {
2088
- ...process.env,
2182
+ ...withoutInvocationHost(),
2089
2183
  CLAW_COMPLETION_NODE: process.execPath,
2090
2184
  CLAW_COMPLETION_ENTRY: resolveCliEntryPath(),
2091
2185
  CLAW_COMPLETION_CWD: input.cwd,
@@ -2121,6 +2215,7 @@ function launchCompletionRefreshWorker(input) {
2121
2215
  detached: true,
2122
2216
  stdio: "ignore",
2123
2217
  windowsHide: true,
2218
+ env: withoutInvocationHost(),
2124
2219
  });
2125
2220
  child.unref();
2126
2221
  }
@@ -2763,6 +2858,18 @@ function normalizeVersionString(value) {
2763
2858
  const trimmed = value.trim();
2764
2859
  return trimmed ? trimmed : null;
2765
2860
  }
2861
+ function readWorkflowScope(args) {
2862
+ const value = readOptionalFlag(args, "--scope");
2863
+ if (value === undefined) {
2864
+ return undefined;
2865
+ }
2866
+ if (value === "session") {
2867
+ return "session";
2868
+ }
2869
+ throw new ClawError("PROJECT_CONFIG_INVALID", "--scope currently accepts only session; omit it for project scope.", {
2870
+ scope: value,
2871
+ });
2872
+ }
2766
2873
  function readOptionalFlag(args, flag) {
2767
2874
  const index = args.indexOf(flag);
2768
2875
  if (index === -1) {