@veewo/claw 0.2.3 → 0.2.4

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
@@ -2,16 +2,19 @@
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
+ import { createInterface } from "node:readline";
5
6
  import { pathToFileURL } from "node:url";
6
7
  import { createHash } from "node:crypto";
7
8
  import { spawn, spawnSync } from "node:child_process";
8
- import { buildDirectWorkflowGuidance, buildKnowledgeDelegateDispatch, buildKnowledgeAssignmentTemplate, buildKnowledgeWriterAssignments, DEFAULT_MAX_TASKS_TO_KEEP, checkProjectProtocol, ClawError, buildPlanWorkflowGuidance, shouldUsePlanHostIntegration, buildMemoryIndex, buildSessionStartDefaultPrompt, buildSessionStartRecoveredPrompt, editPlan, ensureProjectProtocol, enforceTaskRetention, findTaskDirectory, runDailyMaintenance, ingestTruth, initProject, getTemplateTaskDoneChoices, resolvePlanTemplateFile, resolvePlanEffectiveConfig, resolveProjectContext, resolveWorkflowProjectContext, resolveSessionWorkflowContext, deleteSessionWorkflow, sweepExpiredSessionWorkflows, resolveSessionBoundPlan, resolveContext, resolveSeedPlanTemplate, searchMemoryAsync, warmProjectMemoryEmbedding, showPlan, createSubplan, switchTask, tryCaptureKnowledgeStop, claimKnowledgeFinalizationJob, doneKnowledgeFinalizationJob, readKnowledgeFinalizationJob, waitForKnowledgeFinalizationJobReady, listRetryableKnowledgeFinalizationJobs, normalizeTruthMarkdownEncoding, recordKnowledgeFinalizationResult, unbindSession, writePlan, } from "@veewo/claw-core";
9
+ import { buildDirectWorkflowGuidance, buildKnowledgeDelegateDispatch, buildKnowledgeAssignmentTemplate, buildKnowledgeWriterAssignments, DEFAULT_MAX_TASKS_TO_KEEP, checkProjectProtocol, ClawError, buildPlanWorkflowGuidance, buildMemoryIndex, buildSessionStartDefaultPrompt, buildSessionStartRecoveredPrompt, editPlan, ensureProjectProtocol, enforceTaskRetention, findKnowledgeFinalizationJobPath, findTaskDirectory, runDailyMaintenance, ingestTruth, initProject, getTemplateTaskDoneChoices, resolvePlanTemplateFile, resolvePlanEffectiveConfig, resolveProjectContext, resolveWorkflowProjectContext, resolveSessionWorkflowContext, deleteSessionWorkflow, sweepExpiredSessionWorkflows, resolveSessionBoundPlan, resolveContext, resolveSeedPlanTemplate, searchMemoryAsync, warmProjectMemoryEmbedding, showPlan, createSubplan, switchTask, tryCaptureKnowledgeStop, claimKnowledgeFinalizationJob, doneKnowledgeFinalizationJob, readKnowledgeFinalizationJob, waitForKnowledgeFinalizationJobReady, listKnowledgeFinalizationJobs, listRetryableKnowledgeFinalizationJobs, normalizeTruthMarkdownEncoding, recordKnowledgeFinalizationResult, unbindSession, writePlan, } from "@veewo/claw-core";
9
10
  import { buildCodexDriverEnvelope } from "./codex-driver.js";
11
+ import { buildCodexHostActions } from "./codex-host-actions.js";
10
12
  import { checkCodexRuntime, resolveCodexSdkEntryPath } from "./codex-runtime.js";
11
13
  import { extractLatestFinalAssistantMessage, extractTaskDoneConclusions, } from "./codex-transcript.js";
12
14
  import { consumeBufferedHookInput } from "./knowledge-hook-preflight.js";
13
15
  import { resolveInvocationHost, withoutInvocationHost } from "./invocation-host.js";
14
16
  import { runOpencodeKnowledgeWriter } from "./opencode-runner.js";
17
+ import { ClawClient, ClawSessionError, } from "@veewo/claw-client";
15
18
  const CLI_VERSION = readCliVersion();
16
19
  const TOP_LEVEL_COMMANDS = [
17
20
  { name: "init [options]", summary: "Initialize and normalize the .claw project surface." },
@@ -54,8 +57,12 @@ const COMMAND_HELP = {
54
57
  ],
55
58
  },
56
59
  session: {
57
- usage: ["{script} session clean", "{script} session clean --expired"],
58
- description: "Clean ephemeral session-scoped workflow state without touching a project .claw directory.",
60
+ usage: [
61
+ "{script} session open <dir> <session-id>",
62
+ "{script} session clean",
63
+ "{script} session clean --expired",
64
+ ],
65
+ description: "Open a persistent claw command session or clean legacy ephemeral session workflow state.",
59
66
  },
60
67
  check: {
61
68
  usage: ["{script} check"],
@@ -160,10 +167,11 @@ const COMMAND_HELP = {
160
167
  ],
161
168
  },
162
169
  show: {
163
- usage: ["{script} plan show"],
170
+ usage: ["{script} plan show", "{script} plan show --simple"],
164
171
  description: "Show the session-bound current plan, including archived plans through an explicit override.",
165
172
  summary: "Show the current plan for a task.",
166
173
  options: [
174
+ { flag: "--simple", detail: "Return only status, goal.text, tasks[].title, and rules." },
167
175
  { flag: "--task-name <name>", detail: "Advanced: override the session-bound task scope." },
168
176
  { flag: "--plan-file <relative-path>", detail: "Advanced: override the session-bound plan file." },
169
177
  ],
@@ -295,10 +303,11 @@ const COMMAND_HELP = {
295
303
  ],
296
304
  },
