@veewo/claw 0.1.98 → 0.2.2

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.",
@@ -453,6 +491,9 @@ async function main() {
453
491
  case "search":
454
492
  await runSearch(args);
455
493
  return;
494
+ case "knowledge":
495
+ runKnowledge(args);
496
+ return;
456
497
  case "direct":
457
498
  runDirect(args, effectiveHost);
458
499
  return;
@@ -471,6 +512,18 @@ async function main() {
471
512
  case "internal-knowledge-finalize":
472
513
  await runInternalKnowledgeFinalize(args);
473
514
  return;
515
+ case "internal-knowledge-capture":
516
+ await runInternalKnowledgeCapture(args, effectiveHost);
517
+ return;
518
+ case "internal-knowledge-complete":
519
+ runInternalKnowledgeComplete(args);
520
+ return;
521
+ case "internal-knowledge-claim":
522
+ runInternalKnowledgeClaim(args);
523
+ return;
524
+ case "internal-knowledge-fail":
525
+ runInternalKnowledgeFail(args);
526
+ return;
474
527
  case "internal-embedding-warmup":
475
528
  await runInternalEmbeddingWarmup(args);
476
529
  return;
@@ -513,6 +566,87 @@ function runSession(args) {
513
566
  removed: deleteSessionWorkflow(ownerSessionKey),
514
567
  });
515
568
  }
