@veewo/claw 0.1.99 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import path from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { createHash } from "node:crypto";
7
7
  import { spawn, spawnSync } from "node:child_process";
8
- import { buildDirectWorkflowGuidance, DEFAULT_MAX_TASKS_TO_KEEP, checkProjectProtocol, ClawError, buildPlanWorkflowGuidance, shouldUsePlanHostIntegration, buildMemoryIndex, buildSessionStartDefaultPrompt, buildSessionStartRecoveredPrompt, editPlan, ensureProjectProtocol, enforceTaskRetention, findTaskDirectory, runDailyMaintenance, ingestTruth, initProject, getTemplateTaskDoneChoices, resolvePlanTemplateFile, resolveProjectContext, resolveWorkflowProjectContext, resolveSessionWorkflowContext, deleteSessionWorkflow, sweepExpiredSessionWorkflows, resolveSessionBoundPlan, resolveContext, resolveSeedPlanTemplate, searchMemoryAsync, warmProjectMemoryEmbedding, showPlan, createSubplan, switchTask, tryCaptureKnowledgeStop, claimKnowledgeFinalizationJob, changedKnowledgeMarkdownPaths, listRetryableKnowledgeFinalizationJobs, governChangedKnowledgeMarkdown, normalizeTruthMarkdownEncoding, recordKnowledgeFinalizationResult, snapshotKnowledgeMarkdown, writeKnowledgeFinalizationJob, unbindSession, writePlan, } from "@veewo/claw-core";
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
9
  import { buildCodexDriverEnvelope } from "./codex-driver.js";
10
10
  import { checkCodexRuntime, resolveCodexSdkEntryPath } from "./codex-runtime.js";
11
11
  import { extractLatestFinalAssistantMessage, extractTaskDoneConclusions, } from "./codex-transcript.js";
@@ -25,6 +25,7 @@ const TOP_LEVEL_COMMANDS = [
25
25
  { name: "subplan create [options]", summary: "Create a subplan nested under a parent task item." },
26
26
  { name: "switch-task --from <task> --to <task>", summary: "Switch the active task, carrying inherited context." },
27
27
  { name: "search [<query>] [options]", summary: "Recall project memory, truth, ADR, and declared docs." },
28
+ { name: "knowledge <subcommand> [options]", summary: "Knowledge finalization lifecycle: wait, claim, done." },
28
29
  { name: "truth ingest [options]", summary: "Ingest a truth document under .claw/truth." },
29
30
  { name: "hook <event-name>", summary: "Emit host hook output (e.g. SessionStart)." },
30
31
  ];
@@ -311,6 +312,43 @@ const COMMAND_HELP = {
311
312
  },
312
313
  },
313
314
  },
315
+ knowledge: {
316
+ usage: ["{script} knowledge <subcommand> [options]"],
317
+ description: "Session-bound lifecycle commands for a queued knowledge finalization job.",
318
+ subcommands: {
319
+ wait: {
320
+ usage: ["{script} knowledge wait --project-root <path> --finalize-id <id> [--timeout-ms <n>]"],
321
+ description: "Wait for Stop capture to create a knowledge finalization job. This command does not create or inspect session bindings.",
322
+ summary: "Wait until the finalization job exists.",
323
+ options: [
324
+ { flag: "--project-root <path>", detail: "(required) Project that owns the pending finalization." },
325
+ { flag: "--finalize-id <id>", detail: "(required) Stable finalization id returned by plan done." },
326
+ { flag: "--timeout-ms <n>", detail: "Maximum wait in milliseconds (default 300000)." },
327
+ ],
328
+ },
329
+ 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." }],
334
+ },
335
+ done: {
336
+ usage: [
337
+ "{script} knowledge done --job <path> --claim-token <token> --status succeeded --result <text>",
338
+ "{script} knowledge done --job <path> --claim-token <token> --status failed --error <text>",
339
+ ],
340
+ description: "Persist the terminal result for a claimed knowledge finalization job.",
341
+ summary: "Complete a claimed finalization job.",
342
+ options: [
343
+ { flag: "--job <path>", detail: "(required) Finalization job JSON path." },
344
+ { flag: "--claim-token <token>", detail: "(required) Token returned by knowledge claim." },
345
+ { flag: "--status succeeded|failed", detail: "(required) Terminal execution status." },
346
+ { flag: "--result <text>", detail: "Required when status is succeeded." },
347
+ { flag: "--error <text>", detail: "Required when status is failed." },
348
+ ],
349
+ },
350
+ },
351
+ },
314
352
  truth: {
315
353
  usage: ["{script} truth <subcommand> [options]"],
316
354
  description: "Truth document ingestion under .claw/truth.",
@@ -356,6 +394,28 @@ const COMMAND_HELP = {
356
394
  description: "Internal: warms the configured local persistent embedding session after context recovery without delaying SessionStart.",
357
395
  options: [{ flag: "--cwd <dir>", detail: "(required) Project root." }],
358
396
  },
397
+ "internal-daily-maintenance": {
398
+ usage: ["{script} internal-daily-maintenance --cwd <dir> [--session-key <key>] [--session-only]"],
399
+ description: "Internal: runs the lock-protected daily cleanup asynchronously after context recovery.",
400
+ options: [
401
+ { flag: "--cwd <dir>", detail: "(required) Current workspace directory." },
402
+ { flag: "--session-key <key>", detail: "Optional owner session key used for session workflow maintenance." },
403
+ { flag: "--session-only", detail: "Only maintain the session workflow; do not touch project tasks." },
404
+ ],
405
+ },
406
+ "internal-knowledge-sweep": {
407
+ usage: ["{script} internal-knowledge-sweep --cwd <dir>"],
408
+ description: "Internal: discovers retryable knowledge jobs and launches their detached finalizers.",
409
+ options: [{ flag: "--cwd <dir>", detail: "(required) Project root." }],
410
+ },
411
+ "internal-background-maintenance": {
412
+ usage: ["{script} internal-background-maintenance --cwd <dir> [--session-key <key>]"],
413
+ description: "Internal: runs non-blocking cleanup, embedding warmup, and knowledge job discovery in one worker.",
414
+ options: [
415
+ { flag: "--cwd <dir>", detail: "(required) Current workspace directory." },
416
+ { flag: "--session-key <key>", detail: "Optional owner session key used for session workflow maintenance." },
417
+ ],
418
+ },
359
419
  };