297
305
  search: {
298
- usage: ["{script} search [<query>] [--limit <n>]", "{script} search index --refresh"],
306
+ usage: ["{script} search [<query>] [--dir <dir>] [--limit <n>]", "{script} search index --refresh"],
299
307
  description: "Project-scoped recall over .claw memory, truth, ADR, and declared markdown docs. Use a positional query or --query. Task-local scope (--task/--scope) is rejected; put task materials in plan.references instead.",
300
308
  options: [
301
309
  { flag: "--query <text>", detail: "Search query (or pass the query positionally)." },
310
+ { flag: "--dir <dir>", detail: "Override the project directory for this search only." },
302
311
  { flag: "--limit <n>", detail: "Max number of results." },
303
312
  ],
304
313
  subcommands: {
@@ -471,7 +480,7 @@ async function main() {
471
480
  printJson(buildPublicContextOutput(await runContextCommand(args, process.cwd(), resolveOwnerSessionKey(), effectiveHost)));
472
481
  return;
473
482
  case "session":
474
- runSession(args);
483
+ await runSession(args, effectiveHost);
475
484
  return;
476
485
  case "check":
477
486
  const checkResult = ensureProjectProtocol(process.cwd());
@@ -489,7 +498,7 @@ async function main() {
489
498
  await runPlan(args, effectiveHost);
490
499
  return;
491
500
  case "codex":
492
- runCodex(args);
501
+ await runCodex(args);
493
502
  return;
494
503
  case "template":
495
504
  await runTemplate(args);
@@ -567,16 +576,55 @@ async function main() {
567
576
  handleError(error);
568
577
  }
569
578
  }
570
- function runCodex(args) {
579
+ async function runCodex(args) {
571
580
  const subcommand = args.shift();
572
- if (subcommand !== "driver") {
573
- throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown codex subcommand "${subcommand ?? ""}".`);
581
+ if (subcommand === "driver") {
582
+ assertNoRemainingArgs(args, "codex driver");
583
+ printJson(buildCodexDriverEnvelope(CLI_VERSION));
584
+ return;
574
585
  }
575
- assertNoRemainingArgs(args, "codex driver");
576
- printJson(buildCodexDriverEnvelope(CLI_VERSION));
586
+ if (subcommand === "invoke") {
587
+ const encoded = args.shift();
588
+ assertNoRemainingArgs(args, "codex invoke");
589
+ if (!encoded || !/^(?:[a-f0-9]{4})+$/i.test(encoded)) {
590
+ throw new ClawError("PROJECT_CONFIG_INVALID", "codex invoke requires one UTF-16 hex argv payload.");
591
+ }
592
+ let json = "";
593
+ for (let index = 0; index < encoded.length; index += 4) {
594
+ json += String.fromCharCode(Number.parseInt(encoded.slice(index, index + 4), 16));
595
+ }
596
+ const invocation = JSON.parse(json);
597
+ if (!Array.isArray(invocation)
598
+ || invocation.length === 0
599
+ || invocation.some((value) => typeof value !== "string")
600
+ || !["plan", "task", "subplan"].includes(invocation[0])
601
+ || invocation.some((value) => value === "--host")) {
602
+ throw new ClawError("PROJECT_CONFIG_INVALID", "codex invoke accepts only a structured plan, task, or subplan argv without --host.");
603
+ }
604
+ const originalArgv = process.argv;
605
+ try {
606
+ process.argv = [originalArgv[0], originalArgv[1], ...invocation, "--host", "codex"];
607
+ await main();
608
+ }
609
+ finally {
610
+ process.argv = originalArgv;
611
+ }
612
+ return;
613
+ }
614
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown codex subcommand "${subcommand ?? ""}".`);
577
615
  }
578
- function runSession(args) {
616
+ async function runSession(args, effectiveHost) {
579
617
  const subcommand = args.shift();
618
+ if (subcommand === "open") {
619
+ const workdir = args.shift();
620
+ const agentSessionId = args.shift();
621
+ assertNoRemainingArgs(args, "session open");
622
+ if (!workdir || !agentSessionId) {
623
+ throw new ClawError("PROJECT_CONFIG_INVALID", "session open requires <dir> <session-id> in that order.");
624
+ }
625
+ await runPersistentSession(workdir, agentSessionId, effectiveHost);
626
+ return;
627
+ }
580
628
  if (subcommand !== "clean") {
581
629
  throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown session subcommand "${subcommand ?? ""}".`);
582
630
  }
@@ -597,19 +645,390 @@ function runSession(args) {
597
645
  removed: deleteSessionWorkflow(ownerSessionKey),
598
646
  });
599
647
  }
648
+ async function runPersistentSession(workdir, agentSessionId, effectiveHost) {
649
+ const opened = await new ClawClient({
650
+ clientKind: "terminal",
651
+ ...(effectiveHost ? { host: effectiveHost } : {}),
652
+ }).open(agentSessionId, path.resolve(workdir));
653
+ writeSessionOutput({
654
+ ok: true,
655
+ command: "session.open",
656
+ ...opened.openResult,
657
+ });
658
+ const readline = createInterface({
659
+ input: process.stdin,
660
+ output: process.stdout,
661
+ terminal: Boolean(process.stdin.isTTY && process.stdout.isTTY),
662
+ prompt: "claw> ",
663
+ });
664
+ if (process.stdin.isTTY && process.stdout.isTTY)
665
+ readline.prompt();
666
+ try {
667
+ for await (const line of readline) {
668
+ const trimmed = line.trim();
669
+ if (!trimmed) {
670
+ if (process.stdin.isTTY && process.stdout.isTTY)
671
+ readline.prompt();
672
+ continue;
673
+ }
674
+ try {
675
+ const command = parsePersistentSessionCommand(trimmed);
676
+ if (command.kind === "close") {
677
+ await opened.close();
678
+ writeSessionOutput({ ok: true, command: "session.close" });
679
+ return;
680
+ }
681
+ if (command.kind === "status") {
682
+ writeSessionOutput({ ok: true, command: "session.status", ...await opened.status() });
683
+ }
684
+ else {
685
+ const envelope = await opened.commandEnvelope(command.request);
686
+ writeSessionOutput({
687
+ ok: true,
688
+ command: command.request.operation,
689
+ ...envelope,
690
+ });
691
+ }
692
+ }
693
+ catch (error) {
694
+ writeSessionOutput({ ok: false, error: serializeSessionError(error) });
695
+ if (error instanceof ClawSessionError && error.code === "SESSION_CONNECTION_LOST")
696
+ return;
697
+ }
698
+ if (process.stdin.isTTY && process.stdout.isTTY)
699
+ readline.prompt();
700
+ }
701
+ }
702
+ finally {
703
+ readline.close();
704
+ try {
705
+ await opened.close();
706
+ }
707
+ catch {
708
+ // EOF is a soft close; retained state survives a lost daemon connection.
709
+ }
710
+ }
711
+ }
712
+ function parsePersistentSessionCommand(line) {
713
+ if (line.startsWith("{")) {
714
+ const request = JSON.parse(line);
715
+ if (typeof request.operation !== "string") {
716
+ throw new ClawError("PROJECT_CONFIG_INVALID", "JSON session commands require operation.");
717
+ }
718
+ return {
719
+ kind: "command",
720
+ request: { operation: request.operation, input: request.input ?? {} },
721
+ };
722
+ }
723
+ const tokens = tokenizeSessionLine(line);
724
+ const group = tokens.shift();
725
+ const action = tokens.shift();
726
+ if ((group === "session" && action === "close") || group === "exit" || group === "quit") {
727
+ return { kind: "close" };
728
+ }
729
+ if ((group === "session" && action === "status") || group === "status") {
730
+ return { kind: "status" };
731
+ }
732
+ if (group === "plan" && action === "show") {
733
+ const simple = consumeSessionBoolean(tokens, "--simple");
734
+ assertSessionTokensConsumed(tokens, line);
735
+ return {
736
+ kind: "command",
737
+ request: { operation: "plan.show", input: { simple } },
738
+ };
739
+ }
740
+ if (group === "plan" && action === "leave") {
741
+ assertSessionTokensConsumed(tokens, line);
742
+ return { kind: "command", request: { operation: "plan.leave", input: {} } };
743
+ }
744
+ if (group === "plan" && action === "resume") {
745
+ const planId = tokens.shift();
746
+ assertSessionTokensConsumed(tokens, line);
747
+ return {
748
+ kind: "command",
749
+ request: { operation: "plan.resume", input: planId ? { planId } : {} },
750
+ };
751
+ }
752
+ if (group === "plan" && action === "create") {
753
+ const title = consumeSessionFlag(tokens, "--title") ?? tokens.shift();
754
+ if (!title)
755
+ throw new ClawError("PROJECT_CONFIG_INVALID", "plan create requires a title.");
756
+ const goalText = consumeSessionFlag(tokens, "--goal");
757
+ assertSessionTokensConsumed(tokens, line);
758
+ return {
759
+ kind: "command",
760
+ request: {
761
+ operation: "plan.create",
762
+ input: { title, ...(goalText ? { goalText } : {}) },
763
+ },
764
+ };
765
+ }
766
+ if (group === "plan" && action === "wait") {
767
+ assertSessionTokensConsumed(tokens, line);
768
+ return { kind: "command", request: { operation: "plan.wait", input: {} } };
769
+ }
770
+ if (group === "plan" && action === "edit") {
771
+ const operations = parseSessionPlanEditOperations(tokens, line);
772
+ return {
773
+ kind: "command",
774
+ request: { operation: "plan.edit", input: { operations } },
775
+ };
776
+ }
777
+ if (group === "plan" && action === "done") {
778
+ const retrospectiveSummary = consumeSessionFlag(tokens, "--retrospective");
779
+ if (!retrospectiveSummary) {
780
+ throw new ClawError("RETROSPECTIVE_REQUIRED", "plan done requires --retrospective.");
781
+ }
782
+ const keyDecisions = consumeAllSessionFlags(tokens, "--key-decision");
783
+ const whatWorked = consumeAllSessionFlags(tokens, "--what-worked");
784
+ const issues = consumeAllSessionFlags(tokens, "--issue");
785
+ const followUps = consumeAllSessionFlags(tokens, "--follow-up");
786
+ assertSessionTokensConsumed(tokens, line);
787
+ return {
788
+ kind: "command",
789
+ request: {
790
+ operation: "plan.done",
791
+ input: {
792
+ retrospectiveSummary,
793
+ ...(keyDecisions.length ? { keyDecisions } : {}),
794
+ ...(whatWorked.length ? { whatWorked } : {}),
795
+ ...(issues.length ? { issues } : {}),
796
+ ...(followUps.length ? { followUps } : {}),
797
+ },
798
+ },
799
+ };
800
+ }
801
+ if (group === "task" && action === "add") {
802
+ const tasks = [];
803
+ while (tokens.length > 0) {
804
+ if (tokens.shift() !== "--title") {
805
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task add expects --title to start each task.");
806
+ }
807
+ const title = tokens.shift();
808
+ if (!title)
809
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task add requires a title.");
810
+ let detail;
811
+ if (tokens[0] === "--detail") {
812
+ tokens.shift();
813
+ detail = tokens.shift();
814
+ if (!detail)
815
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task add --detail requires a value.");
816
+ }
817
+ tasks.push({ title, ...(detail ? { detail } : {}) });
818
+ }
819
+ if (!tasks.length)
820
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task add requires at least one task.");
821
+ return { kind: "command", request: { operation: "task.add", input: { tasks } } };
822
+ }
823
+ if (group === "task" && action === "edit") {
824
+ const id = Number(consumeSessionFlag(tokens, "--id"));
825
+ if (!Number.isInteger(id))
826
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task edit requires integer --id.");
827
+ const taskTitle = consumeSessionFlag(tokens, "--title");
828
+ const taskDetail = consumeSessionFlag(tokens, "--detail");
829
+ const taskStatus = consumeSessionFlag(tokens, "--status");
830
+ const taskChoiceId = consumeSessionFlag(tokens, "--choice");
831
+ if (!taskTitle && !taskDetail && !taskStatus && !taskChoiceId) {
832
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task edit requires a field to change.");
833
+ }
834
+ assertSessionTokensConsumed(tokens, line);
835
+ return {
836
+ kind: "command",
837
+ request: {
838
+ operation: "task.edit",
839
+ input: {
840
+ taskId: id,
841
+ ...(taskTitle ? { taskTitle } : {}),
842
+ ...(taskDetail ? { taskDetail } : {}),
843
+ ...(taskStatus ? { taskStatus } : {}),
844
+ ...(taskChoiceId ? { taskChoiceId } : {}),
845
+ },
846
+ },
847
+ };
848
+ }
849
+ if (group === "task" && action === "done") {
850
+ const tasks = [];
851
+ while (tokens.length > 0) {
852
+ if (tokens.shift() !== "--id") {
853
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task done expects --id to start each task.");
854
+ }
855
+ const id = Number(tokens.shift());
856
+ if (!Number.isInteger(id))
857
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task done requires integer ids.");
858
+ let choiceId;
859
+ if (tokens[0] === "--choice") {
860
+ tokens.shift();
861
+ choiceId = tokens.shift();
862
+ if (!choiceId)
863
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task done --choice requires a value.");
864
+ }
865
+ tasks.push({ id, ...(choiceId ? { choiceId } : {}) });
866
+ }
867
+ if (!tasks.length)
868
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task done requires at least one task.");
869
+ return { kind: "command", request: { operation: "task.done", input: { tasks } } };
870
+ }
871
+ if (group === "search") {
872
+ const query = consumeSessionFlag(tokens, "--query") ?? action;
873
+ if (!query)
874
+ throw new ClawError("MEMORY_QUERY_REQUIRED", "search requires a query.");
875
+ const dir = consumeSessionFlag(tokens, "--dir");
876
+ const limitRaw = consumeSessionFlag(tokens, "--limit");
877
+ assertSessionTokensConsumed(tokens, line);
878
+ return {
879
+ kind: "command",
880
+ request: {
881
+ operation: "search",
882
+ input: {
883
+ query,
884
+ ...(dir ? { dir } : {}),
885
+ ...(limitRaw ? { limit: Number(limitRaw) } : {}),
886
+ },
887
+ },
888
+ };
889
+ }
890
+ throw new ClawError("SESSION_OPERATION_UNSUPPORTED", `Unsupported persistent session command: ${line}`);
891
+ }
892
+ function tokenizeSessionLine(line) {
893
+ const tokens = [];
894
+ const pattern = /"((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'|(\S+)/g;
895
+ for (const match of line.matchAll(pattern)) {
896
+ tokens.push((match[1] ?? match[2] ?? match[3] ?? "").replace(/\\(["'\\])/g, "$1"));
897
+ }
898
+ return tokens;
899
+ }
900
+ function consumeSessionFlag(tokens, flag) {
901
+ const index = tokens.indexOf(flag);
902
+ if (index < 0)
903
+ return undefined;
904
+ const value = tokens[index + 1];
905
+ if (!value)
906
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Missing value for ${flag}.`);
907
+ tokens.splice(index, 2);
908
+ return value;
909
+ }
910
+ function consumeAllSessionFlags(tokens, flag) {
911
+ const values = [];
912
+ while (tokens.includes(flag)) {
913
+ values.push(consumeSessionFlag(tokens, flag));
914
+ }
915
+ return values;
916
+ }
917
+ function parseSessionPlanEditOperations(tokens, line) {
918
+ const operations = [];
919
+ const updateFlags = {
920
+ "--goal": "goalText",
921
+ "--requirements": "requirementsSummary",
922
+ "--summary": "planSummary",
923
+ "--rule": "rules",
924
+ "--key-decision": "keyDecisions",
925
+ };
926
+ while (tokens.length > 0) {
927
+ const flag = tokens.shift();
928
+ const value = tokens.shift();
929
+ if (!value)
930
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Missing value for ${flag}.`);
931
+ if (flag === "--status") {
932
+ operations.push({ type: "plan.status", status: value });
933
+ continue;
934
+ }
935
+ const field = updateFlags[flag];
936
+ if (!field) {
937
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown plan edit argument in "${line}": ${flag}`);
938
+ }
939
+ const listField = field === "rules" || field === "keyDecisions";
940
+ operations.push({
941
+ type: "plan.update",
942
+ updates: { [field]: listField ? [value] : value },
943
+ });
944
+ }
945
+ if (!operations.length)
946
+ throw new ClawError("PROJECT_CONFIG_INVALID", "plan edit requires a field to change.");
947
+ return operations;
948
+ }
949
+ function consumeSessionBoolean(tokens, flag) {
950
+ const index = tokens.indexOf(flag);
951
+ if (index < 0)
952
+ return false;
953
+ tokens.splice(index, 1);
954
+ return true;
955
+ }
956
+ function assertSessionTokensConsumed(tokens, line) {
957
+ if (tokens.length > 0) {
958
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown session command arguments in "${line}": ${tokens.join(" ")}`);
959
+ }
960
+ }
961
+ function writeSessionOutput(value) {
962
+ process.stdout.write(`${JSON.stringify(value)}\n`);
963
+ }
964
+ function serializeSessionError(error) {
965
+ if (error instanceof ClawSessionError) {
966
+ return {
967
+ code: error.code,
968
+ message: error.message,
969
+ retryable: error.retryable,
970
+ outcome: error.outcome,
971
+ ...(error.recoveryCommand ? { recoveryCommand: error.recoveryCommand } : {}),
972
+ ...(error.details ? { details: error.details } : {}),
973
+ };
974
+ }
975
+ if (error instanceof ClawError) {
976
+ return { code: error.code, message: error.message, ...(error.details ? { details: error.details } : {}) };
977
+ }
978
+ return { code: "SESSION_COMMAND_FAILED", message: error instanceof Error ? error.message : String(error) };
979
+ }
600
980
  function runKnowledge(args) {
601
981
  const subcommand = args.shift();
602
982
  switch (subcommand) {
983
+ case "list": {
984
+ const projectRoot = path.resolve(readRequiredFlag(args, "--project-root"));
985
+ const sessionKey = readOptionalFlag(args, "--session-key");
986
+ const host = readOptionalFlag(args, "--job-host");
987
+ assertNoRemainingArgs(args, "knowledge list");
988
+ const sessionProject = sessionKey ? resolveSessionWorkflowContext(sessionKey) : null;
989
+ const project = resolveProjectContext(projectRoot);
990
+ const candidates = sessionProject && sessionProject.clawDir !== project.clawDir
991
+ ? [sessionProject, project]
992
+ : [sessionProject ?? project];
993
+ const jobs = candidates.flatMap((candidate) => listKnowledgeFinalizationJobs(candidate))
994
+ .map((jobPath) => ({ jobPath, job: readKnowledgeFinalizationJob(jobPath) }))
995
+ .filter(({ job }) => (job.status === "running"
996
+ || ((job.status === "queued" || job.status === "failed") && job.attempts < 3)) && (!host || job.host === host))
997
+ .map(({ jobPath, job }) => ({
998
+ jobPath,
999
+ finalizeId: job.finalizeId,
1000
+ status: job.status,
1001
+ attempts: job.attempts,
1002
+ }));
1003
+ printJson({ ok: true, command: "knowledge.list", jobs });
1004
+ return;
1005
+ }
603
1006
  case "wait": {
604
1007
  const projectRoot = path.resolve(readRequiredFlag(args, "--project-root"));
605
1008
  const finalizeId = readRequiredFlag(args, "--finalize-id");
1009
+ const sessionKey = readOptionalFlag(args, "--session-key");
606
1010
  const timeoutMs = readOptionalNumber(args, "--timeout-ms") ?? 300_000;
607
1011
  assertNoRemainingArgs(args, "knowledge wait");
608
- const { jobPath, job } = waitForKnowledgeFinalizationJobReady({
609
- project: resolveProjectContext(projectRoot),
610
- finalizeId,
611
- timeoutMs,
612
- });
1012
+ const sessionProject = sessionKey ? resolveSessionWorkflowContext(sessionKey) : null;
1013
+ const project = resolveProjectContext(projectRoot);
1014
+ const candidates = sessionProject && sessionProject.clawDir !== project.clawDir
1015
+ ? [sessionProject, project]
1016
+ : [sessionProject ?? project];
1017
+ let located = candidates
1018
+ .map((candidate) => findKnowledgeFinalizationJobPath(candidate, finalizeId))
1019
+ .find((candidate) => Boolean(candidate));
1020
+ if (!located && timeoutMs > 0) {
1021
+ located = waitForKnowledgeFinalizationJobReady({
1022
+ project: candidates[0],
1023
+ finalizeId,
1024
+ timeoutMs,
1025
+ }).jobPath;
1026
+ }
1027
+ if (!located) {
1028
+ throw new Error(`Knowledge finalization ${finalizeId} is unavailable.`);
1029
+ }
1030
+ const jobPath = located;
1031
+ const job = readKnowledgeFinalizationJob(jobPath);
613
1032
  printJson({
614
1033
  ok: true,
615
1034
  command: "knowledge.wait",
@@ -715,11 +1134,11 @@ async function runPlan(args, effectiveHost) {
715
1134
  throw new ClawError("PROJECT_CONFIG_INVALID", "plan edit requires at least one plan field or --status.");
716
1135
  }
717
1136
  const ownerSessionKey = resolveOwnerSessionKey() ?? undefined;
718
- const entersEndState = requestsPlanEndState(operations);
719
- const current = entersEndState
1137
+ const entersEndTerminal = requestsPlanEndTerminal(operations);
1138
+ const current = entersEndTerminal
720
1139
  ? showPlan({ cwd: process.cwd(), ...target, ownerSessionKey })
721
1140
  : undefined;
722
- const project = entersEndState ? tryResolveHookProject(process.cwd()) : null;
1141
+ const project = entersEndTerminal ? tryResolveHookProject(process.cwd()) : null;
723
1142
  const effectiveWriter = current && project
724
1143
  ? resolvePlanEffectiveConfig(project.projectConfig, current.plan)?.knowledgeWriter
725
1144
  : undefined;
@@ -729,7 +1148,7 @@ async function runPlan(args, effectiveHost) {
729
1148
  && effectiveHost !== "codex") {
730
1149
  throw new ClawError("PROJECT_CONFIG_INVALID", 'knowledgeWriter.executionPolicy "subagent" is supported only by the Codex host.', { host: effectiveHost ?? null });
731
1150
  }
732
- const queuePlanEndFinalization = entersEndState
1151
+ const queuePlanEndFinalization = entersEndTerminal
733
1152
  ? preparePlanEndFinalization(process.cwd(), ownerSessionKey)
734
1153
  : undefined;
735
1154
  const result = await editPlan({
@@ -857,6 +1276,7 @@ async function runPlan(args, effectiveHost) {
857
1276
  return;
858
1277
  }
859
1278
  case "show": {
1279
+ const simple = readBooleanFlag(args, "--simple");
860
1280
  const target = readPlanMutationTarget(args);
861
1281
  assertNoRemainingArgs(args, "plan show");
862
1282
  const result = showPlan({
@@ -864,6 +1284,10 @@ async function runPlan(args, effectiveHost) {
864
1284
  ...target,
865
1285
  ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
866
1286
  });
1287
+ if (simple) {
1288
+ printJson(result.simplePlanView);
1289
+ return;
1290
+ }
867
1291
  printJson({
868
1292
  ok: true,
869
1293
  command: "plan.show",
@@ -1052,11 +1476,12 @@ async function runSearch(args) {
1052
1476
  if (args.includes("--scope") || args.includes("--task")) {
1053
1477
  throw new ClawError("PROJECT_CONFIG_INVALID", "claw search is project-scoped only. Put task-specific materials in plan.references instead of using task-local search.");
1054
1478
  }
1479
+ const dir = readOptionalFlag(args, "--dir");
1055
1480
  printJson({
1056
1481
  ok: true,
1057
1482
  command: "search",
1058
1483
  ...await searchMemoryAsync({
1059
- cwd: process.cwd(),
1484
+ cwd: dir ? path.resolve(process.cwd(), dir) : process.cwd(),
1060
1485
  limit: readOptionalNumber(args, "--limit"),
1061
1486
  query: readRequiredSearchQuery(args),
1062
1487
  scope: "project",
@@ -1269,7 +1694,7 @@ function buildPublicContextOutput(context) {
1269
1694
  if (Object.keys(compactRecovery).length > 0) {
1270
1695
  output.startupRecovery = compactRecovery;
1271
1696
  }
1272
- const searchGuidance = buildContextSearchGuidance(context);
1697
+ const searchGuidance = buildContextSearchGuidance(context, "rg");
1273
1698
  if (searchGuidance) {
1274
1699
  output.searchGuidance = searchGuidance;
1275
1700
  }
@@ -1281,20 +1706,26 @@ function shouldExposeVersionSync(versionSync) {
1281
1706
  || versionSync.updateAvailable === true
1282
1707
  || versionSync.projectVersion !== versionSync.cliVersion;
1283
1708
  }
1284
- function buildContextSearchGuidance(context) {
1709
+ function buildContextSearchGuidance(context, style = "default") {
1285
1710
  const project = asJsonRecord(context.project);
1286
1711
  const projectConfig = asJsonRecord(project?.projectConfig);
1287
1712
  const memory = asJsonRecord(projectConfig?.memory);
1288
1713
  const embeddingEnabled = memory?.enabled === true && asJsonRecord(memory.embedding) !== null;
1289
1714
  const gitnexusEnabled = projectConfig?.gitnexus === true;
1290
1715
  if (embeddingEnabled && gitnexusEnabled) {
1291
- return "When useful, use `claw search` to narrow the document search scope and GitNexus to narrow the code search scope, then use the default search to locate exact files or symbols.";
1716
+ return style === "rg"
1717
+ ? "Before using `rg`, use `claw search --query` to narrow the document search scope and GitNexus to narrow the code search scope, then use `rg` to locate exact files or symbols."
1718
+ : "When useful, use `claw search` to narrow the document search scope and GitNexus to narrow the code search scope, then use the default search to locate exact files or symbols.";
1292
1719
  }
1293
1720
  if (embeddingEnabled) {
1294
- return "When useful, use `claw search` to narrow the document search scope, then use the default search to locate exact files or symbols.";
1721
+ return style === "rg"
1722
+ ? "Before using `rg`, use `claw search --query` to narrow the document search scope, then use `rg` to locate exact files or symbols."
1723
+ : "When useful, use `claw search` to narrow the document search scope, then use the default search to locate exact files or symbols.";
1295
1724
  }
1296
1725
  if (gitnexusEnabled) {
1297
- return "When useful, use GitNexus to narrow the code search scope, then use the default search to locate exact files or symbols.";
1726
+ return style === "rg"
1727
+ ? "Before using `rg`, use GitNexus to narrow the code search scope, then use `rg` to locate exact files or symbols."
1728
+ : "When useful, use GitNexus to narrow the code search scope, then use the default search to locate exact files or symbols.";
1298
1729
  }
1299
1730
  return null;
1300
1731
  }
@@ -1505,12 +1936,12 @@ async function runInternalKnowledgeCapture(args, effectiveHost) {
1505
1936
  const turnId = readHookString(payload, "turn_id");
1506
1937
  const message = readHookString(payload, "message");
1507
1938
  const taskConclusions = readHookTaskConclusions(payload, turnId);
1508
- if (!hookCwd || !sessionId || !turnId || !message || !containsClawDir(hookCwd)) {
1939
+ if (!hookCwd || !sessionId || !turnId || !message) {
1509
1940
  printJson({ ok: true, captured: false });
1510
1941
  return;
1511
1942
  }
1512
1943
  try {
1513
- const project = resolveProjectContext(hookCwd);
1944
+ const project = resolveWorkflowProjectContext(hookCwd, sessionId);
1514
1945
  const result = tryCaptureKnowledgeStop({
1515
1946
  project,
1516
1947
  sessionId,
@@ -1592,7 +2023,7 @@ function completeKnowledgeFinalizationJob(jobPath, result, claimToken) {
1592
2023
  if (running.claimToken !== claimToken) {
1593
2024
  throw new Error("Knowledge finalization completion does not match the active claim.");
1594
2025
  }
1595
- const project = resolveProjectContext(running.projectRoot);
2026
+ const project = resolveKnowledgeJobProject(jobPath, running);
1596
2027
  const finishedAt = new Date().toISOString();
1597
2028
  const truthEncoding = normalizeTruthMarkdownEncoding(project);
1598
2029
  recordKnowledgeFinalizationResult(project, running.reportPath, {
@@ -1626,6 +2057,17 @@ function completeKnowledgeFinalizationJob(jobPath, result, claimToken) {
1626
2057
  });
1627
2058
  printJson({ ok: true, completed: true, alreadyDone: terminal.alreadyDone, finalizeId: running.finalizeId });
1628
2059
  }
2060
+ function resolveKnowledgeJobProject(jobPath, job) {
2061
+ const project = resolveProjectContext(job.projectRoot);
2062
+ const sessionProject = resolveSessionWorkflowContext(job.sessionId);
2063
+ for (const candidate of sessionProject ? [sessionProject, project] : [project]) {
2064
+ const relative = path.relative(candidate.clawDir, path.resolve(jobPath));
2065
+ if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
2066
+ return candidate;
2067
+ }
2068
+ }
2069
+ throw new Error("Knowledge finalization job is outside its project or session workflow.");
2070
+ }
1629
2071
  function failKnowledgeFinalizationJob(jobPath, message, claimToken) {
1630
2072
  const terminal = doneKnowledgeFinalizationJob({
1631
2073
  jobPath,
@@ -1677,7 +2119,7 @@ function runInternalKnowledgeSweep(args) {
1677
2119
  printJson({ command: "internal-knowledge-sweep", ok: true, launched: 0, skipped: true });
1678
2120
  return;
1679
2121
  }
1680
- const jobs = listRetryableKnowledgeFinalizationJobs(project);
2122
+ const jobs = listRetryableKnowledgeFinalizationJobs(project, { excludeHosts: ["cindy"] });
1681
2123
  let launched = 0;
1682
2124
  for (const jobPath of jobs) {
1683
2125
  try {
@@ -1714,7 +2156,7 @@ async function runInternalBackgroundMaintenance(args) {
1714
2156
  let launched = 0;
1715
2157
  const knowledgeProject = project ?? sessionProject;
1716
2158
  if (knowledgeProject) {
1717
- const jobs = listRetryableKnowledgeFinalizationJobs(knowledgeProject);
2159
+ const jobs = listRetryableKnowledgeFinalizationJobs(knowledgeProject, { excludeHosts: ["cindy"] });
1718
2160
  discovered = jobs.length;
1719
2161
  for (const jobPath of jobs) {
1720
2162
  try {
@@ -1914,7 +2356,7 @@ async function runSessionStartHook(effectiveHost) {
1914
2356
  const context = await runContextCommand([], hookCwd, ownerSessionKey, effectiveHost);
1915
2357
  const contextProject = asJsonRecord(context.project);
1916
2358
  const retryableJobs = effectiveHost !== "cindy" && contextProject?.scope !== "session" && !context.error
1917
- ? listRetryableKnowledgeFinalizationJobs(resolveProjectContext(hookCwd))
2359
+ ? listRetryableKnowledgeFinalizationJobs(resolveProjectContext(hookCwd), { excludeHosts: ["cindy"] })
1918
2360
  : [];
1919
2361
  if (contextProject?.scope !== "session"
1920
2362
  && !context.error
@@ -2071,31 +2513,21 @@ function buildSessionStartAdditionalContext(context, sessionCwd, effectiveHost)
2071
2513
  * supplies project identity or a recovered workflow snapshot. Goal Mode is
2072
2514
  * intentionally omitted because Cindy does not expose that Host surface.
2073
2515
  */
2074
- function buildCindySessionStartContext(context, sessionCwd, versionSyncPrompt) {
2075
- const activeWorkflow = asJsonRecord(context.activeWorkflow);
2076
- if (activeWorkflow) {
2077
- return stripCindyGoalModeLines(buildRecoveredWorkflowAdditionalContext(activeWorkflow, versionSyncPrompt));
2516
+ function buildCindySessionStartContext(context, _sessionCwd, versionSyncPrompt) {
2517
+ const lines = [];
2518
+ const startupRecovery = asJsonRecord(context.startupRecovery);
2519
+ const fixedPaths = Array.isArray(startupRecovery?.fixedPaths)
2520
+ ? startupRecovery.fixedPaths.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
2521
+ : [];
2522
+ if (startupRecovery?.corrected === true) {
2523
+ lines.push(`claw-kit repaired the project configuration${fixedPaths.length > 0 ? `: ${fixedPaths.join(", ")}` : "."}`);
2078
2524
  }
2079
- const project = asJsonRecord(context.project);
2080
- if (!project)
2081
- return null;
2082
- const projectName = typeof project.projectName === "string" && project.projectName.trim()
2083
- ? project.projectName.trim()
2084
- : typeof project.projectId === "string" && project.projectId.trim()
2085
- ? project.projectId.trim()
2086
- : path.basename(String(project.projectRoot ?? sessionCwd ?? "project"));
2087
- const projectRoot = typeof project.projectRoot === "string" ? project.projectRoot : sessionCwd;
2088
- const projectId = typeof project.projectId === "string" ? project.projectId : projectName;
2089
- const clawDir = typeof project.clawDir === "string" ? project.clawDir : path.join(projectRoot, ".claw");
2090
- const prompt = [
2091
- `This session started inside a .claw project: ${projectName} (${projectId}).`,
2092
- `.claw directory: ${clawDir}`,
2093
- ].join("\n");
2094
- if (!versionSyncPrompt)
2095
- return prompt;
2096
- return versionSyncPrompt.placement === "prefix"
2097
- ? `${versionSyncPrompt.lines.join("\n")}\n${prompt}`
2098
- : `${prompt}\n${versionSyncPrompt.lines.join("\n")}`;
2525
+ if (versionSyncPrompt?.placement === "prefix")
2526
+ lines.push(...versionSyncPrompt.lines);
2527
+ lines.push("Before using `rg`, use `claw search --query` to narrow the document search scope and GitNexus to narrow the code search scope, then use `rg` to locate exact files or symbols.");
2528
+ if (versionSyncPrompt?.placement === "suffix")
2529
+ lines.push(...versionSyncPrompt.lines);
2530
+ return lines.length > 0 ? lines.join("\n") : null;
2099
2531
  }
2100
2532
  function stripCindyGoalModeLines(prompt) {
2101
2533
  return prompt
@@ -2386,7 +2818,7 @@ function compactPlanCommandResult(command, result, effectiveHost, completionRefr
2386
2818
  const resolvedPlanPath = archivedPlanPath ?? result.planPath;
2387
2819
  const codexResult = effectiveHost === "codex";
2388
2820
  const cindyResult = effectiveHost === "cindy";
2389
- const hostActions = codexResult ? buildHostActions(result, { forceProjectionSync, actionIdPrefix: command === "plan.sync" ? `plan.sync:${createHash("sha256").update(result.planPath).digest("hex").slice(0, 16)}` : undefined }) : [];
2821
+ const hostActions = codexResult ? buildCodexHostActions(result, { forceProjectionSync, actionIdPrefix: command === "plan.sync" ? `plan.sync:${createHash("sha256").update(result.planPath).digest("hex").slice(0, 16)}` : undefined }) : [];
2390
2822
  const nextsteps = codexResult
2391
2823
  && result.planStatus === "end.completed"
2392
2824
  && result.workflowGuidance.goalTool?.tool === "update_goal"
@@ -2461,87 +2893,6 @@ function compactPlanCommandResult(command, result, effectiveHost, completionRefr
2461
2893
  ...(!codexResult || !includePlan ? { planSummary } : {}),
2462
2894
  };
2463
2895
  }
2464
- function buildHostActions(result, options = {}) {
2465
- const latestEvent = result.events?.at(-1);
2466
- const actionIdPrefix = options.actionIdPrefix ?? latestEvent?.mutationId;
2467
- if (!actionIdPrefix) {
2468
- return [];
2469
- }
2470
- const actions = [];
2471
- const goalTool = result.workflowGuidance.goalTool;
2472
- const isSubplanGoalHandoff = Boolean(result.plan?.parentPlan
2473
- && goalTool?.tool === "update_goal"
2474
- && goalTool.status === "complete");
2475
- if (isSubplanGoalHandoff && goalTool?.tool === "update_goal") {
2476
- actions.push({
2477
- schemaVersion: 1,
2478
- id: `${actionIdPrefix}:update_goal`,
2479
- tool: "update_goal",
2480
- input: {
2481
- status: "complete",
2482
- },
2483
- });
2484
- }
2485
- if (result.plan
2486
- && shouldUsePlanHostIntegration(result.plan)
2487
- && result.plan.tasks.length > 0
2488
- && (options.forceProjectionSync || codexPlanProjectionChanged(result.previousPlan, result.plan, result.planStatus))) {
2489
- const plan = buildCodexPlanProjection(result.plan, result.planStatus);
2490
- actions.push({
2491
- schemaVersion: 1,
2492
- id: `${actionIdPrefix}:update_plan`,
2493
- tool: "update_plan",
2494
- input: {
2495
- explanation: result.workflowGuidance.summary,
2496
- plan,
2497
- },
2498
- });
2499
- }
2500
- if (goalTool && !isSubplanGoalHandoff) {
2501
- if (goalTool.tool === "create_goal") {
2502
- actions.push({
2503
- schemaVersion: 1,
2504
- id: `${actionIdPrefix}:create_goal`,
2505
- tool: "create_goal",
2506
- input: {
2507
- objective: goalTool.objective,
2508
- },
2509
- });
2510
- }
2511
- else {
2512
- const codexStatus = result.planStatus === "process.wait" || result.planStatus === "process.discussing"
2513
- ? "complete"
2514
- : goalTool.status;
2515
- actions.push({
2516
- schemaVersion: 1,
2517
- id: `${actionIdPrefix}:update_goal`,
2518
- tool: "update_goal",
2519
- input: {
2520
- status: codexStatus,
2521
- },
2522
- });
2523
- }
2524
- }
2525
- return actions;
2526
- }
2527
- function codexPlanProjectionChanged(previousPlan, plan, planStatus) {
2528
- if (!previousPlan)
2529
- return true;
2530
- return JSON.stringify(buildCodexPlanProjection(previousPlan, previousPlan.status))
2531
- !== JSON.stringify(buildCodexPlanProjection(plan, planStatus));
2532
- }
2533
- function buildCodexPlanProjection(plan, planStatus) {
2534
- const activeTask = plan.tasks.find((task) => task.status === "in_progress" || task.status === "subagent_running") ?? (planStatus === "process.active"
2535
- ? plan.tasks.find((task) => task.status !== "done")
2536
- : undefined);
2537
- return plan.tasks.map((task) => {
2538
- let status = task.status === "done" ? "completed" : "pending";
2539
- if (task.id === activeTask?.id) {
2540
- status = "in_progress";
2541
- }
2542
- return { step: task.title, status };
2543
- });
2544
- }
2545
2896
  function compactDirectCommandResult(command, workflowGuidance, completionRefresh) {
2546
2897
  return {
2547
2898
  ok: true,
@@ -2629,7 +2980,7 @@ function readOrderedPlanEditOperations(args) {
2629
2980
  }
2630
2981
  return operations;
2631
2982
  }
2632
- function requestsPlanEndState(operations) {
2983
+ function requestsPlanEndTerminal(operations) {
2633
2984
  return operations.some((operation) => operation.type === "plan.status" && operation.status.startsWith("end."));
2634
2985
  }
2635
2986
  function planCompletionOperations(updates) {