569
+ function runKnowledge(args) {
570
+ const subcommand = args.shift();
571
+ switch (subcommand) {
572
+ case "wait": {
573
+ const projectRoot = path.resolve(readRequiredFlag(args, "--project-root"));
574
+ const finalizeId = readRequiredFlag(args, "--finalize-id");
575
+ const timeoutMs = readOptionalNumber(args, "--timeout-ms") ?? 300_000;
576
+ assertNoRemainingArgs(args, "knowledge wait");
577
+ const { jobPath, job } = waitForKnowledgeFinalizationJobReady({
578
+ project: resolveProjectContext(projectRoot),
579
+ finalizeId,
580
+ timeoutMs,
581
+ });
582
+ printJson({
583
+ ok: true,
584
+ command: "knowledge.wait",
585
+ finalizeId: job.finalizeId,
586
+ status: job.status,
587
+ jobPath,
588
+ });
589
+ return;
590
+ }
591
+ case "claim": {
592
+ const jobPath = readRequiredFlag(args, "--job");
593
+ assertNoRemainingArgs(args, "knowledge claim");
594
+ const job = claimKnowledgeFinalizationJob(jobPath);
595
+ const assignments = job ? buildKnowledgeWriterAssignments(job) : [];
596
+ const templatePath = job
597
+ ? path.join(path.dirname(jobPath), `${job.finalizeId}.assignments.json`)
598
+ : undefined;
599
+ if (job && templatePath) {
600
+ fs.writeFileSync(templatePath, `${JSON.stringify(buildKnowledgeAssignmentTemplate({
601
+ assignments,
602
+ finalizeId: job.finalizeId,
603
+ version: CLI_VERSION,
604
+ }), null, 2)}\n`, "utf-8");
605
+ }
606
+ printJson({
607
+ ok: true,
608
+ command: "knowledge.claim",
609
+ claimed: Boolean(job),
610
+ ...(job ? {
611
+ finalizeId: job.finalizeId,
612
+ claimToken: job.claimToken,
613
+ projectRoot: job.projectRoot,
614
+ writer: job.writer ?? null,
615
+ planPath: job.planPath,
616
+ reportPath: job.reportPath,
617
+ assignments,
618
+ templatePath,
619
+ } : {}),
620
+ });
621
+ return;
622
+ }
623
+ case "done": {
624
+ const jobPath = readRequiredFlag(args, "--job");
625
+ const claimToken = readRequiredFlag(args, "--claim-token");
626
+ const status = readRequiredFlag(args, "--status");
627
+ const result = readOptionalFlag(args, "--result");
628
+ const error = readOptionalFlag(args, "--error");
629
+ assertNoRemainingArgs(args, "knowledge done");
630
+ if (status === "succeeded") {
631
+ if (result === undefined) {
632
+ throw new ClawError("PROJECT_CONFIG_INVALID", "knowledge done --status succeeded requires --result.");
633
+ }
634
+ completeKnowledgeFinalizationJob(jobPath, result, claimToken);
635
+ return;
636
+ }
637
+ if (status === "failed") {
638
+ if (!error) {
639
+ throw new ClawError("PROJECT_CONFIG_INVALID", "knowledge done --status failed requires --error.");
640
+ }
641
+ failKnowledgeFinalizationJob(jobPath, error, claimToken);
642
+ return;
643
+ }
644
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Unsupported knowledge done status "${status}".`);
645
+ }
646
+ default:
647
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown knowledge subcommand "${subcommand ?? ""}".`);
648
+ }
649
+ }
516
650
  async function runPlan(args, effectiveHost) {
517
651
  const subcommand = args.shift();
518
652
  switch (subcommand) {
@@ -551,6 +685,19 @@ async function runPlan(args, effectiveHost) {
551
685
  }
552
686
  const ownerSessionKey = resolveOwnerSessionKey() ?? undefined;
553
687
  const entersEndState = requestsPlanEndState(operations);
688
+ const current = entersEndState
689
+ ? showPlan({ cwd: process.cwd(), ...target, ownerSessionKey })
690
+ : undefined;
691
+ const project = entersEndState ? tryResolveHookProject(process.cwd()) : null;
692
+ const effectiveWriter = current && project
693
+ ? resolvePlanEffectiveConfig(project.projectConfig, current.plan)?.knowledgeWriter
694
+ : undefined;
695
+ if (current
696
+ && !current.plan.parentPlan
697
+ && effectiveWriter?.executionPolicy === "subagent"
698
+ && effectiveHost !== "codex") {
699
+ throw new ClawError("PROJECT_CONFIG_INVALID", 'knowledgeWriter.executionPolicy "subagent" is supported only by the Codex host.', { host: effectiveHost ?? null });
700
+ }
554
701
  const queuePlanEndFinalization = entersEndState
555
702
  ? preparePlanEndFinalization(process.cwd(), ownerSessionKey)
556
703
  : undefined;
@@ -563,7 +710,20 @@ async function runPlan(args, effectiveHost) {
563
710
  ownerSessionKey,
564
711
  });
565
712
  const completionRefresh = queuePlanEndFinalization?.(result.taskName);
566
- printJson(compactPlanCommandResult("plan.edit", result, effectiveHost, completionRefresh));
713
+ const knowledgeDispatch = (current
714
+ && project
715
+ && effectiveHost === "codex"
716
+ && !current.plan.parentPlan
717
+ && effectiveWriter?.executionPolicy === "subagent"
718
+ && result.knowledgeFinalizeId)
719
+ ? buildKnowledgeDispatch({
720
+ projectRoot: project.projectRoot,
721
+ taskName: result.taskName,
722
+ finalizeId: result.knowledgeFinalizeId,
723
+ writer: effectiveWriter,
724
+ })
725
+ : undefined;
726
+ printJson(compactPlanCommandResult("plan.edit", result, effectiveHost, completionRefresh, false, knowledgeDispatch));
567
727
  if (result.operationChain?.status === "partial")
568
728
  process.exitCode = 1;
569
729
  return;
@@ -631,6 +791,20 @@ async function runPlan(args, effectiveHost) {
631
791
  const target = readPlanMutationTarget(args);
632
792
  assertNoRemainingArgs(args, "plan done");
633
793
  const ownerSessionKey = resolveOwnerSessionKey() ?? undefined;
794
+ const current = showPlan({
795
+ cwd: process.cwd(),
796
+ ...target,
797
+ ownerSessionKey,
798
+ });
799
+ const project = tryResolveHookProject(process.cwd());
800
+ const effectiveWriter = project
801
+ ? resolvePlanEffectiveConfig(project.projectConfig, current.plan)?.knowledgeWriter
802
+ : undefined;
803
+ if (!current.plan.parentPlan
804
+ && effectiveWriter?.executionPolicy === "subagent"
805
+ && effectiveHost !== "codex") {
806
+ throw new ClawError("PROJECT_CONFIG_INVALID", 'knowledgeWriter.executionPolicy "subagent" is supported only by the Codex host.', { host: effectiveHost ?? null });
807
+ }
634
808
  const queuePlanEndFinalization = preparePlanEndFinalization(process.cwd(), ownerSessionKey);
635
809
  const result = await editPlan({
636
810
  cwd: process.cwd(),
@@ -641,7 +815,18 @@ async function runPlan(args, effectiveHost) {
641
815
  ownerSessionKey,
642
816
  });
643
817
  const completionRefresh = queuePlanEndFinalization?.(result.taskName);
644
- printJson(compactPlanCommandResult("plan.done", result, effectiveHost, completionRefresh));
818
+ const knowledgeDispatch = (effectiveHost === "codex"
819
+ && !current.plan.parentPlan
820
+ && effectiveWriter?.executionPolicy === "subagent"
821
+ && result.knowledgeFinalizeId)
822
+ ? buildKnowledgeDispatch({
823
+ projectRoot: project.projectRoot,
824
+ taskName: result.taskName,
825
+ finalizeId: result.knowledgeFinalizeId,
826
+ writer: effectiveWriter,
827
+ })
828
+ : undefined;
829
+ printJson(compactPlanCommandResult("plan.done", result, effectiveHost, completionRefresh, false, knowledgeDispatch));
645
830
  return;
646
831
  }
647
832
  case "show": {
@@ -1198,6 +1383,11 @@ async function runHook(args, effectiveHost) {
1198
1383
  });
1199
1384
  }
1200
1385
  async function runStopHook(effectiveHost) {
1386
+ // Cindy owns task-completion closeout in its Host worker path. The CLI sidecar
1387
+ // must not enqueue a job that it cannot execute with a Cindy session runner.
1388
+ if (effectiveHost === "cindy") {
1389
+ return;
1390
+ }
1201
1391
  if (process.env.CLAW_KNOWLEDGE_FINALIZER === "1") {
1202
1392
  return;
1203
1393
  }
@@ -1226,14 +1416,164 @@ async function runStopHook(effectiveHost) {
1226
1416
  host: effectiveHost,
1227
1417
  taskConclusions: transcriptPath ? extractTaskDoneConclusions(transcriptPath, turnId) : [],
1228
1418
  });
1229
- if (result.ok && result.jobPath && process.env.CLAW_KNOWLEDGE_FINALIZER_DISABLE_LAUNCH !== "1") {
1419
+ // Current named hosts own their runner in their adapters. Keep the CLI
1420
+ // launcher only for jobs written by pre-adapter releases with no host.
1421
+ if (result.ok && result.jobPath && !effectiveHost && process.env.CLAW_KNOWLEDGE_FINALIZER_DISABLE_LAUNCH !== "1") {
1230
1422
  launchKnowledgeFinalizationWorker(result.jobPath, project.projectRoot);
1231
1423
  }
1424
+ if (process.env.CLAW_KNOWLEDGE_CAPTURE_RESULT === "1") {
1425
+ printJson(result);
1426
+ }
1232
1427
  }
1233
1428
  catch {
1234
1429
  // Knowledge capture is a fail-open sidecar and must never block Stop.
1235
1430
  }
1236
1431
  }
