@veewo/claw 0.2.3 → 0.2.5

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, appendKnowledgeTaskConclusions, buildKnowledgeAtomicDispatch, 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, resolveKnowledgeWriterForHost, 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
- import { extractLatestFinalAssistantMessage, extractTaskDoneConclusions, } from "./codex-transcript.js";
13
+ import { extractLatestFinalAssistantMessage, extractTaskDoneConclusions, findCodexTranscriptPath, } 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
  ],
@@ -275,7 +283,7 @@ const COMMAND_HELP = {
275
283
  description: "Create a flat subplan file under the task directory. Uses explicit `--template` first, otherwise the project's configured `defaultPlanTemplate`, and finally falls back to the built-in `default`. The current session binding switches to the subplan and returns to its parent when the subplan ends.",
276
284
  summary: "Create a subplan under a parent task's task item.",
277
285
  options: [
278
- { flag: "--parent <task-name>", detail: "(required) Parent task name." },
286
+ { flag: "--parent <task-name>", detail: "(required) Parent task directory name (`taskName`), not its plan title." },
279
287
  { flag: "--task-id <number>", detail: "(required) Parent task item id to split into a subplan." },
280
288
  { flag: "--template <name>", detail: "Optional plan template name. Overrides the project's configured default template." },
281
289
  { flag: "--template-file <path>", detail: "Exact plan template file. Mutually exclusive with --template." },
@@ -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: {
@@ -327,10 +336,17 @@ const COMMAND_HELP = {
327
336
  ],
328
337
  },
329
338
  claim: {
330
- usage: ["{script} knowledge claim --job <path>"],
331
- description: "Claim a queued or retryable job after its executor session has been bound.",
332
- summary: "Claim a session-bound finalization job.",
333
- options: [{ flag: "--job <path>", detail: "(required) Finalization job JSON path." }],
339
+ usage: [
340
+ "{script} knowledge claim --job <path>",
341
+ "{script} knowledge claim --project-root <path> --finalize-id <id>",
342
+ ],
343
+ description: "Claim a queued or retryable job. Codex subagent claims capture the existing task conclusions from the parent transcript before ownership is granted.",
344
+ summary: "Claim a ready finalization job and prepare its report.",
345
+ options: [
346
+ { flag: "--job <path>", detail: "Exact finalization job JSON path." },
347
+ { flag: "--project-root <path>", detail: "Project that owns a ready finalization job." },
348
+ { flag: "--finalize-id <id>", detail: "Finalization id used with --project-root." },
349
+ ],
334
350
  },
335
351
  done: {
336
352
  usage: [
@@ -471,7 +487,7 @@ async function main() {
471
487
  printJson(buildPublicContextOutput(await runContextCommand(args, process.cwd(), resolveOwnerSessionKey(), effectiveHost)));
472
488
  return;
473
489
  case "session":
474
- runSession(args);
490
+ await runSession(args, effectiveHost);
475
491
  return;
476
492
  case "check":
477
493
  const checkResult = ensureProjectProtocol(process.cwd());
@@ -489,7 +505,7 @@ async function main() {
489
505
  await runPlan(args, effectiveHost);
490
506
  return;
491
507
  case "codex":
492
- runCodex(args);
508
+ await runCodex(args);
493
509
  return;
494
510
  case "template":
495
511
  await runTemplate(args);
@@ -567,16 +583,55 @@ async function main() {
567
583
  handleError(error);
568
584
  }
569
585
  }
570
- function runCodex(args) {
586
+ async function runCodex(args) {
571
587
  const subcommand = args.shift();
572
- if (subcommand !== "driver") {
573
- throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown codex subcommand "${subcommand ?? ""}".`);
588
+ if (subcommand === "driver") {
589
+ assertNoRemainingArgs(args, "codex driver");
590
+ printJson(buildCodexDriverEnvelope(CLI_VERSION));
591
+ return;
592
+ }
593
+ if (subcommand === "invoke") {
594
+ const encoded = args.shift();
595
+ assertNoRemainingArgs(args, "codex invoke");
596
+ if (!encoded || !/^(?:[a-f0-9]{4})+$/i.test(encoded)) {
597
+ throw new ClawError("PROJECT_CONFIG_INVALID", "codex invoke requires one UTF-16 hex argv payload.");
598
+ }
599
+ let json = "";
600
+ for (let index = 0; index < encoded.length; index += 4) {
601
+ json += String.fromCharCode(Number.parseInt(encoded.slice(index, index + 4), 16));
602
+ }
603
+ const invocation = JSON.parse(json);
604
+ if (!Array.isArray(invocation)
605
+ || invocation.length === 0
606
+ || invocation.some((value) => typeof value !== "string")
607
+ || !["plan", "task", "subplan"].includes(invocation[0])
608
+ || invocation.some((value) => value === "--host")) {
609
+ throw new ClawError("PROJECT_CONFIG_INVALID", "codex invoke accepts only a structured plan, task, or subplan argv without --host.");
610
+ }
611
+ const originalArgv = process.argv;
612
+ try {
613
+ process.argv = [originalArgv[0], originalArgv[1], ...invocation, "--host", "codex"];
614
+ await main();
615
+ }
616
+ finally {
617
+ process.argv = originalArgv;
618
+ }
619
+ return;
574
620
  }
575
- assertNoRemainingArgs(args, "codex driver");
576
- printJson(buildCodexDriverEnvelope(CLI_VERSION));
621
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown codex subcommand "${subcommand ?? ""}".`);
577
622
  }
578
- function runSession(args) {
623
+ async function runSession(args, effectiveHost) {
579
624
  const subcommand = args.shift();
625
+ if (subcommand === "open") {
626
+ const workdir = args.shift();
627
+ const agentSessionId = args.shift();
628
+ assertNoRemainingArgs(args, "session open");
629
+ if (!workdir || !agentSessionId) {
630
+ throw new ClawError("PROJECT_CONFIG_INVALID", "session open requires <dir> <session-id> in that order.");
631
+ }
632
+ await runPersistentSession(workdir, agentSessionId, effectiveHost);
633
+ return;
634
+ }
580
635
  if (subcommand !== "clean") {
581
636
  throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown session subcommand "${subcommand ?? ""}".`);
582
637
  }
@@ -597,19 +652,419 @@ function runSession(args) {
597
652
  removed: deleteSessionWorkflow(ownerSessionKey),
598
653
  });
599
654
  }
655
+ async function runPersistentSession(workdir, agentSessionId, effectiveHost) {
656
+ const opened = await new ClawClient({
657
+ clientKind: "terminal",
658
+ ...(effectiveHost ? { host: effectiveHost } : {}),
659
+ }).open(agentSessionId, path.resolve(workdir));
660
+ writeSessionOutput({
661
+ ok: true,
662
+ command: "session.open",
663
+ ...opened.openResult,
664
+ });
665
+ const readline = createInterface({
666
+ input: process.stdin,
667
+ output: process.stdout,
668
+ terminal: Boolean(process.stdin.isTTY && process.stdout.isTTY),
669
+ prompt: "claw> ",
670
+ });
671
+ if (process.stdin.isTTY && process.stdout.isTTY)
672
+ readline.prompt();
673
+ try {
674
+ for await (const line of readline) {
675
+ const trimmed = line.trim();
676
+ if (!trimmed) {
677
+ if (process.stdin.isTTY && process.stdout.isTTY)
678
+ readline.prompt();
679
+ continue;
680
+ }
681
+ try {
682
+ const command = parsePersistentSessionCommand(trimmed);
683
+ if (command.kind === "close") {
684
+ await opened.close();
685
+ writeSessionOutput({ ok: true, command: "session.close" });
686
+ return;
687
+ }
688
+ if (command.kind === "status") {
689
+ writeSessionOutput({ ok: true, command: "session.status", ...await opened.status() });
690
+ }
691
+ else {
692
+ const envelope = await opened.commandEnvelope(command.request);
693
+ writeSessionOutput({
694
+ ok: true,
695
+ command: command.request.operation,
696
+ ...envelope,
697
+ });
698
+ }
699
+ }
700
+ catch (error) {
701
+ writeSessionOutput({ ok: false, error: serializeSessionError(error) });
702
+ if (error instanceof ClawSessionError && error.code === "SESSION_CONNECTION_LOST")
703
+ return;
704
+ }
705
+ if (process.stdin.isTTY && process.stdout.isTTY)
706
+ readline.prompt();
707
+ }
708
+ }
709
+ finally {
710
+ readline.close();
711
+ try {
712
+ await opened.close();
713
+ }
714
+ catch {
715
+ // EOF is a soft close; retained state survives a lost daemon connection.
716
+ }
717
+ }
718
+ }
719
+ function parsePersistentSessionCommand(line) {
720
+ if (line.startsWith("{")) {
721
+ const request = JSON.parse(line);
722
+ if (typeof request.operation !== "string") {
723
+ throw new ClawError("PROJECT_CONFIG_INVALID", "JSON session commands require operation.");
724
+ }
725
+ return {
726
+ kind: "command",
727
+ request: { operation: request.operation, input: request.input ?? {} },
728
+ };
729
+ }
730
+ const tokens = tokenizeSessionLine(line);
731
+ const group = tokens.shift();
732
+ const action = tokens.shift();
733
+ if ((group === "session" && action === "close") || group === "exit" || group === "quit") {
734
+ return { kind: "close" };
735
+ }
736
+ if ((group === "session" && action === "status") || group === "status") {
737
+ return { kind: "status" };
738
+ }
739
+ if (group === "plan" && action === "show") {
740
+ const simple = consumeSessionBoolean(tokens, "--simple");
741
+ assertSessionTokensConsumed(tokens, line);
742
+ return {
743
+ kind: "command",
744
+ request: { operation: "plan.show", input: { simple } },
745
+ };
746
+ }
747
+ if (group === "plan" && action === "leave") {
748
+ assertSessionTokensConsumed(tokens, line);
749
+ return { kind: "command", request: { operation: "plan.leave", input: {} } };
750
+ }
751
+ if (group === "plan" && action === "resume") {
752
+ const planId = tokens.shift();
753
+ assertSessionTokensConsumed(tokens, line);
754
+ return {
755
+ kind: "command",
756
+ request: { operation: "plan.resume", input: planId ? { planId } : {} },
757
+ };
758
+ }
759
+ if (group === "plan" && action === "create") {
760
+ const title = consumeSessionFlag(tokens, "--title") ?? tokens.shift();
761
+ if (!title)
762
+ throw new ClawError("PROJECT_CONFIG_INVALID", "plan create requires a title.");
763
+ const goalText = consumeSessionFlag(tokens, "--goal");
764
+ assertSessionTokensConsumed(tokens, line);
765
+ return {
766
+ kind: "command",
767
+ request: {
768
+ operation: "plan.create",
769
+ input: { title, ...(goalText ? { goalText } : {}) },
770
+ },
771
+ };
772
+ }
773
+ if (group === "plan" && action === "wait") {
774
+ assertSessionTokensConsumed(tokens, line);
775
+ return { kind: "command", request: { operation: "plan.wait", input: {} } };
776
+ }
777
+ if (group === "plan" && action === "edit") {
778
+ const operations = parseSessionPlanEditOperations(tokens, line);
779
+ return {
780
+ kind: "command",
781
+ request: { operation: "plan.edit", input: { operations } },
782
+ };
783
+ }
784
+ if (group === "plan" && action === "done") {
785
+ const retrospectiveSummary = consumeSessionFlag(tokens, "--retrospective");
786
+ if (!retrospectiveSummary) {
787
+ throw new ClawError("RETROSPECTIVE_REQUIRED", "plan done requires --retrospective.");
788
+ }
789
+ const keyDecisions = consumeAllSessionFlags(tokens, "--key-decision");
790
+ const whatWorked = consumeAllSessionFlags(tokens, "--what-worked");
791
+ const issues = consumeAllSessionFlags(tokens, "--issue");
792
+ const followUps = consumeAllSessionFlags(tokens, "--follow-up");
793
+ assertSessionTokensConsumed(tokens, line);
794
+ return {
795
+ kind: "command",
796
+ request: {
797
+ operation: "plan.done",
798
+ input: {
799
+ retrospectiveSummary,
800
+ ...(keyDecisions.length ? { keyDecisions } : {}),
801
+ ...(whatWorked.length ? { whatWorked } : {}),
802
+ ...(issues.length ? { issues } : {}),
803
+ ...(followUps.length ? { followUps } : {}),
804
+ },
805
+ },
806
+ };
807
+ }
808
+ if (group === "task" && action === "add") {
809
+ const tasks = [];
810
+ while (tokens.length > 0) {
811
+ if (tokens.shift() !== "--title") {
812
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task add expects --title to start each task.");
813
+ }
814
+ const title = tokens.shift();
815
+ if (!title)
816
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task add requires a title.");
817
+ let detail;
818
+ if (tokens[0] === "--detail") {
819
+ tokens.shift();
820
+ detail = tokens.shift();
821
+ if (!detail)
822
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task add --detail requires a value.");
823
+ }
824
+ tasks.push({ title, ...(detail ? { detail } : {}) });
825
+ }
826
+ if (!tasks.length)
827
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task add requires at least one task.");
828
+ return { kind: "command", request: { operation: "task.add", input: { tasks } } };
829
+ }
830
+ if (group === "task" && action === "edit") {
831
+ const id = Number(consumeSessionFlag(tokens, "--id"));
832
+ if (!Number.isInteger(id))
833
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task edit requires integer --id.");
834
+ const taskTitle = consumeSessionFlag(tokens, "--title");
835
+ const taskDetail = consumeSessionFlag(tokens, "--detail");
836
+ const taskStatus = consumeSessionFlag(tokens, "--status");
837
+ const taskChoiceId = consumeSessionFlag(tokens, "--choice");
838
+ if (!taskTitle && !taskDetail && !taskStatus && !taskChoiceId) {
839
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task edit requires a field to change.");
840
+ }
841
+ assertSessionTokensConsumed(tokens, line);
842
+ return {
843
+ kind: "command",
844
+ request: {
845
+ operation: "task.edit",
846
+ input: {
847
+ taskId: id,
848
+ ...(taskTitle ? { taskTitle } : {}),
849
+ ...(taskDetail ? { taskDetail } : {}),
850
+ ...(taskStatus ? { taskStatus } : {}),
851
+ ...(taskChoiceId ? { taskChoiceId } : {}),
852
+ },
853
+ },
854
+ };
855
+ }
856
+ if (group === "task" && action === "done") {
857
+ const tasks = [];
858
+ while (tokens.length > 0) {
859
+ if (tokens.shift() !== "--id") {
860
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task done expects --id to start each task.");
861
+ }
862
+ const id = Number(tokens.shift());
863
+ if (!Number.isInteger(id))
864
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task done requires integer ids.");
865
+ let choiceId;
866
+ if (tokens[0] === "--choice") {
867
+ tokens.shift();
868
+ choiceId = tokens.shift();
869
+ if (!choiceId)
870
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task done --choice requires a value.");
871
+ }
872
+ tasks.push({ id, ...(choiceId ? { choiceId } : {}) });
873
+ }
874
+ if (!tasks.length)
875
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task done requires at least one task.");
876
+ return { kind: "command", request: { operation: "task.done", input: { tasks } } };
877
+ }
878
+ if (group === "search") {
879
+ const query = consumeSessionFlag(tokens, "--query") ?? action;
880
+ if (!query)
881
+ throw new ClawError("MEMORY_QUERY_REQUIRED", "search requires a query.");
882
+ const dir = consumeSessionFlag(tokens, "--dir");
883
+ const limitRaw = consumeSessionFlag(tokens, "--limit");
884
+ assertSessionTokensConsumed(tokens, line);
885
+ return {
886
+ kind: "command",
887
+ request: {
888
+ operation: "search",
889
+ input: {
890
+ query,
891
+ ...(dir ? { dir } : {}),
892
+ ...(limitRaw ? { limit: Number(limitRaw) } : {}),
893
+ },
894
+ },
895
+ };
896
+ }
897
+ throw new ClawError("SESSION_OPERATION_UNSUPPORTED", `Unsupported persistent session command: ${line}`);
898
+ }
899
+ function tokenizeSessionLine(line) {
900
+ const tokens = [];
901
+ const pattern = /"((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'|(\S+)/g;
902
+ for (const match of line.matchAll(pattern)) {
903
+ tokens.push((match[1] ?? match[2] ?? match[3] ?? "").replace(/\\(["'\\])/g, "$1"));
904
+ }
905
+ return tokens;
906
+ }
907
+ function consumeSessionFlag(tokens, flag) {
908
+ const index = tokens.indexOf(flag);
909
+ if (index < 0)
910
+ return undefined;
911
+ const value = tokens[index + 1];
912
+ if (!value)
913
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Missing value for ${flag}.`);
914
+ tokens.splice(index, 2);
915
+ return value;
916
+ }
917
+ function consumeAllSessionFlags(tokens, flag) {
918
+ const values = [];
919
+ while (tokens.includes(flag)) {
920
+ values.push(consumeSessionFlag(tokens, flag));
921
+ }
922
+ return values;
923
+ }
924
+ function parseSessionPlanEditOperations(tokens, line) {
925
+ const operations = [];
926
+ const updateFlags = {
927
+ "--goal": "goalText",
928
+ "--requirements": "requirementsSummary",
929
+ "--summary": "planSummary",
930
+ "--rule": "rules",
931
+ "--key-decision": "keyDecisions",
932
+ };
933
+ while (tokens.length > 0) {
934
+ const flag = tokens.shift();
935
+ const value = tokens.shift();
936
+ if (!value)
937
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Missing value for ${flag}.`);
938
+ if (flag === "--status") {
939
+ operations.push({ type: "plan.status", status: value });
940
+ continue;
941
+ }
942
+ const field = updateFlags[flag];
943
+ if (!field) {
944
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown plan edit argument in "${line}": ${flag}`);
945
+ }
946
+ const listField = field === "rules" || field === "keyDecisions";
947
+ operations.push({
948
+ type: "plan.update",
949
+ updates: { [field]: listField ? [value] : value },
950
+ });
951
+ }
952
+ if (!operations.length)
953
+ throw new ClawError("PROJECT_CONFIG_INVALID", "plan edit requires a field to change.");
954
+ return operations;
955
+ }
956
+ function consumeSessionBoolean(tokens, flag) {
957
+ const index = tokens.indexOf(flag);
958
+ if (index < 0)
959
+ return false;
960
+ tokens.splice(index, 1);
961
+ return true;
962
+ }
963
+ function assertSessionTokensConsumed(tokens, line) {
964
+ if (tokens.length > 0) {
965
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown session command arguments in "${line}": ${tokens.join(" ")}`);
966
+ }
967
+ }
968
+ function writeSessionOutput(value) {
969
+ process.stdout.write(`${JSON.stringify(value)}\n`);
970
+ }
971
+ function serializeSessionError(error) {
972
+ if (error instanceof ClawSessionError) {
973
+ return {
974
+ code: error.code,
975
+ message: error.message,
976
+ retryable: error.retryable,
977
+ outcome: error.outcome,
978
+ ...(error.recoveryCommand ? { recoveryCommand: error.recoveryCommand } : {}),
979
+ ...(error.details ? { details: error.details } : {}),
980
+ };
981
+ }
982
+ if (error instanceof ClawError) {
983
+ return { code: error.code, message: error.message, ...(error.details ? { details: error.details } : {}) };
984
+ }
985
+ return { code: "SESSION_COMMAND_FAILED", message: error instanceof Error ? error.message : String(error) };
986
+ }
987
+ function readCindyKnowledgeClaimCaptureInput() {
988
+ const raw = fs.readFileSync(0, "utf8").trim();
989
+ if (!raw) {
990
+ throw new ClawError("PROJECT_CONFIG_INVALID", "Cindy knowledge claim report input is empty.");
991
+ }
992
+ let parsed;
993
+ try {
994
+ parsed = JSON.parse(raw);
995
+ }
996
+ catch {
997
+ throw new ClawError("PROJECT_CONFIG_INVALID", "Cindy knowledge claim report input is invalid JSON.");
998
+ }
999
+ const sessionId = typeof parsed.session_id === "string" ? parsed.session_id.trim() : "";
1000
+ const turnId = typeof parsed.turn_id === "string" ? parsed.turn_id.trim() : "";
1001
+ const taskConclusions = Array.isArray(parsed.task_conclusions)
1002
+ ? parsed.task_conclusions.flatMap((entry) => {
1003
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
1004
+ return [];
1005
+ const item = entry;
1006
+ const itemTurnId = typeof item.turnId === "string" ? item.turnId.trim() : "";
1007
+ const message = typeof item.message === "string" ? item.message.trim() : "";
1008
+ return itemTurnId && message ? [{ turnId: itemTurnId, message }] : [];
1009
+ })
1010
+ : [];
1011
+ if (!sessionId || !turnId) {
1012
+ throw new ClawError("PROJECT_CONFIG_INVALID", "Cindy knowledge claim report input requires session_id and turn_id.");
1013
+ }
1014
+ return { sessionId, turnId, taskConclusions };
1015
+ }
600
1016
  function runKnowledge(args) {
601
1017
  const subcommand = args.shift();
602
1018
  switch (subcommand) {
1019
+ case "list": {
1020
+ const projectRoot = path.resolve(readRequiredFlag(args, "--project-root"));
1021
+ const sessionKey = readOptionalFlag(args, "--session-key");
1022
+ const host = readOptionalFlag(args, "--job-host");
1023
+ assertNoRemainingArgs(args, "knowledge list");
1024
+ const sessionProject = sessionKey ? resolveSessionWorkflowContext(sessionKey) : null;
1025
+ const project = resolveProjectContext(projectRoot);
1026
+ const candidates = sessionProject && sessionProject.clawDir !== project.clawDir
1027
+ ? [sessionProject, project]
1028
+ : [sessionProject ?? project];
1029
+ const jobs = candidates.flatMap((candidate) => listKnowledgeFinalizationJobs(candidate))
1030
+ .map((jobPath) => ({ jobPath, job: readKnowledgeFinalizationJob(jobPath) }))
1031
+ .filter(({ job }) => (job.status === "running"
1032
+ || ((job.status === "queued" || job.status === "failed") && job.attempts < 3)) && (!host || job.host === host))
1033
+ .map(({ jobPath, job }) => ({
1034
+ jobPath,
1035
+ finalizeId: job.finalizeId,
1036
+ status: job.status,
1037
+ attempts: job.attempts,
1038
+ }));
1039
+ printJson({ ok: true, command: "knowledge.list", jobs });
1040
+ return;
1041
+ }
603
1042
  case "wait": {
604
1043
  const projectRoot = path.resolve(readRequiredFlag(args, "--project-root"));
605
1044
  const finalizeId = readRequiredFlag(args, "--finalize-id");
1045
+ const sessionKey = readOptionalFlag(args, "--session-key");
606
1046
  const timeoutMs = readOptionalNumber(args, "--timeout-ms") ?? 300_000;
607
1047
  assertNoRemainingArgs(args, "knowledge wait");
608
- const { jobPath, job } = waitForKnowledgeFinalizationJobReady({
609
- project: resolveProjectContext(projectRoot),
610
- finalizeId,
611
- timeoutMs,
612
- });
1048
+ const sessionProject = sessionKey ? resolveSessionWorkflowContext(sessionKey) : null;
1049
+ const project = resolveProjectContext(projectRoot);
1050
+ const candidates = sessionProject && sessionProject.clawDir !== project.clawDir
1051
+ ? [sessionProject, project]
1052
+ : [sessionProject ?? project];
1053
+ let located = candidates
1054
+ .map((candidate) => findKnowledgeFinalizationJobPath(candidate, finalizeId))
1055
+ .find((candidate) => Boolean(candidate));
1056
+ if (!located && timeoutMs > 0) {
1057
+ located = waitForKnowledgeFinalizationJobReady({
1058
+ project: candidates[0],
1059
+ finalizeId,
1060
+ timeoutMs,
1061
+ }).jobPath;
1062
+ }
1063
+ if (!located) {
1064
+ throw new Error(`Knowledge finalization ${finalizeId} is unavailable.`);
1065
+ }
1066
+ const jobPath = located;
1067
+ const job = readKnowledgeFinalizationJob(jobPath);
613
1068
  printJson({
614
1069
  ok: true,
615
1070
  command: "knowledge.wait",
@@ -620,9 +1075,68 @@ function runKnowledge(args) {
620
1075
  return;
621
1076
  }
622
1077
  case "claim": {
623
- const jobPath = readRequiredFlag(args, "--job");
1078
+ const explicitJobPath = readOptionalFlag(args, "--job");
1079
+ const projectRoot = readOptionalFlag(args, "--project-root");
1080
+ const finalizeId = readOptionalFlag(args, "--finalize-id");
1081
+ const captureCindyReport = readBooleanFlag(args, "--cindy-report-stdin");
1082
+ const cindyCapture = captureCindyReport ? readCindyKnowledgeClaimCaptureInput() : undefined;
1083
+ if (explicitJobPath && (projectRoot || finalizeId)) {
1084
+ throw new ClawError("PROJECT_CONFIG_INVALID", "knowledge claim accepts either --job or --project-root with --finalize-id.");
1085
+ }
1086
+ if (!explicitJobPath && (!projectRoot || !finalizeId)) {
1087
+ throw new ClawError("PROJECT_CONFIG_INVALID", "knowledge claim requires --job or both --project-root and --finalize-id.");
1088
+ }
624
1089
  assertNoRemainingArgs(args, "knowledge claim");
625
- const job = claimKnowledgeFinalizationJob(jobPath);
1090
+ const jobPath = explicitJobPath ?? findKnowledgeFinalizationJobPath(resolveProjectContext(path.resolve(projectRoot)), finalizeId);
1091
+ if (!jobPath) {
1092
+ throw new Error(`Knowledge finalization ${finalizeId} is unavailable.`);
1093
+ }
1094
+ const job = claimKnowledgeFinalizationJob(jobPath, {
1095
+ prepare: (queued) => {
1096
+ if (queued.writer?.executionPolicy !== "subagent"
1097
+ || queued.reportCapture?.mode !== "claim"
1098
+ || queued.reportCapture.status === "captured") {
1099
+ return;
1100
+ }
1101
+ if (queued.host === "cindy") {
1102
+ if (!cindyCapture) {
1103
+ throw new Error(`Cindy report capture is unavailable for knowledge session ${queued.sessionId}.`);
1104
+ }
1105
+ if (cindyCapture.sessionId !== queued.sessionId) {
1106
+ throw new Error("Cindy report capture does not match the originating knowledge session.");
1107
+ }
1108
+ const capturedAt = new Date().toISOString();
1109
+ appendKnowledgeTaskConclusions(queued.reportPath, queued.sessionId, cindyCapture.taskConclusions, capturedAt);
1110
+ return {
1111
+ reportCapture: {
1112
+ ...queued.reportCapture,
1113
+ status: "captured",
1114
+ capturedAt,
1115
+ messageCount: cindyCapture.taskConclusions.length,
1116
+ },
1117
+ };
1118
+ }
1119
+ if (queued.host !== "codex") {
1120
+ throw new Error(`Claim-time report capture is unavailable for host ${queued.host ?? "unknown"}.`);
1121
+ }
1122
+ const transcriptPath = findCodexTranscriptPath(queued.sessionId);
1123
+ if (!transcriptPath) {
1124
+ throw new Error(`Codex transcript is unavailable for knowledge session ${queued.sessionId}.`);
1125
+ }
1126
+ const conclusions = extractTaskDoneConclusions(transcriptPath, undefined, queued.reportCapture.startedAt);
1127
+ const capturedAt = new Date().toISOString();
1128
+ appendKnowledgeTaskConclusions(queued.reportPath, queued.sessionId, conclusions, capturedAt);
1129
+ return {
1130
+ reportCapture: {
1131
+ ...queued.reportCapture,
1132
+ status: "captured",
1133
+ capturedAt,
1134
+ transcriptPath,
1135
+ messageCount: conclusions.length,
1136
+ },
1137
+ };
1138
+ },
1139
+ });
626
1140
  const assignments = job ? buildKnowledgeWriterAssignments(job) : [];
627
1141
  const templatePath = job
628
1142
  ? path.join(path.dirname(jobPath), `${job.finalizeId}.assignments.json`)
@@ -640,6 +1154,7 @@ function runKnowledge(args) {
640
1154
  claimed: Boolean(job),
641
1155
  ...(job ? {
642
1156
  finalizeId: job.finalizeId,
1157
+ jobPath,
643
1158
  claimToken: job.claimToken,
644
1159
  projectRoot: job.projectRoot,
645
1160
  writer: job.writer ?? null,
@@ -715,21 +1230,22 @@ async function runPlan(args, effectiveHost) {
715
1230
  throw new ClawError("PROJECT_CONFIG_INVALID", "plan edit requires at least one plan field or --status.");
716
1231
  }
717
1232
  const ownerSessionKey = resolveOwnerSessionKey() ?? undefined;
718
- const entersEndState = requestsPlanEndState(operations);
719
- const current = entersEndState
1233
+ const entersEndTerminal = requestsPlanEndTerminal(operations);
1234
+ const current = entersEndTerminal
720
1235
  ? showPlan({ cwd: process.cwd(), ...target, ownerSessionKey })
721
1236
  : undefined;
722
- const project = entersEndState ? tryResolveHookProject(process.cwd()) : null;
723
- const effectiveWriter = current && project
1237
+ const project = entersEndTerminal ? tryResolveHookProject(process.cwd()) : null;
1238
+ const effectiveWriter = resolveKnowledgeWriterForHost(current && project
724
1239
  ? resolvePlanEffectiveConfig(project.projectConfig, current.plan)?.knowledgeWriter
725
- : undefined;
1240
+ : undefined, effectiveHost);
726
1241
  if (current
727
1242
  && !current.plan.parentPlan
728
1243
  && effectiveWriter?.executionPolicy === "subagent"
729
- && effectiveHost !== "codex") {
730
- throw new ClawError("PROJECT_CONFIG_INVALID", 'knowledgeWriter.executionPolicy "subagent" is supported only by the Codex host.', { host: effectiveHost ?? null });
1244
+ && effectiveHost !== "codex"
1245
+ && effectiveHost !== "cindy") {
1246
+ throw new ClawError("PROJECT_CONFIG_INVALID", 'knowledgeWriter.executionPolicy "subagent" is supported only by the Codex or Cindy host.', { host: effectiveHost ?? null });
731
1247
  }
732
- const queuePlanEndFinalization = entersEndState
1248
+ const queuePlanEndFinalization = entersEndTerminal
733
1249
  ? preparePlanEndFinalization(process.cwd(), ownerSessionKey)
734
1250
  : undefined;
735
1251
  const result = await editPlan({
@@ -743,11 +1259,12 @@ async function runPlan(args, effectiveHost) {
743
1259
  const completionRefresh = queuePlanEndFinalization?.(result.taskName);
744
1260
  const knowledgeDispatch = (current
745
1261
  && project
746
- && effectiveHost === "codex"
1262
+ && (effectiveHost === "codex" || effectiveHost === "cindy")
747
1263
  && !current.plan.parentPlan
748
1264
  && effectiveWriter?.executionPolicy === "subagent"
749
1265
  && result.knowledgeFinalizeId)
750
1266
  ? buildKnowledgeDispatch({
1267
+ host: effectiveHost,
751
1268
  finalizeId: result.knowledgeFinalizeId,
752
1269
  writer: effectiveWriter,
753
1270
  })
@@ -826,13 +1343,14 @@ async function runPlan(args, effectiveHost) {
826
1343
  ownerSessionKey,
827
1344
  });
828
1345
  const project = tryResolveHookProject(process.cwd());
829
- const effectiveWriter = project
1346
+ const effectiveWriter = resolveKnowledgeWriterForHost(project
830
1347
  ? resolvePlanEffectiveConfig(project.projectConfig, current.plan)?.knowledgeWriter
831
- : undefined;
1348
+ : undefined, effectiveHost);
832
1349
  if (!current.plan.parentPlan
833
1350
  && effectiveWriter?.executionPolicy === "subagent"
834
- && effectiveHost !== "codex") {
835
- throw new ClawError("PROJECT_CONFIG_INVALID", 'knowledgeWriter.executionPolicy "subagent" is supported only by the Codex host.', { host: effectiveHost ?? null });
1351
+ && effectiveHost !== "codex"
1352
+ && effectiveHost !== "cindy") {
1353
+ throw new ClawError("PROJECT_CONFIG_INVALID", 'knowledgeWriter.executionPolicy "subagent" is supported only by the Codex or Cindy host.', { host: effectiveHost ?? null });
836
1354
  }
837
1355
  const queuePlanEndFinalization = preparePlanEndFinalization(process.cwd(), ownerSessionKey);
838
1356
  const result = await editPlan({
@@ -844,11 +1362,12 @@ async function runPlan(args, effectiveHost) {
844
1362
  ownerSessionKey,
845
1363
  });
846
1364
  const completionRefresh = queuePlanEndFinalization?.(result.taskName);
847
- const knowledgeDispatch = (effectiveHost === "codex"
1365
+ const knowledgeDispatch = ((effectiveHost === "codex" || effectiveHost === "cindy")
848
1366
  && !current.plan.parentPlan
849
1367
  && effectiveWriter?.executionPolicy === "subagent"
850
1368
  && result.knowledgeFinalizeId)
851
1369
  ? buildKnowledgeDispatch({
1370
+ host: effectiveHost,
852
1371
  finalizeId: result.knowledgeFinalizeId,
853
1372
  writer: effectiveWriter,
854
1373
  })
@@ -857,6 +1376,7 @@ async function runPlan(args, effectiveHost) {
857
1376
  return;
858
1377
  }
859
1378
  case "show": {
1379
+ const simple = readBooleanFlag(args, "--simple");
860
1380
  const target = readPlanMutationTarget(args);
861
1381
  assertNoRemainingArgs(args, "plan show");
862
1382
  const result = showPlan({
@@ -864,6 +1384,10 @@ async function runPlan(args, effectiveHost) {
864
1384
  ...target,
865
1385
  ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
866
1386
  });
1387
+ if (simple) {
1388
+ printJson(result.simplePlanView);
1389
+ return;
1390
+ }
867
1391
  printJson({
868
1392
  ok: true,
869
1393
  command: "plan.show",
@@ -970,7 +1494,7 @@ async function runTask(args, effectiveHost) {
970
1494
  cwd: process.cwd(),
971
1495
  ...target,
972
1496
  operations,
973
- commandSource: "plan.edit",
1497
+ commandSource: "task.add",
974
1498
  host: effectiveHost,
975
1499
  ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
976
1500
  });
@@ -986,7 +1510,7 @@ async function runTask(args, effectiveHost) {
986
1510
  cwd: process.cwd(),
987
1511
  ...target,
988
1512
  operations,
989
- commandSource: "plan.edit",
1513
+ commandSource: "task.edit",
990
1514
  host: effectiveHost,
991
1515
  ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
992
1516
  });
@@ -1002,7 +1526,7 @@ async function runTask(args, effectiveHost) {
1002
1526
  cwd: process.cwd(),
1003
1527
  ...target,
1004
1528
  operations,
1005
- commandSource: "plan.edit",
1529
+ commandSource: "task.remove",
1006
1530
  host: effectiveHost,
1007
1531
  ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
1008
1532
  });
@@ -1018,6 +1542,7 @@ async function runTask(args, effectiveHost) {
1018
1542
  cwd: process.cwd(),
1019
1543
  ...target,
1020
1544
  operations,
1545
+ commandSource: "task.done",
1021
1546
  host: effectiveHost,
1022
1547
  ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
1023
1548
  });
@@ -1052,11 +1577,12 @@ async function runSearch(args) {
1052
1577
  if (args.includes("--scope") || args.includes("--task")) {
1053
1578
  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
1579
  }
1580
+ const dir = readOptionalFlag(args, "--dir");
1055
1581
  printJson({
1056
1582
  ok: true,
1057
1583
  command: "search",
1058
1584
  ...await searchMemoryAsync({
1059
- cwd: process.cwd(),
1585
+ cwd: dir ? path.resolve(process.cwd(), dir) : process.cwd(),
1060
1586
  limit: readOptionalNumber(args, "--limit"),
1061
1587
  query: readRequiredSearchQuery(args),
1062
1588
  scope: "project",
@@ -1269,7 +1795,7 @@ function buildPublicContextOutput(context) {
1269
1795
  if (Object.keys(compactRecovery).length > 0) {
1270
1796
  output.startupRecovery = compactRecovery;
1271
1797
  }
1272
- const searchGuidance = buildContextSearchGuidance(context);
1798
+ const searchGuidance = buildContextSearchGuidance(context, "rg");
1273
1799
  if (searchGuidance) {
1274
1800
  output.searchGuidance = searchGuidance;
1275
1801
  }
@@ -1281,20 +1807,26 @@ function shouldExposeVersionSync(versionSync) {
1281
1807
  || versionSync.updateAvailable === true
1282
1808
  || versionSync.projectVersion !== versionSync.cliVersion;
1283
1809
  }
1284
- function buildContextSearchGuidance(context) {
1810
+ function buildContextSearchGuidance(context, style = "default") {
1285
1811
  const project = asJsonRecord(context.project);
1286
1812
  const projectConfig = asJsonRecord(project?.projectConfig);
1287
1813
  const memory = asJsonRecord(projectConfig?.memory);
1288
1814
  const embeddingEnabled = memory?.enabled === true && asJsonRecord(memory.embedding) !== null;
1289
1815
  const gitnexusEnabled = projectConfig?.gitnexus === true;
1290
1816
  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.";
1817
+ return style === "rg"
1818
+ ? "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."
1819
+ : "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
1820
  }
1293
1821
  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.";
1822
+ return style === "rg"
1823
+ ? "Before using `rg`, use `claw search --query` to narrow the document search scope, then use `rg` to locate exact files or symbols."
1824
+ : "When useful, use `claw search` to narrow the document search scope, then use the default search to locate exact files or symbols.";
1295
1825
  }
1296
1826
  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.";
1827
+ return style === "rg"
1828
+ ? "Before using `rg`, use GitNexus to narrow the code search scope, then use `rg` to locate exact files or symbols."
1829
+ : "When useful, use GitNexus to narrow the code search scope, then use the default search to locate exact files or symbols.";
1298
1830
  }
1299
1831
  return null;
1300
1832
  }
@@ -1505,12 +2037,12 @@ async function runInternalKnowledgeCapture(args, effectiveHost) {
1505
2037
  const turnId = readHookString(payload, "turn_id");
1506
2038
  const message = readHookString(payload, "message");
1507
2039
  const taskConclusions = readHookTaskConclusions(payload, turnId);
1508
- if (!hookCwd || !sessionId || !turnId || !message || !containsClawDir(hookCwd)) {
2040
+ if (!hookCwd || !sessionId || !turnId || !message) {
1509
2041
  printJson({ ok: true, captured: false });
1510
2042
  return;
1511
2043
  }
1512
2044
  try {
1513
- const project = resolveProjectContext(hookCwd);
2045
+ const project = resolveWorkflowProjectContext(hookCwd, sessionId);
1514
2046
  const result = tryCaptureKnowledgeStop({
1515
2047
  project,
1516
2048
  sessionId,
@@ -1592,7 +2124,7 @@ function completeKnowledgeFinalizationJob(jobPath, result, claimToken) {
1592
2124
  if (running.claimToken !== claimToken) {
1593
2125
  throw new Error("Knowledge finalization completion does not match the active claim.");
1594
2126
  }
1595
- const project = resolveProjectContext(running.projectRoot);
2127
+ const project = resolveKnowledgeJobProject(jobPath, running);
1596
2128
  const finishedAt = new Date().toISOString();
1597
2129
  const truthEncoding = normalizeTruthMarkdownEncoding(project);
1598
2130
  recordKnowledgeFinalizationResult(project, running.reportPath, {
@@ -1626,6 +2158,17 @@ function completeKnowledgeFinalizationJob(jobPath, result, claimToken) {
1626
2158
  });
1627
2159
  printJson({ ok: true, completed: true, alreadyDone: terminal.alreadyDone, finalizeId: running.finalizeId });
1628
2160
  }
2161
+ function resolveKnowledgeJobProject(jobPath, job) {
2162
+ const project = resolveProjectContext(job.projectRoot);
2163
+ const sessionProject = resolveSessionWorkflowContext(job.sessionId);
2164
+ for (const candidate of sessionProject ? [sessionProject, project] : [project]) {
2165
+ const relative = path.relative(candidate.clawDir, path.resolve(jobPath));
2166
+ if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
2167
+ return candidate;
2168
+ }
2169
+ }
2170
+ throw new Error("Knowledge finalization job is outside its project or session workflow.");
2171
+ }
1629
2172
  function failKnowledgeFinalizationJob(jobPath, message, claimToken) {
1630
2173
  const terminal = doneKnowledgeFinalizationJob({
1631
2174
  jobPath,
@@ -1677,7 +2220,7 @@ function runInternalKnowledgeSweep(args) {
1677
2220
  printJson({ command: "internal-knowledge-sweep", ok: true, launched: 0, skipped: true });
1678
2221
  return;
1679
2222
  }
1680
- const jobs = listRetryableKnowledgeFinalizationJobs(project);
2223
+ const jobs = listRetryableKnowledgeFinalizationJobs(project, { excludeHosts: ["cindy"] });
1681
2224
  let launched = 0;
1682
2225
  for (const jobPath of jobs) {
1683
2226
  try {
@@ -1714,7 +2257,7 @@ async function runInternalBackgroundMaintenance(args) {
1714
2257
  let launched = 0;
1715
2258
  const knowledgeProject = project ?? sessionProject;
1716
2259
  if (knowledgeProject) {
1717
- const jobs = listRetryableKnowledgeFinalizationJobs(knowledgeProject);
2260
+ const jobs = listRetryableKnowledgeFinalizationJobs(knowledgeProject, { excludeHosts: ["cindy"] });
1718
2261
  discovered = jobs.length;
1719
2262
  for (const jobPath of jobs) {
1720
2263
  try {
@@ -1914,7 +2457,7 @@ async function runSessionStartHook(effectiveHost) {
1914
2457
  const context = await runContextCommand([], hookCwd, ownerSessionKey, effectiveHost);
1915
2458
  const contextProject = asJsonRecord(context.project);
1916
2459
  const retryableJobs = effectiveHost !== "cindy" && contextProject?.scope !== "session" && !context.error
1917
- ? listRetryableKnowledgeFinalizationJobs(resolveProjectContext(hookCwd))
2460
+ ? listRetryableKnowledgeFinalizationJobs(resolveProjectContext(hookCwd), { excludeHosts: ["cindy"] })
1918
2461
  : [];
1919
2462
  if (contextProject?.scope !== "session"
1920
2463
  && !context.error
@@ -2071,31 +2614,21 @@ function buildSessionStartAdditionalContext(context, sessionCwd, effectiveHost)
2071
2614
  * supplies project identity or a recovered workflow snapshot. Goal Mode is
2072
2615
  * intentionally omitted because Cindy does not expose that Host surface.
2073
2616
  */
2074
- function buildCindySessionStartContext(context, sessionCwd, versionSyncPrompt) {
2075
- const activeWorkflow = asJsonRecord(context.activeWorkflow);
2076
- if (activeWorkflow) {
2077
- return stripCindyGoalModeLines(buildRecoveredWorkflowAdditionalContext(activeWorkflow, versionSyncPrompt));
2617
+ function buildCindySessionStartContext(context, _sessionCwd, versionSyncPrompt) {
2618
+ const lines = [];
2619
+ const startupRecovery = asJsonRecord(context.startupRecovery);
2620
+ const fixedPaths = Array.isArray(startupRecovery?.fixedPaths)
2621
+ ? startupRecovery.fixedPaths.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
2622
+ : [];
2623
+ if (startupRecovery?.corrected === true) {
2624
+ lines.push(`claw-kit repaired the project configuration${fixedPaths.length > 0 ? `: ${fixedPaths.join(", ")}` : "."}`);
2078
2625
  }
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")}`;
2626
+ if (versionSyncPrompt?.placement === "prefix")
2627
+ lines.push(...versionSyncPrompt.lines);
2628
+ 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.");
2629
+ if (versionSyncPrompt?.placement === "suffix")
2630
+ lines.push(...versionSyncPrompt.lines);
2631
+ return lines.length > 0 ? lines.join("\n") : null;
2099
2632
  }
2100
2633
  function stripCindyGoalModeLines(prompt) {
2101
2634
  return prompt
@@ -2373,9 +2906,13 @@ function stripBom(content) {
2373
2906
  return content.charCodeAt(0) === 0xfeff ? content.slice(1) : content;
2374
2907
  }
2375
2908
  function buildKnowledgeDispatch(input) {
2909
+ if (input.host === "cindy") {
2910
+ return buildKnowledgeAtomicDispatch(input);
2911
+ }
2376
2912
  return buildKnowledgeDelegateDispatch({
2377
2913
  policy: "subagent",
2378
- ...input,
2914
+ finalizeId: input.finalizeId,
2915
+ writer: input.writer,
2379
2916
  });
2380
2917
  }
2381
2918
  function compactPlanCommandResult(command, result, effectiveHost, completionRefresh, forceProjectionSync = false, knowledgeDispatch) {
@@ -2386,7 +2923,7 @@ function compactPlanCommandResult(command, result, effectiveHost, completionRefr
2386
2923
  const resolvedPlanPath = archivedPlanPath ?? result.planPath;
2387
2924
  const codexResult = effectiveHost === "codex";
2388
2925
  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 }) : [];
2926
+ const hostActions = codexResult ? buildCodexHostActions(result, { forceProjectionSync, actionIdPrefix: command === "plan.sync" ? `plan.sync:${createHash("sha256").update(result.planPath).digest("hex").slice(0, 16)}` : undefined }) : [];
2390
2927
  const nextsteps = codexResult
2391
2928
  && result.planStatus === "end.completed"
2392
2929
  && result.workflowGuidance.goalTool?.tool === "update_goal"
@@ -2461,87 +2998,6 @@ function compactPlanCommandResult(command, result, effectiveHost, completionRefr
2461
2998
  ...(!codexResult || !includePlan ? { planSummary } : {}),
2462
2999
  };
2463
3000
  }
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
3001
  function compactDirectCommandResult(command, workflowGuidance, completionRefresh) {
2546
3002
  return {
2547
3003
  ok: true,
@@ -2629,7 +3085,7 @@ function readOrderedPlanEditOperations(args) {
2629
3085
  }
2630
3086
  return operations;
2631
3087
  }
2632
- function requestsPlanEndState(operations) {
3088
+ function requestsPlanEndTerminal(operations) {
2633
3089
  return operations.some((operation) => operation.type === "plan.status" && operation.status.startsWith("end."));
2634
3090
  }
2635
3091
  function planCompletionOperations(updates) {