@veewo/claw 0.2.2 → 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: {
@@ -394,6 +403,28 @@ const COMMAND_HELP = {
394
403
  description: "Internal: warms the configured local persistent embedding session after context recovery without delaying SessionStart.",
395
404
  options: [{ flag: "--cwd <dir>", detail: "(required) Project root." }],
396
405
  },
406
+ "internal-daily-maintenance": {
407
+ usage: ["{script} internal-daily-maintenance --cwd <dir> [--session-key <key>] [--session-only]"],
408
+ description: "Internal: runs the lock-protected daily cleanup asynchronously after context recovery.",
409
+ options: [
410
+ { flag: "--cwd <dir>", detail: "(required) Current workspace directory." },
411
+ { flag: "--session-key <key>", detail: "Optional owner session key used for session workflow maintenance." },
412
+ { flag: "--session-only", detail: "Only maintain the session workflow; do not touch project tasks." },
413
+ ],
414
+ },
415
+ "internal-knowledge-sweep": {
416
+ usage: ["{script} internal-knowledge-sweep --cwd <dir>"],
417
+ description: "Internal: discovers retryable knowledge jobs and launches their detached finalizers.",
418
+ options: [{ flag: "--cwd <dir>", detail: "(required) Project root." }],
419
+ },
420
+ "internal-background-maintenance": {
421
+ usage: ["{script} internal-background-maintenance --cwd <dir> [--session-key <key>]"],
422
+ description: "Internal: runs non-blocking cleanup, embedding warmup, and knowledge job discovery in one worker.",
423
+ options: [
424
+ { flag: "--cwd <dir>", detail: "(required) Current workspace directory." },
425
+ { flag: "--session-key <key>", detail: "Optional owner session key used for session workflow maintenance." },
426
+ ],
427
+ },
397
428
  };