1432
+ /**
1433
+ * Machine-facing report capture for hosts whose final-message hook is owned by
1434
+ * the adapter rather than by the CLI. Unlike `hook auto-doc`, this only
1435
+ * persists the report/job hand-off; it never chooses or launches a writer.
1436
+ */
1437
+ async function runInternalKnowledgeCapture(args, effectiveHost) {
1438
+ assertNoRemainingArgs(args, "internal-knowledge-capture");
1439
+ const payload = await readStdinJson();
1440
+ const hookCwd = resolveHookCwd(payload);
1441
+ const sessionId = resolveOwnerSessionKey(payload);
1442
+ const turnId = readHookString(payload, "turn_id");
1443
+ const message = readHookString(payload, "message");
1444
+ if (!hookCwd || !sessionId || !turnId || !message || !containsClawDir(hookCwd)) {
1445
+ printJson({ ok: true, captured: false });
1446
+ return;
1447
+ }
1448
+ try {
1449
+ const project = resolveProjectContext(hookCwd);
1450
+ const result = tryCaptureKnowledgeStop({
1451
+ project,
1452
+ sessionId,
1453
+ turnId,
1454
+ message,
1455
+ host: effectiveHost === "cindy" ? "cindy" : effectiveHost,
1456
+ });
1457
+ printJson(result);
1458
+ }
1459
+ catch (error) {
1460
+ // Capture is a non-blocking sidecar. Return structured failure so the
1461
+ // owning adapter can surface/retry it without affecting the assistant turn.
1462
+ printJson({ ok: false, error: error instanceof Error ? error.message : String(error) });
1463
+ }
1464
+ }
1465
+ /** Record a successful adapter-owned writer run without invoking a host SDK. */
1466
+ function runInternalKnowledgeComplete(args) {
1467
+ const jobPath = readRequiredFlag(args, "--job");
1468
+ const result = readRequiredFlag(args, "--result");
1469
+ assertNoRemainingArgs(args, "internal-knowledge-complete");
1470
+ const running = ensureLegacyKnowledgeClaim(jobPath);
1471
+ if (!running?.claimToken) {
1472
+ printJson({ ok: true, completed: false, reason: "job is not claimable" });
1473
+ return;
1474
+ }
1475
+ completeKnowledgeFinalizationJob(jobPath, result, running.claimToken);
1476
+ }
1477
+ function runInternalKnowledgeClaim(args) {
1478
+ const jobPath = readRequiredFlag(args, "--job");
1479
+ assertNoRemainingArgs(args, "internal-knowledge-claim");
1480
+ const job = ensureLegacyKnowledgeClaim(jobPath);
1481
+ printJson({
1482
+ ok: true,
1483
+ claimed: Boolean(job),
1484
+ ...(job ? {
1485
+ finalizeId: job.finalizeId,
1486
+ claimToken: job.claimToken,
1487
+ } : {}),
1488
+ });
1489
+ }
1490
+ function runInternalKnowledgeFail(args) {
1491
+ const jobPath = readRequiredFlag(args, "--job");
1492
+ const message = readRequiredFlag(args, "--message");
1493
+ assertNoRemainingArgs(args, "internal-knowledge-fail");
1494
+ const job = ensureLegacyKnowledgeClaim(jobPath);
1495
+ if (!job?.claimToken) {
1496
+ printJson({ ok: true, failed: false, reason: "job is not claimable" });
1497
+ return;
1498
+ }
1499
+ failKnowledgeFinalizationJob(jobPath, message, job.claimToken);
1500
+ }
1501
+ function ensureLegacyKnowledgeClaim(jobPath) {
1502
+ const job = readKnowledgeFinalizationJob(jobPath);
1503
+ if (job.status === "succeeded" || job.status === "failed" && job.attempts >= 3) {
1504
+ return null;
1505
+ }
1506
+ if (job.status === "running") {
1507
+ return job;
1508
+ }
1509
+ return claimKnowledgeFinalizationJob(jobPath);
1510
+ }
1511
+ function completeKnowledgeFinalizationJob(jobPath, result, claimToken) {
1512
+ const running = readKnowledgeFinalizationJob(jobPath);
1513
+ if (running.status === "succeeded") {
1514
+ const terminal = doneKnowledgeFinalizationJob({
1515
+ jobPath,
1516
+ claimToken,
1517
+ status: "succeeded",
1518
+ result,
1519
+ });
1520
+ removeKnowledgeAssignmentTemplate(jobPath, running.finalizeId);
1521
+ printJson({ ok: true, completed: true, alreadyDone: terminal.alreadyDone, finalizeId: running.finalizeId });
1522
+ return;
1523
+ }
1524
+ if (running.status !== "running") {
1525
+ throw new Error("Knowledge finalization job must be claimed before successful completion.");
1526
+ }
1527
+ if (running.claimToken !== claimToken) {
1528
+ throw new Error("Knowledge finalization completion does not match the active claim.");
1529
+ }
1530
+ const project = resolveProjectContext(running.projectRoot);
1531
+ const finishedAt = new Date().toISOString();
1532
+ const truthEncoding = normalizeTruthMarkdownEncoding(project);
1533
+ recordKnowledgeFinalizationResult(project, running.reportPath, {
1534
+ schemaVersion: 1,
1535
+ entryType: "knowledge_finalization",
1536
+ finalizeId: running.finalizeId,
1537
+ taskName: running.taskName,
1538
+ recordedAt: finishedAt,
1539
+ status: "succeeded",
1540
+ result,
1541
+ attempts: running.attempts,
1542
+ ...(running.host !== undefined ? { host: running.host } : {}),
1543
+ truthEncoding,
1544
+ });
1545
+ const terminal = doneKnowledgeFinalizationJob({
1546
+ jobPath,
1547
+ claimToken,
1548
+ status: "succeeded",
1549
+ result,
1550
+ finishedAt,
1551
+ patch: { truthEncoding },
1552
+ });
1553
+ removeKnowledgeAssignmentTemplate(jobPath, running.finalizeId);
1554
+ queueCompletionRefresh({
1555
+ cwd: running.projectRoot,
1556
+ taskName: running.taskName,
1557
+ includeTaskRetention: false,
1558
+ includeTaskMemory: false,
1559
+ statusLabel: `knowledge-${running.finalizeId.slice(0, 12)}`,
1560
+ skipGitNexusRefresh: true,
1561
+ });
1562
+ printJson({ ok: true, completed: true, alreadyDone: terminal.alreadyDone, finalizeId: running.finalizeId });
1563
+ }
1564
+ function failKnowledgeFinalizationJob(jobPath, message, claimToken) {
1565
+ const terminal = doneKnowledgeFinalizationJob({
1566
+ jobPath,
1567
+ claimToken,
1568
+ status: "failed",
1569
+ error: message,
1570
+ });
1571
+ removeKnowledgeAssignmentTemplate(jobPath, terminal.job.finalizeId);
1572
+ printJson({ ok: true, failed: true, alreadyDone: terminal.alreadyDone, finalizeId: terminal.job.finalizeId });
1573
+ }
1574
+ function removeKnowledgeAssignmentTemplate(jobPath, finalizeId) {
1575
+ fs.rmSync(path.join(path.dirname(jobPath), `${finalizeId}.assignments.json`), { force: true });
1576
+ }
1237
1577
  async function runInternalEmbeddingWarmup(args) {
1238
1578
  const cwd = readRequiredFlag(args, "--cwd");
1239
1579
  assertNoRemainingArgs(args, "internal-embedding-warmup");
@@ -1245,110 +1585,76 @@ async function runInternalEmbeddingWarmup(args) {
1245
1585
  }
1246
1586
  async function runInternalKnowledgeFinalize(args) {
1247
1587
  const jobPath = readRequiredFlag(args, "--job");
1248
- const running = claimKnowledgeFinalizationJob(jobPath);
1249
- if (!running) {
1588
+ const queued = readKnowledgeFinalizationJob(jobPath);
1589
+ assertNoRemainingArgs(args, "internal-knowledge-finalize");
1590
+ if ((queued.writer?.executionPolicy ?? "background") !== "background") {
1250
1591
  return;
1251
1592
  }
1252
1593
  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 } : {}),
1594
+ const writerRun = await runKnowledgeDelegateForJob(queued);
1595
+ const terminal = readKnowledgeFinalizationJob(jobPath);
1596
+ if (terminal.status !== "succeeded" && terminal.status !== "failed") {
1597
+ throw new Error("Knowledge delegate returned without acknowledging a terminal result.");
1598
+ }
1599
+ if (writerRun.threadId) {
1600
+ assertCompletedKnowledgeWriterSession(writerRun.threadId);
1601
+ }
1602
+ printJson({
1603
+ ok: terminal.status === "succeeded",
1604
+ command: "internal-knowledge-finalize",
1605
+ finalizeId: terminal.finalizeId,
1606
+ status: terminal.status,
1285
1607
  ...(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
1608
  });
1299
1609
  }
1300
1610
  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);