360
420
  async function main() {
361
421
  const args = process.argv.slice(2);
@@ -453,6 +513,9 @@ async function main() {
453
513
  case "search":
454
514
  await runSearch(args);
455
515
  return;
516
+ case "knowledge":
517
+ runKnowledge(args);
518
+ return;
456
519
  case "direct":
457
520
  runDirect(args, effectiveHost);
458
521
  return;
@@ -471,9 +534,30 @@ async function main() {
471
534
  case "internal-knowledge-finalize":
472
535
  await runInternalKnowledgeFinalize(args);
473
536
  return;
537
+ case "internal-knowledge-capture":
538
+ await runInternalKnowledgeCapture(args, effectiveHost);
539
+ return;
540
+ case "internal-knowledge-complete":
541
+ runInternalKnowledgeComplete(args);
542
+ return;
543
+ case "internal-knowledge-claim":
544
+ runInternalKnowledgeClaim(args);
545
+ return;
546
+ case "internal-knowledge-fail":
547
+ runInternalKnowledgeFail(args);
548
+ return;
474
549
  case "internal-embedding-warmup":
475
550
  await runInternalEmbeddingWarmup(args);
476
551
  return;
552
+ case "internal-daily-maintenance":
553
+ runInternalDailyMaintenance(args);
554
+ return;
555
+ case "internal-knowledge-sweep":
556
+ runInternalKnowledgeSweep(args);
557
+ return;
558
+ case "internal-background-maintenance":
559
+ await runInternalBackgroundMaintenance(args);
560
+ return;
477
561
  default:
478
562
  printTopLevelUsage();
479
563
  process.exitCode = 1;
@@ -513,6 +597,87 @@ function runSession(args) {
513
597
  removed: deleteSessionWorkflow(ownerSessionKey),
514
598
  });
515
599
  }
600
+ function runKnowledge(args) {
601
+ const subcommand = args.shift();
602
+ switch (subcommand) {
603
+ case "wait": {
604
+ const projectRoot = path.resolve(readRequiredFlag(args, "--project-root"));
605
+ const finalizeId = readRequiredFlag(args, "--finalize-id");
606
+ const timeoutMs = readOptionalNumber(args, "--timeout-ms") ?? 300_000;
607
+ assertNoRemainingArgs(args, "knowledge wait");
608
+ const { jobPath, job } = waitForKnowledgeFinalizationJobReady({
609
+ project: resolveProjectContext(projectRoot),
610
+ finalizeId,
611
+ timeoutMs,
612
+ });
613
+ printJson({
614
+ ok: true,
615
+ command: "knowledge.wait",
616
+ finalizeId: job.finalizeId,
617
+ status: job.status,
618
+ jobPath,
619
+ });
620
+ return;
621
+ }
622
+ case "claim": {
623
+ const jobPath = readRequiredFlag(args, "--job");
624
+ assertNoRemainingArgs(args, "knowledge claim");
625
+ const job = claimKnowledgeFinalizationJob(jobPath);
626
+ const assignments = job ? buildKnowledgeWriterAssignments(job) : [];
627
+ const templatePath = job
628
+ ? path.join(path.dirname(jobPath), `${job.finalizeId}.assignments.json`)
629
+ : undefined;
630
+ if (job && templatePath) {
631
+ fs.writeFileSync(templatePath, `${JSON.stringify(buildKnowledgeAssignmentTemplate({
632
+ assignments,
633
+ finalizeId: job.finalizeId,
634
+ version: CLI_VERSION,
635
+ }), null, 2)}\n`, "utf-8");
636
+ }
637
+ printJson({
638
+ ok: true,
639
+ command: "knowledge.claim",
640
+ claimed: Boolean(job),
641
+ ...(job ? {
642
+ finalizeId: job.finalizeId,
643
+ claimToken: job.claimToken,
644
+ projectRoot: job.projectRoot,
645
+ writer: job.writer ?? null,
646
+ planPath: job.planPath,
647
+ reportPath: job.reportPath,
648
+ assignments,
649
+ templatePath,
650
+ } : {}),
651
+ });
652
+ return;
653
+ }
654
+ case "done": {
655
+ const jobPath = readRequiredFlag(args, "--job");
656
+ const claimToken = readRequiredFlag(args, "--claim-token");
657
+ const status = readRequiredFlag(args, "--status");
658
+ const result = readOptionalFlag(args, "--result");
659
+ const error = readOptionalFlag(args, "--error");
660
+ assertNoRemainingArgs(args, "knowledge done");
661
+ if (status === "succeeded") {
662
+ if (result === undefined) {
663
+ throw new ClawError("PROJECT_CONFIG_INVALID", "knowledge done --status succeeded requires --result.");
664
+ }
665
+ completeKnowledgeFinalizationJob(jobPath, result, claimToken);
666
+ return;
667
+ }
668
+ if (status === "failed") {
669
+ if (!error) {
670
+ throw new ClawError("PROJECT_CONFIG_INVALID", "knowledge done --status failed requires --error.");
671
+ }
672
+ failKnowledgeFinalizationJob(jobPath, error, claimToken);
673
+ return;
674
+ }
675
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Unsupported knowledge done status "${status}".`);
676
+ }
677
+ default:
678
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown knowledge subcommand "${subcommand ?? ""}".`);
679
+ }
680
+ }
516
681
  async function runPlan(args, effectiveHost) {
517
682
  const subcommand = args.shift();
518
683
  switch (subcommand) {
@@ -551,6 +716,19 @@ async function runPlan(args, effectiveHost) {
551
716
  }
552
717
  const ownerSessionKey = resolveOwnerSessionKey() ?? undefined;
553
718
  const entersEndState = requestsPlanEndState(operations);
719
+ const current = entersEndState
720
+ ? showPlan({ cwd: process.cwd(), ...target, ownerSessionKey })
721
+ : undefined;
722
+ const project = entersEndState ? tryResolveHookProject(process.cwd()) : null;
723
+ const effectiveWriter = current && project
724
+ ? resolvePlanEffectiveConfig(project.projectConfig, current.plan)?.knowledgeWriter
725
+ : undefined;
726
+ if (current
727
+ && !current.plan.parentPlan
728
+ && 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 });
731
+ }
554
732
  const queuePlanEndFinalization = entersEndState
555
733
  ? preparePlanEndFinalization(process.cwd(), ownerSessionKey)
556
734
  : undefined;
@@ -563,7 +741,18 @@ async function runPlan(args, effectiveHost) {
563
741
  ownerSessionKey,
564
742
  });
565
743
  const completionRefresh = queuePlanEndFinalization?.(result.taskName);
566
- printJson(compactPlanCommandResult("plan.edit", result, effectiveHost, completionRefresh));
744
+ const knowledgeDispatch = (current
745
+ && project
746
+ && effectiveHost === "codex"
747
+ && !current.plan.parentPlan
748
+ && effectiveWriter?.executionPolicy === "subagent"
749
+ && result.knowledgeFinalizeId)
750
+ ? buildKnowledgeDispatch({
751
+ finalizeId: result.knowledgeFinalizeId,
752
+ writer: effectiveWriter,
753
+ })
754
+ : undefined;
755
+ printJson(compactPlanCommandResult("plan.edit", result, effectiveHost, completionRefresh, false, knowledgeDispatch));
567
756
  if (result.operationChain?.status === "partial")
568
757
  process.exitCode = 1;
569
758
  return;