398
429
  async function main() {
399
430
  const args = process.argv.slice(2);
@@ -449,7 +480,7 @@ async function main() {
449
480
  printJson(buildPublicContextOutput(await runContextCommand(args, process.cwd(), resolveOwnerSessionKey(), effectiveHost)));
450
481
  return;
451
482
  case "session":
452
- runSession(args);
483
+ await runSession(args, effectiveHost);
453
484
  return;
454
485
  case "check":
455
486
  const checkResult = ensureProjectProtocol(process.cwd());
@@ -467,7 +498,7 @@ async function main() {
467
498
  await runPlan(args, effectiveHost);
468
499
  return;
469
500
  case "codex":
470
- runCodex(args);
501
+ await runCodex(args);
471
502
  return;
472
503
  case "template":
473
504
  await runTemplate(args);
@@ -527,6 +558,15 @@ async function main() {
527
558
  case "internal-embedding-warmup":
528
559
  await runInternalEmbeddingWarmup(args);
529
560
  return;
561
+ case "internal-daily-maintenance":
562
+ runInternalDailyMaintenance(args);
563
+ return;
564
+ case "internal-knowledge-sweep":
565
+ runInternalKnowledgeSweep(args);
566
+ return;
567
+ case "internal-background-maintenance":
568
+ await runInternalBackgroundMaintenance(args);
569
+ return;
530
570
  default:
531
571
  printTopLevelUsage();
532
572
  process.exitCode = 1;
@@ -536,16 +576,55 @@ async function main() {
536
576
  handleError(error);
537
577
  }
538
578
  }
539
- function runCodex(args) {
579
+ async function runCodex(args) {
540
580
  const subcommand = args.shift();
541
- if (subcommand !== "driver") {
542
- 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;
585
+ }
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;
543
613
  }
544
- assertNoRemainingArgs(args, "codex driver");
545
- printJson(buildCodexDriverEnvelope(CLI_VERSION));
614
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown codex subcommand "${subcommand ?? ""}".`);
546
615
  }
547
- function runSession(args) {
616
+ async function runSession(args, effectiveHost) {
548
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
+ }
549
628
  if (subcommand !== "clean") {
550
629
  throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown session subcommand "${subcommand ?? ""}".`);
551
630
  }
@@ -566,19 +645,390 @@ function runSession(args) {
566
645
  removed: deleteSessionWorkflow(ownerSessionKey),
567
646
  });
568
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
+ }
569
980
  function runKnowledge(args) {
570
981
  const subcommand = args.shift();
571
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
+ }
572
1006
  case "wait": {
573
1007
  const projectRoot = path.resolve(readRequiredFlag(args, "--project-root"));
574
1008
  const finalizeId = readRequiredFlag(args, "--finalize-id");
1009
+ const sessionKey = readOptionalFlag(args, "--session-key");
575
1010
  const timeoutMs = readOptionalNumber(args, "--timeout-ms") ?? 300_000;
576
1011
  assertNoRemainingArgs(args, "knowledge wait");
577
- const { jobPath, job } = waitForKnowledgeFinalizationJobReady({
578
- project: resolveProjectContext(projectRoot),
579
- finalizeId,
580
- timeoutMs,
581
- });
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);
582
1032
  printJson({
583
1033
  ok: true,
584
1034
  command: "knowledge.wait",
@@ -684,11 +1134,11 @@ async function runPlan(args, effectiveHost) {
684
1134
  throw new ClawError("PROJECT_CONFIG_INVALID", "plan edit requires at least one plan field or --status.");
685
1135
  }
686
1136
  const ownerSessionKey = resolveOwnerSessionKey() ?? undefined;
687
- const entersEndState = requestsPlanEndState(operations);
688
- const current = entersEndState
1137
+ const entersEndTerminal = requestsPlanEndTerminal(operations);
1138
+ const current = entersEndTerminal
689
1139
  ? showPlan({ cwd: process.cwd(), ...target, ownerSessionKey })
690
1140
  : undefined;
691
- const project = entersEndState ? tryResolveHookProject(process.cwd()) : null;
1141
+ const project = entersEndTerminal ? tryResolveHookProject(process.cwd()) : null;
692
1142
  const effectiveWriter = current && project
693
1143
  ? resolvePlanEffectiveConfig(project.projectConfig, current.plan)?.knowledgeWriter
694
1144
  : undefined;
@@ -698,7 +1148,7 @@ async function runPlan(args, effectiveHost) {
698
1148
  && effectiveHost !== "codex") {
699
1149
  throw new ClawError("PROJECT_CONFIG_INVALID", 'knowledgeWriter.executionPolicy "subagent" is supported only by the Codex host.', { host: effectiveHost ?? null });
700
1150
  }
701
- const queuePlanEndFinalization = entersEndState
1151
+ const queuePlanEndFinalization = entersEndTerminal
702
1152
  ? preparePlanEndFinalization(process.cwd(), ownerSessionKey)
703
1153
  : undefined;
704
1154
  const result = await editPlan({
@@ -717,8 +1167,6 @@ async function runPlan(args, effectiveHost) {
717
1167
  && effectiveWriter?.executionPolicy === "subagent"
718
1168
  && result.knowledgeFinalizeId)
719
1169
  ? buildKnowledgeDispatch({
720
- projectRoot: project.projectRoot,
721
- taskName: result.taskName,
722
1170
  finalizeId: result.knowledgeFinalizeId,
723
1171
  writer: effectiveWriter,
724
1172
  })
@@ -820,8 +1268,6 @@ async function runPlan(args, effectiveHost) {
820
1268
  && effectiveWriter?.executionPolicy === "subagent"
821
1269
  && result.knowledgeFinalizeId)
822
1270
  ? buildKnowledgeDispatch({
823
- projectRoot: project.projectRoot,
824
- taskName: result.taskName,
825
1271
  finalizeId: result.knowledgeFinalizeId,
826
1272
  writer: effectiveWriter,
827
1273
  })
@@ -830,6 +1276,7 @@ async function runPlan(args, effectiveHost) {
830
1276
  return;
831
1277
  }
832
1278
  case "show": {
1279
+ const simple = readBooleanFlag(args, "--simple");
833
1280
  const target = readPlanMutationTarget(args);
834
1281
  assertNoRemainingArgs(args, "plan show");
835
1282
  const result = showPlan({
@@ -837,6 +1284,10 @@ async function runPlan(args, effectiveHost) {
837
1284
  ...target,
838
1285
  ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
839
1286
  });
1287
+ if (simple) {
1288
+ printJson(result.simplePlanView);
1289
+ return;
1290
+ }
840
1291
  printJson({
841
1292
  ok: true,
842
1293
  command: "plan.show",
@@ -1025,11 +1476,12 @@ async function runSearch(args) {
1025
1476
  if (args.includes("--scope") || args.includes("--task")) {
1026
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.");
1027
1478
  }
1479
+ const dir = readOptionalFlag(args, "--dir");
1028
1480
  printJson({
1029
1481
  ok: true,
1030
1482
  command: "search",
1031
1483
  ...await searchMemoryAsync({
1032
- cwd: process.cwd(),
1484
+ cwd: dir ? path.resolve(process.cwd(), dir) : process.cwd(),
1033
1485
  limit: readOptionalNumber(args, "--limit"),
1034
1486
  query: readRequiredSearchQuery(args),
1035
1487
  scope: "project",
@@ -1057,7 +1509,15 @@ async function runContextCommand(args, cwd = process.cwd(), ownerSessionKey = re
1057
1509
  let fixedPaths = [];
1058
1510
  const sessionProject = resolveSessionWorkflowContext(ownerSessionKey ?? undefined);
1059
1511
  if (sessionProject) {
1060
- const maintenance = runDailyMaintenance(sessionProject, { excludeSessionKey: ownerSessionKey ?? undefined, includeProject: false });
1512
+ const maintenance = effectiveHost
1513
+ ? null
1514
+ : runDailyMaintenance(sessionProject, {
1515
+ excludeSessionKey: ownerSessionKey ?? undefined,
1516
+ includeProject: false,
1517
+ });
1518
+ if (effectiveHost) {
1519
+ launchDailyMaintenance(cwd, ownerSessionKey, true);
1520
+ }
1061
1521
  const activeWorkflow = !taskName && ownerSessionKey
1062
1522
  ? await tryResolveActiveWorkflowSnapshot(cwd, ownerSessionKey, effectiveHost)
1063
1523
  : null;
@@ -1068,7 +1528,7 @@ async function runContextCommand(args, cwd = process.cwd(), ownerSessionKey = re
1068
1528
  return {
1069
1529
  project: sessionProject,
1070
1530
  ...(activeWorkflow ? { activeWorkflow } : {}),
1071
- ...(maintenance.ran ? { maintenance } : {}),
1531
+ ...(maintenance?.ran ? { maintenance } : {}),
1072
1532
  ...(codexRuntimeError ? { error: codexRuntimeError } : {}),
1073
1533
  };
1074
1534
  }
@@ -1084,7 +1544,13 @@ async function runContextCommand(args, cwd = process.cwd(), ownerSessionKey = re
1084
1544
  initProject({ cwd, version: CLI_VERSION });
1085
1545
  initialized = true;
1086
1546
  }
1087
- const maintenance = runDailyMaintenance(resolveProjectContext(cwd), { excludeSessionKey: ownerSessionKey ?? undefined });
1547
+ const project = resolveProjectContext(cwd);
1548
+ const maintenance = effectiveHost
1549
+ ? null
1550
+ : runDailyMaintenance(project, { excludeSessionKey: ownerSessionKey ?? undefined });
1551
+ if (effectiveHost) {
1552
+ launchDailyMaintenance(cwd, ownerSessionKey, false);
1553
+ }
1088
1554
  let resolved = resolveContext(cwd, taskName);
1089
1555
  const versionSync = syncProjectVersionWithCli(cwd, resolved.project);
1090
1556
  if (versionSync.projectVersionUpdated) {
@@ -1104,7 +1570,7 @@ async function runContextCommand(args, cwd = process.cwd(), ownerSessionKey = re
1104
1570
  launchProjectEmbeddingWarmup(resolved.project);
1105
1571
  return {
1106
1572
  ...resolved,
1107
- ...(maintenance.ran ? { maintenance } : {}),
1573
+ ...(maintenance?.ran ? { maintenance } : {}),
1108
1574
  ...(activeWorkflow ? { activeWorkflow } : {}),
1109
1575
  ...(codexRuntimeError ? { error: codexRuntimeError } : {}),
1110
1576
  protocolCheck: checkProjectProtocol(cwd),
@@ -1152,6 +1618,28 @@ function launchProjectEmbeddingWarmup(project) {
1152
1618
  // Context recovery is authoritative; embedding warmup is fail-open latency work.
1153
1619
  }
1154
1620
  }
1621
+ function launchDailyMaintenance(cwd, ownerSessionKey, sessionOnly) {
1622
+ const args = [resolveCliEntryPath(), "internal-daily-maintenance", "--cwd", cwd];
1623
+ if (ownerSessionKey) {
1624
+ args.push("--session-key", ownerSessionKey);
1625
+ }
1626
+ if (sessionOnly) {
1627
+ args.push("--session-only");
1628
+ }
1629
+ try {
1630
+ const child = spawn(process.execPath, args, {
1631
+ cwd,
1632
+ detached: true,
1633
+ stdio: "ignore",
1634
+ windowsHide: true,
1635
+ env: withoutInvocationHost(),
1636
+ });
1637
+ child.unref();
1638
+ }
1639
+ catch {
1640
+ // Context recovery is authoritative; daily cleanup is fail-open latency work.
1641
+ }
1642
+ }
1155
1643
  function buildPublicContextOutput(context) {
1156
1644
  const project = asJsonRecord(context.project);
1157
1645
  const output = {};
@@ -1206,7 +1694,7 @@ function buildPublicContextOutput(context) {
1206
1694
  if (Object.keys(compactRecovery).length > 0) {
1207
1695
  output.startupRecovery = compactRecovery;
1208
1696
  }
1209
- const searchGuidance = buildContextSearchGuidance(context);
1697
+ const searchGuidance = buildContextSearchGuidance(context, "rg");
1210
1698
  if (searchGuidance) {
1211
1699
  output.searchGuidance = searchGuidance;
1212
1700
  }
@@ -1218,20 +1706,26 @@ function shouldExposeVersionSync(versionSync) {
1218
1706
  || versionSync.updateAvailable === true
1219
1707
  || versionSync.projectVersion !== versionSync.cliVersion;
1220
1708
  }
1221
- function buildContextSearchGuidance(context) {
1709
+ function buildContextSearchGuidance(context, style = "default") {
1222
1710
  const project = asJsonRecord(context.project);
1223
1711
  const projectConfig = asJsonRecord(project?.projectConfig);
1224
1712
  const memory = asJsonRecord(projectConfig?.memory);
1225
1713
  const embeddingEnabled = memory?.enabled === true && asJsonRecord(memory.embedding) !== null;
1226
1714
  const gitnexusEnabled = projectConfig?.gitnexus === true;
1227
1715
  if (embeddingEnabled && gitnexusEnabled) {
1228
- 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.";
1229
1719
  }
1230
1720
  if (embeddingEnabled) {
1231
- 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.";
1232
1724
  }
1233
1725
  if (gitnexusEnabled) {
1234
- 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.";
1235
1729
  }
1236
1730
  return null;
1237
1731
  }
@@ -1441,18 +1935,20 @@ async function runInternalKnowledgeCapture(args, effectiveHost) {
1441
1935
  const sessionId = resolveOwnerSessionKey(payload);
1442
1936
  const turnId = readHookString(payload, "turn_id");
1443
1937
  const message = readHookString(payload, "message");
1444
- if (!hookCwd || !sessionId || !turnId || !message || !containsClawDir(hookCwd)) {
1938
+ const taskConclusions = readHookTaskConclusions(payload, turnId);
1939
+ if (!hookCwd || !sessionId || !turnId || !message) {
1445
1940
  printJson({ ok: true, captured: false });
1446
1941
  return;
1447
1942
  }
1448
1943
  try {
1449
- const project = resolveProjectContext(hookCwd);
1944
+ const project = resolveWorkflowProjectContext(hookCwd, sessionId);
1450
1945
  const result = tryCaptureKnowledgeStop({
1451
1946
  project,
1452
1947
  sessionId,
1453
1948
  turnId,
1454
1949
  message,
1455
1950
  host: effectiveHost === "cindy" ? "cindy" : effectiveHost,
1951
+ taskConclusions,
1456
1952
  });
1457
1953
  printJson(result);
1458
1954
  }
@@ -1527,7 +2023,7 @@ function completeKnowledgeFinalizationJob(jobPath, result, claimToken) {
1527
2023
  if (running.claimToken !== claimToken) {
1528
2024
  throw new Error("Knowledge finalization completion does not match the active claim.");
1529
2025
  }
1530
- const project = resolveProjectContext(running.projectRoot);
2026
+ const project = resolveKnowledgeJobProject(jobPath, running);
1531
2027
  const finishedAt = new Date().toISOString();
1532
2028
  const truthEncoding = normalizeTruthMarkdownEncoding(project);
1533
2029
  recordKnowledgeFinalizationResult(project, running.reportPath, {
@@ -1561,6 +2057,17 @@ function completeKnowledgeFinalizationJob(jobPath, result, claimToken) {
1561
2057
  });
1562
2058
  printJson({ ok: true, completed: true, alreadyDone: terminal.alreadyDone, finalizeId: running.finalizeId });
1563
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
+ }
1564
2071
  function failKnowledgeFinalizationJob(jobPath, message, claimToken) {
1565
2072
  const terminal = doneKnowledgeFinalizationJob({
1566
2073
  jobPath,
@@ -1583,6 +2090,86 @@ async function runInternalEmbeddingWarmup(args) {
1583
2090
  ...await warmProjectMemoryEmbedding({ cwd }),
1584
2091
  });
1585
2092
  }
2093
+ function runInternalDailyMaintenance(args) {
2094
+ const cwd = readRequiredFlag(args, "--cwd");
2095
+ const sessionKey = readOptionalFlag(args, "--session-key");
2096
+ const sessionOnly = readBooleanFlag(args, "--session-only");
2097
+ assertNoRemainingArgs(args, "internal-daily-maintenance");
2098
+ const project = sessionOnly
2099
+ ? (sessionKey ? resolveSessionWorkflowContext(sessionKey) : null)
2100
+ : resolveProjectContext(cwd);
2101
+ if (!project) {
2102
+ printJson({ command: "internal-daily-maintenance", ok: true, ran: false, skipped: true });
2103
+ return;
2104
+ }
2105
+ printJson({
2106
+ command: "internal-daily-maintenance",
2107
+ ok: true,
2108
+ ...runDailyMaintenance(project, {
2109
+ excludeSessionKey: sessionKey ?? undefined,
2110
+ includeProject: !sessionOnly,
2111
+ }),
2112
+ });
2113
+ }
2114
+ function runInternalKnowledgeSweep(args) {
2115
+ const cwd = readRequiredFlag(args, "--cwd");
2116
+ assertNoRemainingArgs(args, "internal-knowledge-sweep");
2117
+ const project = tryResolveHookProject(cwd);
2118
+ if (!project) {
2119
+ printJson({ command: "internal-knowledge-sweep", ok: true, launched: 0, skipped: true });
2120
+ return;
2121
+ }
2122
+ const jobs = listRetryableKnowledgeFinalizationJobs(project, { excludeHosts: ["cindy"] });
2123
+ let launched = 0;
2124
+ for (const jobPath of jobs) {
2125
+ try {
2126
+ launchKnowledgeFinalizationWorker(jobPath, project.projectRoot);
2127
+ launched += 1;
2128
+ }
2129
+ catch {
2130
+ // A later background sweep can retry a job that could not be launched.
2131
+ }
2132
+ }
2133
+ printJson({ command: "internal-knowledge-sweep", ok: true, discovered: jobs.length, launched });
2134
+ }
2135
+ async function runInternalBackgroundMaintenance(args) {
2136
+ const cwd = readRequiredFlag(args, "--cwd");
2137
+ const sessionKey = readOptionalFlag(args, "--session-key");
2138
+ assertNoRemainingArgs(args, "internal-background-maintenance");
2139
+ const sessionProject = sessionKey ? resolveSessionWorkflowContext(sessionKey) : null;
2140
+ const project = sessionProject ? null : tryResolveHookProject(cwd);
2141
+ let maintenance = { ran: false, skipped: true };
2142
+ if (sessionProject) {
2143
+ maintenance = runDailyMaintenance(sessionProject, { excludeSessionKey: sessionKey ?? undefined, includeProject: false });
2144
+ }
2145
+ else if (project) {
2146
+ maintenance = runDailyMaintenance(project, { excludeSessionKey: sessionKey ?? undefined });
2147
+ }
2148
+ let embedding = { warmed: false, reason: "skipped" };
2149
+ try {
2150
+ embedding = await warmProjectMemoryEmbedding({ cwd });
2151
+ }
2152
+ catch (error) {
2153
+ embedding = { warmed: false, reason: "failed", error: error instanceof Error ? error.message : String(error) };
2154
+ }
2155
+ let discovered = 0;
2156
+ let launched = 0;
2157
+ const knowledgeProject = project ?? sessionProject;
2158
+ if (knowledgeProject) {
2159
+ const jobs = listRetryableKnowledgeFinalizationJobs(knowledgeProject, { excludeHosts: ["cindy"] });
2160
+ discovered = jobs.length;
2161
+ for (const jobPath of jobs) {
2162
+ try {
2163
+ launchKnowledgeFinalizationWorker(jobPath, knowledgeProject.projectRoot);
2164
+ launched += 1;
2165
+ }
2166
+ catch {
2167
+ // A later background sweep can retry a job that could not be launched.
2168
+ }
2169
+ }
2170
+ }
2171
+ printJson({ command: "internal-background-maintenance", ok: true, maintenance, embedding, discovered, launched });
2172
+ }
1586
2173
  async function runInternalKnowledgeFinalize(args) {
1587
2174
  const jobPath = readRequiredFlag(args, "--job");
1588
2175
  const queued = readKnowledgeFinalizationJob(jobPath);
@@ -1640,8 +2227,6 @@ async function runInternalKnowledgeFinalize(args) {
1640
2227
  async function runKnowledgeDelegateForJob(running) {
1641
2228
  const dispatch = buildKnowledgeDelegateDispatch({
1642
2229
  policy: "background",
1643
- projectRoot: running.projectRoot,
1644
- taskName: running.taskName,
1645
2230
  finalizeId: running.finalizeId,
1646
2231
  writer: running.writer,
1647
2232
  });
@@ -1770,8 +2355,8 @@ async function runSessionStartHook(effectiveHost) {
1770
2355
  try {
1771
2356
  const context = await runContextCommand([], hookCwd, ownerSessionKey, effectiveHost);
1772
2357
  const contextProject = asJsonRecord(context.project);
1773
- const retryableJobs = contextProject?.scope !== "session" && !context.error
1774
- ? listRetryableKnowledgeFinalizationJobs(resolveProjectContext(hookCwd))
2358
+ const retryableJobs = effectiveHost !== "cindy" && contextProject?.scope !== "session" && !context.error
2359
+ ? listRetryableKnowledgeFinalizationJobs(resolveProjectContext(hookCwd), { excludeHosts: ["cindy"] })
1775
2360
  : [];
1776
2361
  if (contextProject?.scope !== "session"
1777
2362
  && !context.error
@@ -1795,7 +2380,6 @@ async function runSessionStartHook(effectiveHost) {
1795
2380
  hookSpecificOutput: {
1796
2381
  hookEventName: "SessionStart",
1797
2382
  additionalContext,
1798
- ...(effectiveHost === "cindy" ? { knowledgeJobs: retryableJobs } : {}),
1799
2383
  },
1800
2384
  })}\n`);
1801
2385
  }
@@ -1829,6 +2413,21 @@ function readHookString(payload, key) {
1829
2413
  const value = payload[key];
1830
2414
  return typeof value === "string" && value.trim() ? value.trim() : null;
1831
2415
  }
2416
+ function readHookTaskConclusions(payload, turnId) {
2417
+ if (!turnId || !payload || typeof payload !== "object")
2418
+ return [];
2419
+ const value = payload.task_conclusions;
2420
+ if (!Array.isArray(value))
2421
+ return [];
2422
+ return value.flatMap((item) => {
2423
+ if (!item || typeof item !== "object")
2424
+ return [];
2425
+ const message = item.message;
2426
+ return typeof message === "string" && message.trim()
2427
+ ? [{ turnId, message: message.trim() }]
2428
+ : [];
2429
+ });
2430
+ }
1832
2431
  function containsClawDir(cwd) {
1833
2432
  try {
1834
2433
  const startDir = path.resolve(cwd);
@@ -1869,10 +2468,10 @@ function safeResolveTempDir() {
1869
2468
  }
1870
2469
  }
1871
2470
  function buildSessionStartAdditionalContext(context, sessionCwd, effectiveHost) {
2471
+ const versionSyncPrompt = buildVersionSyncPrompt(context);
1872
2472
  if (effectiveHost === "cindy") {
1873
- return buildCindySessionStartContext(context, sessionCwd);
2473
+ return buildCindySessionStartContext(context, sessionCwd, versionSyncPrompt);
1874
2474
  }
1875
- const versionSyncPrompt = buildVersionSyncPrompt(context);
1876
2475
  const searchGuidance = buildContextSearchGuidance(context);
1877
2476
  const runtimeErrorPrompt = buildCodexRuntimeErrorPrompt(context);
1878
2477
  const activeWorkflow = context.activeWorkflow;
@@ -1909,37 +2508,32 @@ function buildSessionStartAdditionalContext(context, sessionCwd, effectiveHost)
1909
2508
  }
1910
2509
  /**
1911
2510
  * Cindy Agents use the Ghost Tool gateway, not shell commands or Host actions.
1912
- * Keep startup recovery to the actionable plan snapshot only; the worker owns
1913
- * session identity, host selection, and any future Goal projection.
2511
+ * Match the Codex session-start wording and structure. Cindy's static entry
2512
+ * prompt supplies the shared skill and guidance lines; this dynamic context
2513
+ * supplies project identity or a recovered workflow snapshot. Goal Mode is
2514
+ * intentionally omitted because Cindy does not expose that Host surface.
1914
2515
  */
1915
- function buildCindySessionStartContext(context, sessionCwd) {
1916
- const activeWorkflow = asJsonRecord(context.activeWorkflow);
1917
- if (activeWorkflow) {
1918
- const taskName = typeof activeWorkflow.taskName === "string" ? activeWorkflow.taskName.trim() : "current task";
1919
- const planStatus = typeof activeWorkflow.planStatus === "string" ? activeWorkflow.planStatus.trim() : "unknown";
1920
- const planSummary = typeof activeWorkflow.planSummary === "string" ? activeWorkflow.planSummary.trim() : "";
1921
- const nextTask = asJsonRecord(activeWorkflow.workflowGuidance)?.nextTask;
1922
- const nextTaskTitle = asJsonRecord(nextTask)?.title;
1923
- const lines = [
1924
- "claw-kit recovered a Cindy workflow.",
1925
- `- task: ${taskName}`,
1926
- `- plan status: ${planStatus}`,
1927
- ...(planSummary ? [`- plan summary: ${planSummary}`] : []),
1928
- ...(typeof nextTaskTitle === "string" && nextTaskTitle.trim() ? [`- next task: ${nextTaskTitle.trim()}`] : []),
1929
- "Use the claw-kit Ghost tools to inspect or advance this workflow. Do not run claw shell commands or manage host/session/Goal state yourself.",
1930
- ];
1931
- return lines.join("\n");
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(", ")}` : "."}`);
1932
2524
  }
1933
- const project = asJsonRecord(context.project);
1934
- if (!project)
1935
- return null;
1936
- const projectName = typeof project.projectName === "string" && project.projectName.trim()
1937
- ? project.projectName.trim()
1938
- : path.basename(String(project.projectRoot ?? sessionCwd ?? "project"));
1939
- return [
1940
- `claw-kit is ready for the Cindy workspace: ${projectName}.`,
1941
- "Use the claw-kit Ghost tools when this work needs a plan; do not invoke claw shell commands directly.",
1942
- ].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;
2531
+ }
2532
+ function stripCindyGoalModeLines(prompt) {
2533
+ return prompt
2534
+ .split("\n")
2535
+ .filter((line) => !/goal mode/i.test(line))
2536
+ .join("\n");
1943
2537
  }
1944
2538
  function buildCodexRuntimeErrorPrompt(context) {
1945
2539
  const error = asJsonRecord(context.error);
@@ -2224,7 +2818,7 @@ function compactPlanCommandResult(command, result, effectiveHost, completionRefr
2224
2818
  const resolvedPlanPath = archivedPlanPath ?? result.planPath;
2225
2819
  const codexResult = effectiveHost === "codex";
2226
2820
  const cindyResult = effectiveHost === "cindy";
2227
- 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 }) : [];
2228
2822
  const nextsteps = codexResult
2229
2823
  && result.planStatus === "end.completed"
2230
2824
  && result.workflowGuidance.goalTool?.tool === "update_goal"
@@ -2299,87 +2893,6 @@ function compactPlanCommandResult(command, result, effectiveHost, completionRefr
2299
2893
  ...(!codexResult || !includePlan ? { planSummary } : {}),
2300
2894
  };
2301
2895
  }
2302
- function buildHostActions(result, options = {}) {
2303
- const latestEvent = result.events?.at(-1);
2304
- const actionIdPrefix = options.actionIdPrefix ?? latestEvent?.mutationId;
2305
- if (!actionIdPrefix) {
2306
- return [];
2307
- }
2308
- const actions = [];
2309
- const goalTool = result.workflowGuidance.goalTool;
2310
- const isSubplanGoalHandoff = Boolean(result.plan?.parentPlan
2311
- && goalTool?.tool === "update_goal"
2312
- && goalTool.status === "complete");
2313
- if (isSubplanGoalHandoff && goalTool?.tool === "update_goal") {
2314
- actions.push({
2315
- schemaVersion: 1,
2316
- id: `${actionIdPrefix}:update_goal`,
2317
- tool: "update_goal",
2318
- input: {
2319
- status: "complete",
2320
- },
2321
- });
2322
- }
2323
- if (result.plan
2324
- && shouldUsePlanHostIntegration(result.plan)
2325
- && result.plan.tasks.length > 0
2326
- && (options.forceProjectionSync || codexPlanProjectionChanged(result.previousPlan, result.plan, result.planStatus))) {
2327
- const plan = buildCodexPlanProjection(result.plan, result.planStatus);
2328
- actions.push({
2329
- schemaVersion: 1,
2330
- id: `${actionIdPrefix}:update_plan`,
2331
- tool: "update_plan",
2332
- input: {
2333
- explanation: result.workflowGuidance.summary,
2334
- plan,
2335
- },
2336
- });
2337
- }
2338
- if (goalTool && !isSubplanGoalHandoff) {
2339
- if (goalTool.tool === "create_goal") {
2340
- actions.push({
2341
- schemaVersion: 1,
2342
- id: `${actionIdPrefix}:create_goal`,
2343
- tool: "create_goal",
2344
- input: {
2345
- objective: goalTool.objective,
2346
- },
2347
- });
2348
- }
2349
- else {
2350
- const codexStatus = result.planStatus === "process.wait" || result.planStatus === "process.discussing"
2351
- ? "complete"
2352
- : goalTool.status;
2353
- actions.push({
2354
- schemaVersion: 1,
2355
- id: `${actionIdPrefix}:update_goal`,
2356
- tool: "update_goal",
2357
- input: {
2358
- status: codexStatus,
2359
- },
2360
- });
2361
- }
2362
- }
2363
- return actions;
2364
- }
2365
- function codexPlanProjectionChanged(previousPlan, plan, planStatus) {
2366
- if (!previousPlan)
2367
- return true;
2368
- return JSON.stringify(buildCodexPlanProjection(previousPlan, previousPlan.status))
2369
- !== JSON.stringify(buildCodexPlanProjection(plan, planStatus));
2370
- }
2371
- function buildCodexPlanProjection(plan, planStatus) {
2372
- const activeTask = plan.tasks.find((task) => task.status === "in_progress" || task.status === "subagent_running") ?? (planStatus === "process.active"
2373
- ? plan.tasks.find((task) => task.status !== "done")
2374
- : undefined);
2375
- return plan.tasks.map((task) => {
2376
- let status = task.status === "done" ? "completed" : "pending";
2377
- if (task.id === activeTask?.id) {
2378
- status = "in_progress";
2379
- }
2380
- return { step: task.title, status };
2381
- });
2382
- }
2383
2896
  function compactDirectCommandResult(command, workflowGuidance, completionRefresh) {
2384
2897
  return {
2385
2898
  ok: true,
@@ -2467,7 +2980,7 @@ function readOrderedPlanEditOperations(args) {
2467
2980
  }
2468
2981
  return operations;
2469
2982
  }
2470
- function requestsPlanEndState(operations) {
2983
+ function requestsPlanEndTerminal(operations) {
2471
2984
  return operations.some((operation) => operation.type === "plan.status" && operation.status.startsWith("end."));
2472
2985
  }
2473
2986
  function planCompletionOperations(updates) {