1611
+ const message = error instanceof Error ? error.message : String(error);
1612
+ let failed = readKnowledgeFinalizationJob(jobPath);
1613
+ if (failed.status === "running" && failed.claimToken) {
1614
+ failed = doneKnowledgeFinalizationJob({
1615
+ jobPath,
1616
+ claimToken: failed.claimToken,
1617
+ status: "failed",
1618
+ error: message,
1619
+ }).job;
1620
+ }
1621
+ else if (failed.status === "queued") {
1622
+ const supervisorClaim = claimKnowledgeFinalizationJob(jobPath);
1623
+ if (supervisorClaim?.claimToken) {
1624
+ failed = doneKnowledgeFinalizationJob({
1625
+ jobPath,
1626
+ claimToken: supervisorClaim.claimToken,
1627
+ status: "failed",
1628
+ error: message,
1629
+ }).job;
1630
+ }
1631
+ }
1632
+ if (failed.status === "failed") {
1633
+ removeKnowledgeAssignmentTemplate(jobPath, failed.finalizeId);
1634
+ }
1308
1635
  if (failed.attempts < 3 && process.env.CLAW_KNOWLEDGE_FINALIZER_DISABLE_RETRY !== "1") {
1309
1636
  launchKnowledgeFinalizationWorker(jobPath, failed.projectRoot);
1310
1637
  }