@@ -631,6 +820,20 @@ async function runPlan(args, effectiveHost) {
631
820
  const target = readPlanMutationTarget(args);
632
821
  assertNoRemainingArgs(args, "plan done");
633
822
  const ownerSessionKey = resolveOwnerSessionKey() ?? undefined;
823
+ const current = showPlan({
824
+ cwd: process.cwd(),
825
+ ...target,
826
+ ownerSessionKey,
827
+ });
828
+ const project = tryResolveHookProject(process.cwd());
829
+ const effectiveWriter = project
830
+ ? resolvePlanEffectiveConfig(project.projectConfig, current.plan)?.knowledgeWriter
831
+ : undefined;
832
+ if (!current.plan.parentPlan
833
+ && 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 });
836
+ }
634
837
  const queuePlanEndFinalization = preparePlanEndFinalization(process.cwd(), ownerSessionKey);
635
838
  const result = await editPlan({
636
839
  cwd: process.cwd(),
@@ -641,7 +844,16 @@ async function runPlan(args, effectiveHost) {
641
844
  ownerSessionKey,
642
845
  });
643
846
  const completionRefresh = queuePlanEndFinalization?.(result.taskName);
644
- printJson(compactPlanCommandResult("plan.done", result, effectiveHost, completionRefresh));
847
+ const knowledgeDispatch = (effectiveHost === "codex"
848
+ && !current.plan.parentPlan
849
+ && effectiveWriter?.executionPolicy === "subagent"
850
+ && result.knowledgeFinalizeId)
851
+ ? buildKnowledgeDispatch({
852
+ finalizeId: result.knowledgeFinalizeId,
853
+ writer: effectiveWriter,
854
+ })
855
+ : undefined;
856
+ printJson(compactPlanCommandResult("plan.done", result, effectiveHost, completionRefresh, false, knowledgeDispatch));
645
857
  return;
646
858
  }
647
859
  case "show": {
@@ -872,7 +1084,15 @@ async function runContextCommand(args, cwd = process.cwd(), ownerSessionKey = re
872
1084
  let fixedPaths = [];
873
1085
  const sessionProject = resolveSessionWorkflowContext(ownerSessionKey ?? undefined);
874
1086
  if (sessionProject) {
875
- const maintenance = runDailyMaintenance(sessionProject, { excludeSessionKey: ownerSessionKey ?? undefined, includeProject: false });
1087
+ const maintenance = effectiveHost
1088
+ ? null
1089
+ : runDailyMaintenance(sessionProject, {
1090
+ excludeSessionKey: ownerSessionKey ?? undefined,
1091
+ includeProject: false,
1092
+ });
1093
+ if (effectiveHost) {
1094
+ launchDailyMaintenance(cwd, ownerSessionKey, true);
1095
+ }
876
1096
  const activeWorkflow = !taskName && ownerSessionKey
877
1097
  ? await tryResolveActiveWorkflowSnapshot(cwd, ownerSessionKey, effectiveHost)
878
1098
  : null;
@@ -883,7 +1103,7 @@ async function runContextCommand(args, cwd = process.cwd(), ownerSessionKey = re
883
1103
  return {
884
1104
  project: sessionProject,
885
1105
  ...(activeWorkflow ? { activeWorkflow } : {}),
886
- ...(maintenance.ran ? { maintenance } : {}),
1106
+ ...(maintenance?.ran ? { maintenance } : {}),
887
1107
  ...(codexRuntimeError ? { error: codexRuntimeError } : {}),
888
1108
  };
889
1109
  }
@@ -899,7 +1119,13 @@ async function runContextCommand(args, cwd = process.cwd(), ownerSessionKey = re
899
1119
  initProject({ cwd, version: CLI_VERSION });
900
1120
  initialized = true;
901
1121
  }
902
- const maintenance = runDailyMaintenance(resolveProjectContext(cwd), { excludeSessionKey: ownerSessionKey ?? undefined });
1122
+ const project = resolveProjectContext(cwd);
1123
+ const maintenance = effectiveHost
1124
+ ? null
1125
+ : runDailyMaintenance(project, { excludeSessionKey: ownerSessionKey ?? undefined });
1126
+ if (effectiveHost) {
1127
+ launchDailyMaintenance(cwd, ownerSessionKey, false);
1128
+ }
903
1129
  let resolved = resolveContext(cwd, taskName);
904
1130
  const versionSync = syncProjectVersionWithCli(cwd, resolved.project);
905
1131
  if (versionSync.projectVersionUpdated) {
@@ -919,7 +1145,7 @@ async function runContextCommand(args, cwd = process.cwd(), ownerSessionKey = re
919
1145
  launchProjectEmbeddingWarmup(resolved.project);
920
1146
  return {
921
1147
  ...resolved,
922
- ...(maintenance.ran ? { maintenance } : {}),
1148
+ ...(maintenance?.ran ? { maintenance } : {}),
923
1149
  ...(activeWorkflow ? { activeWorkflow } : {}),
924
1150
  ...(codexRuntimeError ? { error: codexRuntimeError } : {}),
925
1151
  protocolCheck: checkProjectProtocol(cwd),
@@ -967,6 +1193,28 @@ function launchProjectEmbeddingWarmup(project) {
967
1193
  // Context recovery is authoritative; embedding warmup is fail-open latency work.
968
1194
  }
969
1195
  }
1196
+ function launchDailyMaintenance(cwd, ownerSessionKey, sessionOnly) {
1197
+ const args = [resolveCliEntryPath(), "internal-daily-maintenance", "--cwd", cwd];
1198
+ if (ownerSessionKey) {
1199
+ args.push("--session-key", ownerSessionKey);
1200
+ }
1201
+ if (sessionOnly) {
1202
+ args.push("--session-only");
1203
+ }
1204
+ try {
1205
+ const child = spawn(process.execPath, args, {
1206
+ cwd,
1207
+ detached: true,
1208
+ stdio: "ignore",
1209
+ windowsHide: true,
1210
+ env: withoutInvocationHost(),
1211
+ });
1212
+ child.unref();
1213
+ }
1214
+ catch {
1215
+ // Context recovery is authoritative; daily cleanup is fail-open latency work.
1216
+ }
1217
+ }
970
1218
  function buildPublicContextOutput(context) {
971
1219
  const project = asJsonRecord(context.project);
972
1220
  const output = {};
@@ -1198,6 +1446,11 @@ async function runHook(args, effectiveHost) {
1198
1446
  });
1199
1447
  }
1200
1448
  async function runStopHook(effectiveHost) {
1449
+ // Cindy owns task-completion closeout in its Host worker path. The CLI sidecar
1450
+ // must not enqueue a job that it cannot execute with a Cindy session runner.
1451
+ if (effectiveHost === "cindy") {
1452
+ return;
1453
+ }
1201
1454
  if (process.env.CLAW_KNOWLEDGE_FINALIZER === "1") {
1202
1455
  return;
1203
1456
  }
@@ -1226,14 +1479,166 @@ async function runStopHook(effectiveHost) {
1226
1479
  host: effectiveHost,
1227
1480
  taskConclusions: transcriptPath ? extractTaskDoneConclusions(transcriptPath, turnId) : [],
1228
1481
  });
1229
- if (result.ok && result.jobPath && process.env.CLAW_KNOWLEDGE_FINALIZER_DISABLE_LAUNCH !== "1") {
1482
+ // Current named hosts own their runner in their adapters. Keep the CLI
1483
+ // launcher only for jobs written by pre-adapter releases with no host.
1484
+ if (result.ok && result.jobPath && !effectiveHost && process.env.CLAW_KNOWLEDGE_FINALIZER_DISABLE_LAUNCH !== "1") {
1230
1485
  launchKnowledgeFinalizationWorker(result.jobPath, project.projectRoot);
1231
1486
  }
1487
+ if (process.env.CLAW_KNOWLEDGE_CAPTURE_RESULT === "1") {
1488
+ printJson(result);
1489
+ }
1232
1490
  }
1233
1491
  catch {
1234
1492
  // Knowledge capture is a fail-open sidecar and must never block Stop.
1235
1493
  }
1236
1494
  }
1495
+ /**
1496
+ * Machine-facing report capture for hosts whose final-message hook is owned by
1497
+ * the adapter rather than by the CLI. Unlike `hook auto-doc`, this only
1498
+ * persists the report/job hand-off; it never chooses or launches a writer.
1499
+ */
1500
+ async function runInternalKnowledgeCapture(args, effectiveHost) {
1501
+ assertNoRemainingArgs(args, "internal-knowledge-capture");
1502
+ const payload = await readStdinJson();
1503
+ const hookCwd = resolveHookCwd(payload);
1504
+ const sessionId = resolveOwnerSessionKey(payload);
1505
+ const turnId = readHookString(payload, "turn_id");
1506
+ const message = readHookString(payload, "message");
1507
+ const taskConclusions = readHookTaskConclusions(payload, turnId);
1508
+ if (!hookCwd || !sessionId || !turnId || !message || !containsClawDir(hookCwd)) {
1509
+ printJson({ ok: true, captured: false });
1510
+ return;
1511
+ }
1512
+ try {
1513
+ const project = resolveProjectContext(hookCwd);
1514
+ const result = tryCaptureKnowledgeStop({
1515
+ project,
1516
+ sessionId,
1517
+ turnId,
1518
+ message,
1519
+ host: effectiveHost === "cindy" ? "cindy" : effectiveHost,
1520
+ taskConclusions,
1521
+ });
1522
+ printJson(result);
1523
+ }
1524
+ catch (error) {
1525
+ // Capture is a non-blocking sidecar. Return structured failure so the
1526
+ // owning adapter can surface/retry it without affecting the assistant turn.
1527
+ printJson({ ok: false, error: error instanceof Error ? error.message : String(error) });
1528
+ }
1529
+ }
1530
+ /** Record a successful adapter-owned writer run without invoking a host SDK. */
1531
+ function runInternalKnowledgeComplete(args) {
1532
+ const jobPath = readRequiredFlag(args, "--job");
1533
+ const result = readRequiredFlag(args, "--result");
1534
+ assertNoRemainingArgs(args, "internal-knowledge-complete");
1535
+ const running = ensureLegacyKnowledgeClaim(jobPath);
1536
+ if (!running?.claimToken) {
1537
+ printJson({ ok: true, completed: false, reason: "job is not claimable" });
1538
+ return;
1539
+ }
1540
+ completeKnowledgeFinalizationJob(jobPath, result, running.claimToken);
1541
+ }
1542
+ function runInternalKnowledgeClaim(args) {
1543
+ const jobPath = readRequiredFlag(args, "--job");
1544
+ assertNoRemainingArgs(args, "internal-knowledge-claim");
1545
+ const job = ensureLegacyKnowledgeClaim(jobPath);
1546
+ printJson({
1547
+ ok: true,
1548
+ claimed: Boolean(job),
1549
+ ...(job ? {
1550
+ finalizeId: job.finalizeId,
1551
+ claimToken: job.claimToken,
1552
+ } : {}),
1553
+ });
1554
+ }
1555
+ function runInternalKnowledgeFail(args) {
1556
+ const jobPath = readRequiredFlag(args, "--job");
1557
+ const message = readRequiredFlag(args, "--message");
1558
+ assertNoRemainingArgs(args, "internal-knowledge-fail");
1559
+ const job = ensureLegacyKnowledgeClaim(jobPath);
1560
+ if (!job?.claimToken) {
1561
+ printJson({ ok: true, failed: false, reason: "job is not claimable" });
1562
+ return;
1563
+ }
1564
+ failKnowledgeFinalizationJob(jobPath, message, job.claimToken);
1565
+ }
1566
+ function ensureLegacyKnowledgeClaim(jobPath) {
1567
+ const job = readKnowledgeFinalizationJob(jobPath);
1568
+ if (job.status === "succeeded" || job.status === "failed" && job.attempts >= 3) {
1569
+ return null;
1570
+ }
1571
+ if (job.status === "running") {
1572
+ return job;
1573
+ }
1574
+ return claimKnowledgeFinalizationJob(jobPath);
1575
+ }
1576
+ function completeKnowledgeFinalizationJob(jobPath, result, claimToken) {
1577
+ const running = readKnowledgeFinalizationJob(jobPath);
1578
+ if (running.status === "succeeded") {
1579
+ const terminal = doneKnowledgeFinalizationJob({
1580
+ jobPath,
1581
+ claimToken,
1582
+ status: "succeeded",
1583
+ result,
1584
+ });
1585
+ removeKnowledgeAssignmentTemplate(jobPath, running.finalizeId);
1586
+ printJson({ ok: true, completed: true, alreadyDone: terminal.alreadyDone, finalizeId: running.finalizeId });
1587
+ return;
1588
+ }
1589
+ if (running.status !== "running") {
1590
+ throw new Error("Knowledge finalization job must be claimed before successful completion.");
1591
+ }
1592
+ if (running.claimToken !== claimToken) {
1593
+ throw new Error("Knowledge finalization completion does not match the active claim.");
1594
+ }
1595
+ const project = resolveProjectContext(running.projectRoot);
1596
+ const finishedAt = new Date().toISOString();
1597
+ const truthEncoding = normalizeTruthMarkdownEncoding(project);
1598
+ recordKnowledgeFinalizationResult(project, running.reportPath, {
1599
+ schemaVersion: 1,
1600
+ entryType: "knowledge_finalization",
1601
+ finalizeId: running.finalizeId,
1602
+ taskName: running.taskName,
1603
+ recordedAt: finishedAt,
1604
+ status: "succeeded",
1605
+ result,
1606
+ attempts: running.attempts,
1607
+ ...(running.host !== undefined ? { host: running.host } : {}),
1608
+ truthEncoding,
1609
+ });
1610
+ const terminal = doneKnowledgeFinalizationJob({
1611
+ jobPath,
1612
+ claimToken,
1613
+ status: "succeeded",
1614
+ result,
1615
+ finishedAt,
1616
+ patch: { truthEncoding },
1617
+ });
1618
+ removeKnowledgeAssignmentTemplate(jobPath, running.finalizeId);
1619
+ queueCompletionRefresh({
1620
+ cwd: running.projectRoot,
1621
+ taskName: running.taskName,
1622
+ includeTaskRetention: false,
1623
+ includeTaskMemory: false,
1624
+ statusLabel: `knowledge-${running.finalizeId.slice(0, 12)}`,
1625
+ skipGitNexusRefresh: true,
1626
+ });
1627
+ printJson({ ok: true, completed: true, alreadyDone: terminal.alreadyDone, finalizeId: running.finalizeId });
1628
+ }
1629
+ function failKnowledgeFinalizationJob(jobPath, message, claimToken) {
1630
+ const terminal = doneKnowledgeFinalizationJob({
1631
+ jobPath,
1632
+ claimToken,
1633
+ status: "failed",
1634
+ error: message,
1635
+ });
1636
+ removeKnowledgeAssignmentTemplate(jobPath, terminal.job.finalizeId);
1637
+ printJson({ ok: true, failed: true, alreadyDone: terminal.alreadyDone, finalizeId: terminal.job.finalizeId });
1638
+ }
1639
+ function removeKnowledgeAssignmentTemplate(jobPath, finalizeId) {
1640
+ fs.rmSync(path.join(path.dirname(jobPath), `${finalizeId}.assignments.json`), { force: true });
1641
+ }
1237
1642
  async function runInternalEmbeddingWarmup(args) {
1238
1643
  const cwd = readRequiredFlag(args, "--cwd");
1239
1644
  assertNoRemainingArgs(args, "internal-embedding-warmup");
@@ -1243,112 +1648,156 @@ async function runInternalEmbeddingWarmup(args) {
1243
1648
  ...await warmProjectMemoryEmbedding({ cwd }),
1244
1649
  });
1245
1650
  }
1651
+ function runInternalDailyMaintenance(args) {
1652
+ const cwd = readRequiredFlag(args, "--cwd");
1653
+ const sessionKey = readOptionalFlag(args, "--session-key");
1654
+ const sessionOnly = readBooleanFlag(args, "--session-only");
1655
+ assertNoRemainingArgs(args, "internal-daily-maintenance");
1656
+ const project = sessionOnly
1657
+ ? (sessionKey ? resolveSessionWorkflowContext(sessionKey) : null)
1658
+ : resolveProjectContext(cwd);
1659
+ if (!project) {
1660
+ printJson({ command: "internal-daily-maintenance", ok: true, ran: false, skipped: true });
1661
+ return;
1662
+ }
1663
+ printJson({
1664
+ command: "internal-daily-maintenance",
1665
+ ok: true,
1666
+ ...runDailyMaintenance(project, {
1667
+ excludeSessionKey: sessionKey ?? undefined,
1668
+ includeProject: !sessionOnly,
1669
+ }),
1670
+ });
1671
+ }
1672
+ function runInternalKnowledgeSweep(args) {
1673
+ const cwd = readRequiredFlag(args, "--cwd");
1674
+ assertNoRemainingArgs(args, "internal-knowledge-sweep");
1675
+ const project = tryResolveHookProject(cwd);
1676
+ if (!project) {
1677
+ printJson({ command: "internal-knowledge-sweep", ok: true, launched: 0, skipped: true });
1678
+ return;
1679
+ }
1680
+ const jobs = listRetryableKnowledgeFinalizationJobs(project);
1681
+ let launched = 0;
1682
+ for (const jobPath of jobs) {
1683
+ try {
1684
+ launchKnowledgeFinalizationWorker(jobPath, project.projectRoot);
1685
+ launched += 1;
1686
+ }
1687
+ catch {
1688
+ // A later background sweep can retry a job that could not be launched.
1689
+ }
1690
+ }
1691
+ printJson({ command: "internal-knowledge-sweep", ok: true, discovered: jobs.length, launched });
1692
+ }
1693
+ async function runInternalBackgroundMaintenance(args) {
1694
+ const cwd = readRequiredFlag(args, "--cwd");
1695
+ const sessionKey = readOptionalFlag(args, "--session-key");
1696
+ assertNoRemainingArgs(args, "internal-background-maintenance");
1697
+ const sessionProject = sessionKey ? resolveSessionWorkflowContext(sessionKey) : null;
1698
+ const project = sessionProject ? null : tryResolveHookProject(cwd);
1699
+ let maintenance = { ran: false, skipped: true };
1700
+ if (sessionProject) {
1701
+ maintenance = runDailyMaintenance(sessionProject, { excludeSessionKey: sessionKey ?? undefined, includeProject: false });
1702
+ }
1703
+ else if (project) {
1704
+ maintenance = runDailyMaintenance(project, { excludeSessionKey: sessionKey ?? undefined });
1705
+ }
1706
+ let embedding = { warmed: false, reason: "skipped" };
1707
+ try {
1708
+ embedding = await warmProjectMemoryEmbedding({ cwd });
1709
+ }
1710
+ catch (error) {
1711
+ embedding = { warmed: false, reason: "failed", error: error instanceof Error ? error.message : String(error) };
1712
+ }
1713
+ let discovered = 0;
1714
+ let launched = 0;
1715
+ const knowledgeProject = project ?? sessionProject;
1716
+ if (knowledgeProject) {
1717
+ const jobs = listRetryableKnowledgeFinalizationJobs(knowledgeProject);
1718
+ discovered = jobs.length;
1719
+ for (const jobPath of jobs) {
1720
+ try {
1721
+ launchKnowledgeFinalizationWorker(jobPath, knowledgeProject.projectRoot);
1722
+ launched += 1;
1723
+ }
1724
+ catch {
1725
+ // A later background sweep can retry a job that could not be launched.
1726
+ }
1727
+ }
1728
+ }
1729
+ printJson({ command: "internal-background-maintenance", ok: true, maintenance, embedding, discovered, launched });
1730
+ }
1246
1731
  async function runInternalKnowledgeFinalize(args) {
1247
1732
  const jobPath = readRequiredFlag(args, "--job");
1248
- const running = claimKnowledgeFinalizationJob(jobPath);
1249
- if (!running) {
1733
+ const queued = readKnowledgeFinalizationJob(jobPath);
1734
+ assertNoRemainingArgs(args, "internal-knowledge-finalize");
1735
+ if ((queued.writer?.executionPolicy ?? "background") !== "background") {
1250
1736
  return;
1251
1737
  }
1252
1738
  try {
1253
- const project = resolveProjectContext(running.projectRoot);
1254
- const useBuiltInAutomation = usesBuiltInKnowledgeWriter(running);
1255
- const knowledgeBefore = snapshotKnowledgeMarkdown(project.truthDir);
1256
- const writerRun = await runKnowledgeWriterForJob(running);
1257
- const knowledgeGovernance = useBuiltInAutomation
1258
- ? governChangedKnowledgeMarkdown({
1259
- truthDir: project.truthDir,
1260
- before: knowledgeBefore,
1261
- datedSectionsToKeep: running.writer?.datedSectionsToKeep ?? 6,
1262
- })
1263
- : undefined;
1264
- const truthEncoding = normalizeTruthMarkdownEncoding(project);
1265
- const changedKnowledgePaths = changedKnowledgeMarkdownPaths(knowledgeBefore, snapshotKnowledgeMarkdown(project.truthDir));
1266
- queueCompletionRefresh({
1267
- cwd: running.projectRoot,
1268
- taskName: running.taskName,
1269
- includeTaskRetention: false,
1270
- includeTaskMemory: false,
1271
- statusLabel: `knowledge-${running.finalizeId.slice(0, 12)}`,
1272
- skipGitNexusRefresh: true,
1273
- });
1274
- const finishedAt = new Date().toISOString();
1275
- recordKnowledgeFinalizationResult(project, running.reportPath, {
1276
- schemaVersion: 1,
1277
- entryType: "knowledge_finalization",
1278
- finalizeId: running.finalizeId,
1279
- taskName: running.taskName,
1280
- recordedAt: finishedAt,
1281
- status: "succeeded",
1282
- result: writerRun.finalResponse,
1283
- attempts: running.attempts,
1284
- ...(running.host !== undefined ? { host: running.host } : {}),
1739
+ const writerRun = await runKnowledgeDelegateForJob(queued);
1740
+ const terminal = readKnowledgeFinalizationJob(jobPath);
1741
+ if (terminal.status !== "succeeded" && terminal.status !== "failed") {
1742
+ throw new Error("Knowledge delegate returned without acknowledging a terminal result.");
1743
+ }
1744
+ if (writerRun.threadId) {
1745
+ assertCompletedKnowledgeWriterSession(writerRun.threadId);
1746
+ }
1747
+ printJson({
1748
+ ok: terminal.status === "succeeded",
1749
+ command: "internal-knowledge-finalize",
1750
+ finalizeId: terminal.finalizeId,
1751
+ status: terminal.status,
1285
1752
  ...(writerRun.threadId ? { threadId: writerRun.threadId } : {}),
1286
- ...(knowledgeGovernance ? { knowledgeGovernance } : {}),
1287
- truthEncoding,
1288
- });
1289
- writeKnowledgeFinalizationJob(jobPath, {
1290
- ...running,
1291
- status: "succeeded",
1292
- finishedAt,
1293
- ...(writerRun.threadId ? { sdkThreadId: writerRun.threadId } : {}),
1294
- ...(writerRun.threadIds ? { sdkThreadIds: writerRun.threadIds } : {}),
1295
- finalResponse: writerRun.finalResponse,
1296
- ...(knowledgeGovernance ? { knowledgeGovernance } : {}),
1297
- truthEncoding,
1298
1753
  });
1299
1754
  }
1300
1755
  catch (error) {
1301
- const failed = {
1302
- ...running,
1303
- status: "failed",
1304
- finishedAt: new Date().toISOString(),
1305
- error: { message: error instanceof Error ? error.message : String(error) },
1306
- };
1307
- writeKnowledgeFinalizationJob(jobPath, failed);
1756
+ const message = error instanceof Error ? error.message : String(error);
1757
+ let failed = readKnowledgeFinalizationJob(jobPath);
1758
+ if (failed.status === "running" && failed.claimToken) {
1759
+ failed = doneKnowledgeFinalizationJob({
1760
+ jobPath,
1761
+ claimToken: failed.claimToken,
1762
+ status: "failed",
1763
+ error: message,
1764
+ }).job;
1765
+ }
1766
+ else if (failed.status === "queued") {
1767
+ const supervisorClaim = claimKnowledgeFinalizationJob(jobPath);
1768
+ if (supervisorClaim?.claimToken) {
1769
+ failed = doneKnowledgeFinalizationJob({
1770
+ jobPath,
1771
+ claimToken: supervisorClaim.claimToken,
1772
+ status: "failed",
1773
+ error: message,
1774
+ }).job;
1775
+ }
1776
+ }
1777
+ if (failed.status === "failed") {
1778
+ removeKnowledgeAssignmentTemplate(jobPath, failed.finalizeId);
1779
+ }
1308
1780
  if (failed.attempts < 3 && process.env.CLAW_KNOWLEDGE_FINALIZER_DISABLE_RETRY !== "1") {
1309
1781
  launchKnowledgeFinalizationWorker(jobPath, failed.projectRoot);
1310
1782
  }
1311
1783
  }
1312
1784
  }
1313
- const BUILT_IN_KNOWLEDGE_WRITER_SKILL = "claw-kit:knowledge-writer";
1314
- /**
1315
- * Pick the host-aware finalization runner. The opencode host never assumes a Codex SDK
1316
- * runtime is installed and runs the writer through `opencode run`; the Codex host and
1317
- * legacy jobs without a host field keep using the versioned Codex SDK runtime.
1318
- */
1319
- async function runKnowledgeWriterForJob(running) {
1320
- const skills = resolveKnowledgeWriterSkills(running);
1321
- const results = [];
1322
- for (const skill of skills) {
1323
- if (running.host === "opencode") {
1324
- const result = runOpencodeKnowledgeWriter({
1325
- prompt: buildKnowledgeWriterPrompt(running, skill),
1326
- projectRoot: running.projectRoot,
1327
- writer: running.writer ?? null,
1328
- });
1329
- if (isBuiltInKnowledgeWriterSkill(skill)) {
1330
- assertCompletedKnowledgeWriterSession(result.threadId ?? null);
1331
- }
1332
- results.push({
1333
- finalResponse: result.finalResponse,
1334
- ...(result.threadId ? { threadId: result.threadId } : {}),
1335
- });
1336
- continue;
1337
- }
1338
- if (running.host !== undefined && running.host !== null && running.host !== "codex") {
1339
- throw new Error(`Unsupported knowledge finalization job host "${String(running.host)}".`);
1340
- }
1341
- results.push(await runCodexSdkWriter(running, skill));
1785
+ async function runKnowledgeDelegateForJob(running) {
1786
+ const dispatch = buildKnowledgeDelegateDispatch({
1787
+ policy: "background",
1788
+ finalizeId: running.finalizeId,
1789
+ writer: running.writer,
1790
+ });
1791
+ if (running.host === "opencode") {
1792
+ return runOpencodeKnowledgeWriter({
1793
+ prompt: dispatch.prompt,
1794
+ projectRoot: running.projectRoot,
1795
+ writer: running.writer ?? null,
1796
+ });
1797
+ }
1798
+ if (running.host !== undefined && running.host !== null && running.host !== "codex") {
1799
+ throw new Error(`Unsupported knowledge finalization job host "${String(running.host)}".`);
1342
1800
  }
1343
- const threadIds = results.flatMap((result) => result.threadId ? [result.threadId] : []);
1344
- const last = results.at(-1);
1345
- return {
1346
- finalResponse: results.map((result) => result.finalResponse).join("\n\n"),
1347
- ...(last?.threadId ? { threadId: last.threadId } : {}),
1348
- ...(threadIds.length > 0 ? { threadIds } : {}),
1349
- };
1350
- }
1351
- async function runCodexSdkWriter(running, skill) {
1352
1801
  const sdk = await import(pathToFileURL(resolveCodexSdkEntryPath()).href);
1353
1802
  const Codex = sdk.Codex;
1354
1803
  const codex = new Codex({
@@ -1368,10 +1817,7 @@ async function runCodexSdkWriter(running, skill) {
1368
1817
  ? { modelReasoningEffort: writer.reasoningEffort }
1369
1818
  : {}),
1370
1819
  });
1371
- const turn = await thread.run(buildKnowledgeWriterPrompt(running, skill));
1372
- if (isBuiltInKnowledgeWriterSkill(skill)) {
1373
- assertCompletedKnowledgeWriterSession(thread.id);
1374
- }
1820
+ const turn = await thread.run(dispatch.prompt);
1375
1821
  return {
1376
1822
  finalResponse: turn.finalResponse,
1377
1823
  ...(thread.id ? { threadId: thread.id } : {}),
@@ -1416,28 +1862,11 @@ function knowledgeFinalizerEnvironment() {
1416
1862
  }
1417
1863
  }
1418
1864
  env.CLAW_KNOWLEDGE_FINALIZER = "1";
1865
+ delete env.CLAW_SESSION_ID;
1866
+ delete env.CODEX_THREAD_ID;
1867
+ delete env.CODEX_SESSION_ID;
1419
1868
  return env;
1420
1869
  }
1421
- function buildKnowledgeWriterPrompt(job, writerSkill) {
1422
- return [
1423
- `Apply the ${writerSkill} skill's documentation-governance rules to the supplied materials and update the governed project documentation. Work unattended; do not request review or confirmation. Use task status to distinguish completed work from pending or blocked intent, and never present requirements or intentions as results. Skip and report ambiguous or unsafe changes.`,
1424
- "Materials:",
1425
- `- ${job.planPath}`,
1426
- `- ${job.reportPath}`,
1427
- `Finalization id: ${job.finalizeId}`,
1428
- "Interpret inputs by content, regardless of filename or schema. Do not reference or link to the supplied materials in governed documentation; they are transient and will be destroyed after finalization. Do not modify inputs, delegate, reimplement, or rerun tests.",
1429
- ].join("\n");
1430
- }
1431
- function resolveKnowledgeWriterSkills(job) {
1432
- const configured = job.writer?.externalSkills?.map((skill) => skill.trim()).filter(Boolean);
1433
- return configured && configured.length > 0 ? configured : [BUILT_IN_KNOWLEDGE_WRITER_SKILL];
1434
- }
1435
- function isBuiltInKnowledgeWriterSkill(skill) {
1436
- return skill === BUILT_IN_KNOWLEDGE_WRITER_SKILL;
1437
- }
1438
- function usesBuiltInKnowledgeWriter(job) {
1439
- return resolveKnowledgeWriterSkills(job).every(isBuiltInKnowledgeWriterSkill);
1440
- }
1441
1870
  function launchKnowledgeFinalizationWorker(jobPath, cwd) {
1442
1871
  if (process.platform === "win32") {
1443
1872
  const launcherScript = [
@@ -1484,11 +1913,15 @@ async function runSessionStartHook(effectiveHost) {
1484
1913
  try {
1485
1914
  const context = await runContextCommand([], hookCwd, ownerSessionKey, effectiveHost);
1486
1915
  const contextProject = asJsonRecord(context.project);
1916
+ const retryableJobs = effectiveHost !== "cindy" && contextProject?.scope !== "session" && !context.error
1917
+ ? listRetryableKnowledgeFinalizationJobs(resolveProjectContext(hookCwd))
1918
+ : [];
1487
1919
  if (contextProject?.scope !== "session"
1488
1920
  && !context.error
1921
+ && effectiveHost !== "cindy"
1489
1922
  && process.env.CLAW_KNOWLEDGE_FINALIZER_DISABLE_LAUNCH !== "1") {
1490
1923
  const project = resolveProjectContext(hookCwd);
1491
- for (const jobPath of listRetryableKnowledgeFinalizationJobs(project)) {
1924
+ for (const jobPath of retryableJobs) {
1492
1925
  try {
1493
1926
  launchKnowledgeFinalizationWorker(jobPath, project.projectRoot);
1494
1927
  }
@@ -1538,6 +1971,21 @@ function readHookString(payload, key) {
1538
1971
  const value = payload[key];
1539
1972
  return typeof value === "string" && value.trim() ? value.trim() : null;
1540
1973
  }
1974
+ function readHookTaskConclusions(payload, turnId) {
1975
+ if (!turnId || !payload || typeof payload !== "object")
1976
+ return [];
1977
+ const value = payload.task_conclusions;
1978
+ if (!Array.isArray(value))
1979
+ return [];
1980
+ return value.flatMap((item) => {
1981
+ if (!item || typeof item !== "object")
1982
+ return [];
1983
+ const message = item.message;
1984
+ return typeof message === "string" && message.trim()
1985
+ ? [{ turnId, message: message.trim() }]
1986
+ : [];
1987
+ });
1988
+ }
1541
1989
  function containsClawDir(cwd) {
1542
1990
  try {
1543
1991
  const startDir = path.resolve(cwd);
@@ -1579,6 +2027,9 @@ function safeResolveTempDir() {
1579
2027
  }
1580
2028
  function buildSessionStartAdditionalContext(context, sessionCwd, effectiveHost) {
1581
2029
  const versionSyncPrompt = buildVersionSyncPrompt(context);
2030
+ if (effectiveHost === "cindy") {
2031
+ return buildCindySessionStartContext(context, sessionCwd, versionSyncPrompt);
2032
+ }
1582
2033
  const searchGuidance = buildContextSearchGuidance(context);
1583
2034
  const runtimeErrorPrompt = buildCodexRuntimeErrorPrompt(context);
1584
2035
  const activeWorkflow = context.activeWorkflow;
@@ -1613,6 +2064,45 @@ function buildSessionStartAdditionalContext(context, sessionCwd, effectiveHost)
1613
2064
  const promptWithSearch = searchGuidance ? `${promptWithVersion}\n${searchGuidance}` : promptWithVersion;
1614
2065
  return runtimeErrorPrompt ? `${runtimeErrorPrompt}\n\n${promptWithSearch}` : promptWithSearch;
1615
2066
  }
2067
+ /**
2068
+ * Cindy Agents use the Ghost Tool gateway, not shell commands or Host actions.
2069
+ * Match the Codex session-start wording and structure. Cindy's static entry
2070
+ * prompt supplies the shared skill and guidance lines; this dynamic context
2071
+ * supplies project identity or a recovered workflow snapshot. Goal Mode is
2072
+ * intentionally omitted because Cindy does not expose that Host surface.
2073
+ */
2074
+ function buildCindySessionStartContext(context, sessionCwd, versionSyncPrompt) {
2075
+ const activeWorkflow = asJsonRecord(context.activeWorkflow);
2076
+ if (activeWorkflow) {
2077
+ return stripCindyGoalModeLines(buildRecoveredWorkflowAdditionalContext(activeWorkflow, versionSyncPrompt));
2078
+ }
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")}`;
2099
+ }
2100
+ function stripCindyGoalModeLines(prompt) {
2101
+ return prompt
2102
+ .split("\n")
2103
+ .filter((line) => !/goal mode/i.test(line))
2104
+ .join("\n");
2105
+ }
1616
2106
  function buildCodexRuntimeErrorPrompt(context) {
1617
2107
  const error = asJsonRecord(context.error);
1618
2108
  if (error?.code !== "CODEX_SDK_RUNTIME_MISSING") {
@@ -1674,8 +2164,9 @@ function buildVersionSyncPrompt(context) {
1674
2164
  return {
1675
2165
  placement: "suffix",
1676
2166
  lines: [
1677
- `Before anything else, a newer claw-kit version was detected: local CLI ${cliVersion}, published latest ${latestPublishedVersion}.`,
1678
- `First action: use ${updateSkill} to update the claw-kit CLI and the current host plugin surface before continuing any other work.`,
2167
+ `A newer claw-kit version is available: installed CLI ${cliVersion}, published latest ${latestPublishedVersion}.`,
2168
+ "Tell the user in their language that the current claw-kit installation is out of date and must be updated before they can continue using claw-kit. Ask whether they want to update now, then wait for their answer.",
2169
+ `After the user confirms, use ${updateSkill} to update the claw-kit CLI and the current host plugin surface, then continue the original task.`,
1679
2170
  ],
1680
2171
  };
1681
2172
  }
@@ -1820,6 +2311,7 @@ async function runSubplan(args, effectiveHost) {
1820
2311
  }
1821
2312
  function resolveOwnerSessionKey(payload) {
1822
2313
  const envCandidates = [
2314
+ process.env.CLAW_SESSION_ID,
1823
2315
  process.env.CODEX_THREAD_ID,
1824
2316
  process.env.CODEX_SESSION_ID,
1825
2317
  ];
@@ -1880,13 +2372,20 @@ function readJson(filePath) {
1880
2372
  function stripBom(content) {
1881
2373
  return content.charCodeAt(0) === 0xfeff ? content.slice(1) : content;
1882
2374
  }
1883
- function compactPlanCommandResult(command, result, effectiveHost, completionRefresh, forceProjectionSync = false) {
2375
+ function buildKnowledgeDispatch(input) {
2376
+ return buildKnowledgeDelegateDispatch({
2377
+ policy: "subagent",
2378
+ ...input,
2379
+ });
2380
+ }
2381
+ function compactPlanCommandResult(command, result, effectiveHost, completionRefresh, forceProjectionSync = false, knowledgeDispatch) {
1884
2382
  const archivedPlanPath = completionRefresh?.taskRetention.archivedCurrentTask?.taskName === result.taskName &&
1885
2383
  completionRefresh.taskRetention.archivedCurrentTask.archivedPlanPath
1886
2384
  ? completionRefresh.taskRetention.archivedCurrentTask.archivedPlanPath
1887
2385
  : undefined;
1888
2386
  const resolvedPlanPath = archivedPlanPath ?? result.planPath;
1889
2387
  const codexResult = effectiveHost === "codex";
2388
+ const cindyResult = effectiveHost === "cindy";
1890
2389
  const hostActions = codexResult ? buildHostActions(result, { forceProjectionSync, actionIdPrefix: command === "plan.sync" ? `plan.sync:${createHash("sha256").update(result.planPath).digest("hex").slice(0, 16)}` : undefined }) : [];
1891
2390
  const nextsteps = codexResult
1892
2391
  && result.planStatus === "end.completed"
@@ -1917,16 +2416,17 @@ function compactPlanCommandResult(command, result, effectiveHost, completionRefr
1917
2416
  ...(archivedPlanPath ? { archivedPlanPath } : {}),
1918
2417
  planStatus: result.planStatus,
1919
2418
  ...(achievement ? { achievement } : {}),
2419
+ ...(knowledgeDispatch ? { knowledgeDispatch } : {}),
1920
2420
  ...(!codexResult && result.previousPlanStatus ? { previousPlanStatus: result.previousPlanStatus } : {}),
1921
2421
  ...(hostActions.length ? { hostActions } : {}),
1922
2422
  ...(!codexResult && result.changedTaskIds?.length ? { changedTaskIds: result.changedTaskIds } : {}),
1923
2423
  ...(!codexResult && result.appendedTaskIds?.length ? { appendedTaskIds: result.appendedTaskIds } : {}),
1924
2424
  ...(codexResult ? { stage: result.workflowGuidance.stage } : {}),
1925
- ...(!codexResult || result.planStatus === "end.completed"
2425
+ ...((!codexResult && !cindyResult) || result.planStatus === "end.completed" && !cindyResult
1926
2426
  ? { nextsteps }
1927
2427
  : {}),
1928
2428
  ...(result.workflowGuidance.nextTask ? { nextTask: result.workflowGuidance.nextTask } : {}),
1929
- ...(result.workflowGuidance.notes?.trim() && !codexResult
2429
+ ...(result.workflowGuidance.notes?.trim() && !codexResult && !cindyResult
1930
2430
  ? { notes: result.workflowGuidance.notes }
1931
2431
  : {}),
1932
2432
  ...(result.workflowGuidance.commandHints?.length
@@ -1941,9 +2441,13 @@ function compactPlanCommandResult(command, result, effectiveHost, completionRefr
1941
2441
  failedOperation: result.operationChain.failedOperation,
1942
2442
  }
1943
2443
  : {}),
1944
- ...(!codexResult && result.workflowGuidance.goalMode ? { goalMode: result.workflowGuidance.goalMode } : {}),
1945
- ...(!codexResult && result.workflowGuidance.goalTool ? { goalTool: result.workflowGuidance.goalTool } : {}),
2444
+ ...(!codexResult && !cindyResult && result.workflowGuidance.goalMode ? { goalMode: result.workflowGuidance.goalMode } : {}),
2445
+ ...(!codexResult && !cindyResult && result.workflowGuidance.goalTool ? { goalTool: result.workflowGuidance.goalTool } : {}),
1946
2446
  ...(includePlan && result.plan ? { plan: result.plan } : {}),
2447
+ // Cindy's Ghost card is a Host-owned projection. It needs the
2448
+ // canonical task list to render its expandable Todo view, but that
2449
+ // view stays out of Agent guidance in the Cindy adapter.
2450
+ ...(cindyResult ? { planView: result.planView } : {}),
1947
2451
  ...(result.planReview
1948
2452
  ? {
1949
2453
  planReview: {
@@ -3305,7 +3809,7 @@ function printTopLevelUsage() {
3305
3809
  "Global flags:",
3306
3810
  " -h, --help Show help (use `claw help <command>` for command details).",
3307
3811
  " -v, --version Print the CLI version.",
3308
- " --host <host> Select host-specific output projection (codex or opencode).",
3812
+ " --host <host> Select host-specific output projection (codex, opencode, or cindy).",
3309
3813
  "",
3310
3814
  "Run `claw help <command>` or `claw help <command> <subcommand>` for detailed help.",
3311
3815
  ];