1311
1638
  }
1312
1639
  }
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));
1640
+ async function runKnowledgeDelegateForJob(running) {
1641
+ const dispatch = buildKnowledgeDelegateDispatch({
1642
+ policy: "background",
1643
+ projectRoot: running.projectRoot,
1644
+ taskName: running.taskName,
1645
+ finalizeId: running.finalizeId,
1646
+ writer: running.writer,
1647
+ });
1648
+ if (running.host === "opencode") {
1649
+ return runOpencodeKnowledgeWriter({
1650
+ prompt: dispatch.prompt,
1651
+ projectRoot: running.projectRoot,
1652
+ writer: running.writer ?? null,
1653
+ });
1654
+ }
1655
+ if (running.host !== undefined && running.host !== null && running.host !== "codex") {
1656
+ throw new Error(`Unsupported knowledge finalization job host "${String(running.host)}".`);
1342
1657
  }
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
1658
  const sdk = await import(pathToFileURL(resolveCodexSdkEntryPath()).href);
1353
1659
  const Codex = sdk.Codex;
1354
1660
  const codex = new Codex({
@@ -1368,10 +1674,7 @@ async function runCodexSdkWriter(running, skill) {
1368
1674
  ? { modelReasoningEffort: writer.reasoningEffort }
1369
1675
  : {}),
1370
1676
  });
1371
- const turn = await thread.run(buildKnowledgeWriterPrompt(running, skill));
1372
- if (isBuiltInKnowledgeWriterSkill(skill)) {
1373
- assertCompletedKnowledgeWriterSession(thread.id);
1374
- }
1677
+ const turn = await thread.run(dispatch.prompt);
1375
1678
  return {
1376
1679
  finalResponse: turn.finalResponse,
1377
1680
  ...(thread.id ? { threadId: thread.id } : {}),
@@ -1416,28 +1719,11 @@ function knowledgeFinalizerEnvironment() {
1416
1719
  }
1417
1720
  }
1418
1721
  env.CLAW_KNOWLEDGE_FINALIZER = "1";
1722
+ delete env.CLAW_SESSION_ID;
1723
+ delete env.CODEX_THREAD_ID;
1724
+ delete env.CODEX_SESSION_ID;
1419
1725
  return env;
1420
1726
  }
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
1727
  function launchKnowledgeFinalizationWorker(jobPath, cwd) {
1442
1728
  if (process.platform === "win32") {
1443
1729
  const launcherScript = [
@@ -1484,11 +1770,15 @@ async function runSessionStartHook(effectiveHost) {
1484
1770
  try {
1485
1771
  const context = await runContextCommand([], hookCwd, ownerSessionKey, effectiveHost);
1486
1772
  const contextProject = asJsonRecord(context.project);
1773
+ const retryableJobs = contextProject?.scope !== "session" && !context.error
1774
+ ? listRetryableKnowledgeFinalizationJobs(resolveProjectContext(hookCwd))
1775
+ : [];
1487
1776
  if (contextProject?.scope !== "session"
1488
1777
  && !context.error
1778
+ && effectiveHost !== "cindy"
1489
1779
  && process.env.CLAW_KNOWLEDGE_FINALIZER_DISABLE_LAUNCH !== "1") {
1490
1780
  const project = resolveProjectContext(hookCwd);
1491
- for (const jobPath of listRetryableKnowledgeFinalizationJobs(project)) {
1781
+ for (const jobPath of retryableJobs) {
1492
1782
  try {
1493
1783
  launchKnowledgeFinalizationWorker(jobPath, project.projectRoot);
1494
1784
  }
@@ -1505,6 +1795,7 @@ async function runSessionStartHook(effectiveHost) {
1505
1795
  hookSpecificOutput: {
1506
1796
  hookEventName: "SessionStart",
1507
1797
  additionalContext,
1798
+ ...(effectiveHost === "cindy" ? { knowledgeJobs: retryableJobs } : {}),
1508
1799
  },
1509
1800
  })}\n`);
1510
1801
  }
@@ -1578,6 +1869,9 @@ function safeResolveTempDir() {
1578
1869
  }
1579
1870
  }
1580
1871
  function buildSessionStartAdditionalContext(context, sessionCwd, effectiveHost) {
1872
+ if (effectiveHost === "cindy") {
1873
+ return buildCindySessionStartContext(context, sessionCwd);
1874
+ }
1581
1875
  const versionSyncPrompt = buildVersionSyncPrompt(context);
1582
1876
  const searchGuidance = buildContextSearchGuidance(context);
1583
1877
  const runtimeErrorPrompt = buildCodexRuntimeErrorPrompt(context);
@@ -1613,6 +1907,40 @@ function buildSessionStartAdditionalContext(context, sessionCwd, effectiveHost)
1613
1907
  const promptWithSearch = searchGuidance ? `${promptWithVersion}\n${searchGuidance}` : promptWithVersion;
1614
1908
  return runtimeErrorPrompt ? `${runtimeErrorPrompt}\n\n${promptWithSearch}` : promptWithSearch;
1615
1909
  }
1910
+ /**
1911
+ * 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.
1914
+ */
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");
1932
+ }
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");
1943
+ }
1616
1944
  function buildCodexRuntimeErrorPrompt(context) {
1617
1945
  const error = asJsonRecord(context.error);
1618
1946
  if (error?.code !== "CODEX_SDK_RUNTIME_MISSING") {
@@ -1674,8 +2002,9 @@ function buildVersionSyncPrompt(context) {
1674
2002
  return {
1675
2003
  placement: "suffix",
1676
2004
  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.`,
2005
+ `A newer claw-kit version is available: installed CLI ${cliVersion}, published latest ${latestPublishedVersion}.`,
2006
+ "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.",
2007
+ `After the user confirms, use ${updateSkill} to update the claw-kit CLI and the current host plugin surface, then continue the original task.`,
1679
2008
  ],
1680
2009
  };
1681
2010
  }
@@ -1820,6 +2149,7 @@ async function runSubplan(args, effectiveHost) {
1820
2149
  }
1821
2150
  function resolveOwnerSessionKey(payload) {
1822
2151
  const envCandidates = [
2152
+ process.env.CLAW_SESSION_ID,
1823
2153
  process.env.CODEX_THREAD_ID,
1824
2154
  process.env.CODEX_SESSION_ID,
1825
2155
  ];
@@ -1880,13 +2210,20 @@ function readJson(filePath) {
1880
2210
  function stripBom(content) {
1881
2211
  return content.charCodeAt(0) === 0xfeff ? content.slice(1) : content;
1882
2212
  }
1883
- function compactPlanCommandResult(command, result, effectiveHost, completionRefresh, forceProjectionSync = false) {
2213
+ function buildKnowledgeDispatch(input) {
2214
+ return buildKnowledgeDelegateDispatch({
2215
+ policy: "subagent",
2216
+ ...input,
2217
+ });
2218
+ }
2219
+ function compactPlanCommandResult(command, result, effectiveHost, completionRefresh, forceProjectionSync = false, knowledgeDispatch) {
1884
2220
  const archivedPlanPath = completionRefresh?.taskRetention.archivedCurrentTask?.taskName === result.taskName &&
1885
2221
  completionRefresh.taskRetention.archivedCurrentTask.archivedPlanPath
1886
2222
  ? completionRefresh.taskRetention.archivedCurrentTask.archivedPlanPath
1887
2223
  : undefined;
1888
2224
  const resolvedPlanPath = archivedPlanPath ?? result.planPath;
1889
2225
  const codexResult = effectiveHost === "codex";
2226
+ const cindyResult = effectiveHost === "cindy";
1890
2227
  const hostActions = codexResult ? buildHostActions(result, { forceProjectionSync, actionIdPrefix: command === "plan.sync" ? `plan.sync:${createHash("sha256").update(result.planPath).digest("hex").slice(0, 16)}` : undefined }) : [];
1891
2228
  const nextsteps = codexResult
1892
2229
  && result.planStatus === "end.completed"
@@ -1917,16 +2254,17 @@ function compactPlanCommandResult(command, result, effectiveHost, completionRefr
1917
2254
  ...(archivedPlanPath ? { archivedPlanPath } : {}),
1918
2255
  planStatus: result.planStatus,
1919
2256
  ...(achievement ? { achievement } : {}),
2257
+ ...(knowledgeDispatch ? { knowledgeDispatch } : {}),
1920
2258
  ...(!codexResult && result.previousPlanStatus ? { previousPlanStatus: result.previousPlanStatus } : {}),
1921
2259
  ...(hostActions.length ? { hostActions } : {}),
1922
2260
  ...(!codexResult && result.changedTaskIds?.length ? { changedTaskIds: result.changedTaskIds } : {}),
1923
2261
  ...(!codexResult && result.appendedTaskIds?.length ? { appendedTaskIds: result.appendedTaskIds } : {}),
1924
2262
  ...(codexResult ? { stage: result.workflowGuidance.stage } : {}),
1925
- ...(!codexResult || result.planStatus === "end.completed"
2263
+ ...((!codexResult && !cindyResult) || result.planStatus === "end.completed" && !cindyResult
1926
2264
  ? { nextsteps }
1927
2265
  : {}),
1928
2266
  ...(result.workflowGuidance.nextTask ? { nextTask: result.workflowGuidance.nextTask } : {}),
1929
- ...(result.workflowGuidance.notes?.trim() && !codexResult
2267
+ ...(result.workflowGuidance.notes?.trim() && !codexResult && !cindyResult
1930
2268
  ? { notes: result.workflowGuidance.notes }
1931
2269
  : {}),
1932
2270
  ...(result.workflowGuidance.commandHints?.length
@@ -1941,9 +2279,13 @@ function compactPlanCommandResult(command, result, effectiveHost, completionRefr
1941
2279
  failedOperation: result.operationChain.failedOperation,
1942
2280
  }
1943
2281
  : {}),
1944
- ...(!codexResult && result.workflowGuidance.goalMode ? { goalMode: result.workflowGuidance.goalMode } : {}),
1945
- ...(!codexResult && result.workflowGuidance.goalTool ? { goalTool: result.workflowGuidance.goalTool } : {}),
2282
+ ...(!codexResult && !cindyResult && result.workflowGuidance.goalMode ? { goalMode: result.workflowGuidance.goalMode } : {}),
2283
+ ...(!codexResult && !cindyResult && result.workflowGuidance.goalTool ? { goalTool: result.workflowGuidance.goalTool } : {}),
1946
2284
  ...(includePlan && result.plan ? { plan: result.plan } : {}),
2285
+ // Cindy's Ghost card is a Host-owned projection. It needs the
2286
+ // canonical task list to render its expandable Todo view, but that
2287
+ // view stays out of Agent guidance in the Cindy adapter.
2288
+ ...(cindyResult ? { planView: result.planView } : {}),
1947
2289
  ...(result.planReview
1948
2290
  ? {
1949
2291
  planReview: {
@@ -3305,7 +3647,7 @@ function printTopLevelUsage() {
3305
3647
  "Global flags:",
3306
3648
  " -h, --help Show help (use `claw help <command>` for command details).",
3307
3649
  " -v, --version Print the CLI version.",
3308
- " --host <host> Select host-specific output projection (codex or opencode).",
3650
+ " --host <host> Select host-specific output projection (codex, opencode, or cindy).",
3309
3651
  "",
3310
3652
  "Run `claw help <command>` or `claw help <command> <subcommand>` for detailed help.",
3311
3653
  ];