@veewo/claw 0.1.75 → 0.1.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2,15 +2,20 @@
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
5
6
  import { createHash } from "node:crypto";
6
7
  import { spawn, spawnSync } from "node:child_process";
7
- import { buildDirectWorkflowGuidance, checkProjectProtocol, ClawError, buildPlanWorkflowGuidance, buildMemoryIndex, buildSessionStartDefaultPrompt, buildSessionStartRecoveredPrompt, editPlan, ensureProjectProtocol, enforceTaskRetention, ingestTruth, initProject, getTemplateTaskDoneChoices, resolvePlanTemplateFile, resolveProjectContext, resolveSessionBoundPlan, resolveContext, resolveSeedPlanTemplate, searchMemory, showPlan, createSubplan, switchTask, unbindSession, writePlan, } from "@veewo/claw-core";
8
+ import { buildDirectWorkflowGuidance, checkProjectProtocol, ClawError, buildPlanWorkflowGuidance, buildMemoryIndex, buildSessionStartDefaultPrompt, buildSessionStartRecoveredPrompt, editPlan, ensureProjectProtocol, enforceTaskRetention, ingestTruth, initProject, getTemplateTaskDoneChoices, resolvePlanTemplateFile, resolveProjectContext, resolveSessionBoundPlan, resolveContext, resolveSeedPlanTemplate, searchMemory, showPlan, createSubplan, switchTask, tryCaptureKnowledgeStop, claimKnowledgeFinalizationJob, listRetryableKnowledgeFinalizationJobs, normalizeTruthMarkdownEncoding, tryCleanupKnowledgeFinalizationReport, writeKnowledgeFinalizationJob, unbindSession, writePlan, } from "@veewo/claw-core";
9
+ import { buildCodexDriverEnvelope } from "./codex-driver.js";
10
+ import { checkCodexRuntime, resolveCodexSdkEntryPath } from "./codex-runtime.js";
11
+ import { extractLatestFinalAssistantMessage } from "./codex-transcript.js";
8
12
  const CLI_VERSION = readCliVersion();
9
13
  const TOP_LEVEL_COMMANDS = [
10
14
  { name: "init [options]", summary: "Initialize and normalize the .claw project surface." },
11
15
  { name: "context [--task <name>]", summary: "Resolve project context, auto-initializing or correcting .claw state." },
12
16
  { name: "check", summary: "Check and auto-correct .claw project protocol fields." },
13
- { name: "plan <subcommand> [options]", summary: "Plan lifecycle: create, start, edit, show, done." },
17
+ { name: "plan <subcommand> [options]", summary: "Plan lifecycle: create, start, edit, remove, wait, resume, show, done." },
18
+ { name: "codex driver", summary: "Return the versioned code-mode driver used by the Codex adapter." },
14
19
  { name: "template <subcommand> [options]", summary: "Plan template helpers such as validation." },
15
20
  { name: "task <subcommand> [options]", summary: "Task lifecycle helpers inside an existing plan." },
16
21
  { name: "subplan create [options]", summary: "Create a subplan nested under a parent task item." },
@@ -28,8 +33,7 @@ const COMMAND_HELP = {
28
33
  { flag: "--name <project-name>", detail: "Human-readable project name." },
29
34
  { flag: "--context-path <file>", detail: "Extra context path to track (repeatable)." },
30
35
  { flag: "--ext-path <path>", detail: "External doc path to index (repeatable)." },
31
- { flag: "--external-truth-skill <skill>", detail: "Skill id for external truth-writer dispatch." },
32
- { flag: "--external-adr-skill <skill>", detail: "Skill id for external adr-writer dispatch." },
36
+ { flag: "--external-writer-skill <skill>", detail: "Skill id for the combined knowledge writer." },
33
37
  { flag: "--planning true|false", detail: "Enable planning-aware default template behavior (default true)." },
34
38
  { flag: "--external-planning-skill <skill>", detail: "Skill id for an external planning skill." },
35
39
  { flag: "--gitnexus true|false", detail: "Enable GitNexus integration (default false)." },
@@ -66,60 +70,111 @@ const COMMAND_HELP = {
66
70
  ],
67
71
  },
68
72
  edit: {
69
- usage: ["{script} plan edit --task <name> [options]"],
70
- description: "Edit an existing plan: update status, append tasks, apply merge-patch updates, add references, rules, and key decisions.",
71
- summary: "Edit a plan: status, tasks, references, rules, key decisions.",
73
+ usage: ["{script} plan edit [options]"],
74
+ description: "Apply plan field and status edits in argument order to the session-bound current plan.",
75
+ summary: "Edit plan fields in an ordered chain; repeat collection options to append multiple values.",
72
76
  options: [
73
- { flag: "--task <name>", detail: "(required) Task name to edit." },
74
- { flag: "--plan <relative-path>", detail: "Plan file relative to the task dir (defaults to the active plan)." },
75
- { flag: "--plan-status <status>", detail: "Set plan status (e.g. process.active, process.wait)." },
76
- { flag: "--task-id <id>", detail: "Target a specific task by id for status updates." },
77
- { flag: "--task-status <status>", detail: "Set the task status (e.g. pending, in_progress, done)." },
78
- { flag: "--task-choice <choice-id>", detail: "Record the route choice when a task is marked done through a route-aware template." },
79
- { flag: "--append-tasks <json-file>", detail: "Append tasks from a JSON array file." },
80
- { flag: "--patch <json-file>", detail: "Apply a JSON merge-patch object from a file. Objects merge recursively, null deletes fields, arrays replace the whole field." },
77
+ { flag: "--status <status>", detail: "Advanced: set the plan status directly." },
78
+ { flag: "--goal <text>", detail: "Set goal.text." },
79
+ { flag: "--requirements <text>", detail: "Set the requirements summary." },
80
+ { flag: "--question <text>", detail: "Add an open question (repeatable)." },
81
+ { flag: "--acceptance <text>", detail: "Add an acceptance criterion (repeatable)." },
82
+ { flag: "--summary <text>", detail: "Set the plan summary." },
81
83
  { flag: "--rule <text>", detail: "Append a rule (repeatable)." },
82
84
  { flag: "--key-decision <text>", detail: "Append a key decision (repeatable)." },
83
- { flag: "--reference-path <path>", detail: "Add a reference (requires --reference-why)." },
84
- { flag: "--reference-why <why>", detail: "Why the reference matters (requires --reference-path)." },
85
- { flag: "--summary <text>", detail: "Optional change summary." },
85
+ { flag: "--reference <path>", detail: "Add a reference; follow it immediately with --why (repeatable)." },
86
+ { flag: "--why <text>", detail: "Explain the immediately preceding --reference." },
87
+ { flag: "--task-name <name>", detail: "Advanced: override the session-bound task scope." },
88
+ { flag: "--plan-file <relative-path>", detail: "Advanced: override the session-bound plan file." },
89
+ ],
90
+ },
91
+ remove: {
92
+ usage: ["{script} plan remove [options]"],
93
+ description: "Remove exact values from array fields on the session-bound current plan.",
94
+ summary: "Remove questions, acceptance criteria, rules, decisions, or references.",
95
+ options: [
96
+ { flag: "--question <text>", detail: "Remove an open question by exact text (repeatable)." },
97
+ { flag: "--acceptance <text>", detail: "Remove an acceptance criterion by exact text (repeatable)." },
98
+ { flag: "--rule <text>", detail: "Remove a rule by exact text (repeatable)." },
99
+ { flag: "--key-decision <text>", detail: "Remove a key decision by exact text (repeatable)." },
100
+ { flag: "--reference <path>", detail: "Remove references matching a path (repeatable)." },
101
+ { flag: "--task-name <name>", detail: "Advanced: override the session-bound task scope." },
102
+ { flag: "--plan-file <relative-path>", detail: "Advanced: override the session-bound plan file." },
103
+ ],
104
+ },
105
+ wait: {
106
+ usage: ["{script} plan wait"],
107
+ description: "Pause active execution by moving the plan to process.wait.",
108
+ summary: "Pause active execution.",
109
+ options: [
110
+ { flag: "--task-name <name>", detail: "Advanced: override the session-bound task scope." },
111
+ { flag: "--plan-file <relative-path>", detail: "Advanced: override the session-bound plan file." },
112
+ ],
113
+ },
114
+ resume: {
115
+ usage: ["{script} plan resume"],
116
+ description: "Resume paused execution by moving the plan to process.active.",
117
+ summary: "Resume paused execution.",
118
+ options: [
119
+ { flag: "--task-name <name>", detail: "Advanced: override the session-bound task scope." },
120
+ { flag: "--plan-file <relative-path>", detail: "Advanced: override the session-bound plan file." },
86
121
  ],
87
122
  },
88
123
  start: {
89
- usage: ["{script} plan start --task <name> --patch <json-file> --append-tasks <json-file>"],
124
+ usage: ["{script} plan start --requirements <text> --add-task <title> [--detail <text>] [options]"],
90
125
  description: "Atomically apply refined plan content, append business tasks, complete the default planning/activation bridge, and enter process.active in one serialized mutation.",
91
126
  summary: "Atomically refine and activate a default planning plan.",
92
127
  options: [
93
- { flag: "--task <name>", detail: "(required) Task name to refine and activate." },
94
- { flag: "--plan <relative-path>", detail: "Plan file relative to the task dir (defaults to the active plan)." },
95
- { flag: "--patch <json-file>", detail: "Apply a JSON merge-patch containing requirements, rules, references, or decisions." },
96
- { flag: "--append-tasks <json-file>", detail: "Append business tasks from a JSON array file." },
97
- { flag: "--summary <text>", detail: "Optional change summary." },
128
+ { flag: "--goal <text>", detail: "Set goal.text." },
129
+ { flag: "--requirements <text>", detail: "Set the requirements summary." },
130
+ { flag: "--question <text>", detail: "Add an open question (repeatable)." },
131
+ { flag: "--acceptance <text>", detail: "Add an acceptance criterion (repeatable)." },
132
+ { flag: "--add-task <title>", detail: "Add a business task; optionally follow it immediately with --detail (repeatable)." },
133
+ { flag: "--detail <text>", detail: "Describe the immediately preceding --add-task." },
134
+ { flag: "--rule <text>", detail: "Append a rule (repeatable)." },
135
+ { flag: "--key-decision <text>", detail: "Append a key decision (repeatable)." },
136
+ { flag: "--reference <path>", detail: "Add a reference; follow it immediately with --why (repeatable)." },
137
+ { flag: "--why <text>", detail: "Explain the immediately preceding --reference." },
138
+ { flag: "--task-name <name>", detail: "Advanced: override the session-bound task scope." },
139
+ { flag: "--plan-file <relative-path>", detail: "Advanced: override the session-bound plan file." },
98
140
  ],
99
141
  },
100
142
  show: {
101
- usage: ["{script} plan show --task <name> [--plan <relative-path>]"],
102
- description: "Show the current plan (status, goal, tasks, references) for a task, including archived plans.",
143
+ usage: ["{script} plan show"],
144
+ description: "Show the session-bound current plan, including archived plans through an explicit override.",
103
145
  summary: "Show the current plan for a task.",
104
146
  options: [
105
- { flag: "--task <name>", detail: "(required) Task name to show." },
106
- { flag: "--plan <relative-path>", detail: "Plan file relative to the task dir." },
147
+ { flag: "--task-name <name>", detail: "Advanced: override the session-bound task scope." },
148
+ { flag: "--plan-file <relative-path>", detail: "Advanced: override the session-bound plan file." },
107
149
  ],
108
150
  },
109
151
  done: {
110
- usage: ["{script} plan done --task <name> [--summary <text>] [options]"],
152
+ usage: ["{script} plan done --retrospective <text> [options]"],
111
153
  description: "Close out a plan: write a retrospective, mark status end.completed with completedAt, retain it for at least one hour, sweep older completed tasks into the archive, and queue the async completion refresh.",
112
154
  summary: "Close out a plan with a retrospective and queue completion refresh.",
113
155
  options: [
114
- { flag: "--task <name>", detail: "(required) Task name to close out." },
115
- { flag: "--plan <relative-path>", detail: "Plan file relative to the task dir." },
116
- { flag: "--summary <text>", detail: "Retrospective summary (required unless a patch provides retrospective.summary)." },
117
- { flag: "--change-summary <text>", detail: "Optional change summary." },
118
- { flag: "--patch <json-file>", detail: "Apply a JSON merge-patch object (for example one that sets retrospective.summary)." },
156
+ { flag: "--retrospective <text>", detail: "Retrospective summary (required)." },
157
+ { flag: "--key-decision <text>", detail: "Append a durable key decision when one exists (repeatable)." },
158
+ { flag: "--what-worked <text>", detail: "Append a retrospective success (repeatable)." },
159
+ { flag: "--issue <text>", detail: "Append a retrospective issue (repeatable)." },
160
+ { flag: "--follow-up <text>", detail: "Append a retrospective follow-up (repeatable)." },
161
+ { flag: "--task-name <name>", detail: "Advanced: override the session-bound task scope." },
162
+ { flag: "--plan-file <relative-path>", detail: "Advanced: override the session-bound plan file." },
119
163
  ],
120
164
  },
121
165
  },
122
166
  },
167
+ codex: {
168
+ usage: ["{script} codex <subcommand>"],
169
+ description: "Codex adapter runtime helpers.",
170
+ subcommands: {
171
+ driver: {
172
+ usage: ["{script} codex driver"],
173
+ description: "Return the versioned JavaScript driver source used by the short code-mode bootstrap.",
174
+ summary: "Return the versioned code-mode driver source.",
175
+ },
176
+ },
177
+ },
123
178
  template: {
124
179
  usage: ["{script} template <subcommand> [options]"],
125
180
  description: "Helpers for inspecting and validating plan templates.",
@@ -141,17 +196,52 @@ const COMMAND_HELP = {
141
196
  },
142
197
  task: {
143
198
  usage: ["{script} task <subcommand> [options]"],
144
- description: "Task-focused helpers layered on top of plan edits.",
199
+ description: "Add, edit, remove, or complete task items on the session-bound current plan.",
145
200
  subcommands: {
201
+ add: {
202
+ usage: ["{script} task add --title <text> [--detail <text>] [--title <text> [--detail <text>] ...]"],
203
+ description: "Add one or more pending task items to the current plan in argument order.",
204
+ summary: "Add task items with repeated --title groups.",
205
+ options: [
206
+ { flag: "--title <text>", detail: "Task title (required)." },
207
+ { flag: "--detail <text>", detail: "Optional task detail." },
208
+ { flag: "--task-name <name>", detail: "Advanced: override the session-bound task scope." },
209
+ { flag: "--plan-file <relative-path>", detail: "Advanced: override the session-bound plan file." },
210
+ ],
211
+ },
212
+ edit: {
213
+ usage: ["{script} task edit --id <number> [options] [--id <number> [options] ...]"],
214
+ description: "Update one or more task items on the current plan in argument order.",
215
+ summary: "Edit task items with repeated --id groups.",
216
+ options: [
217
+ { flag: "--id <number>", detail: "Task item id (required)." },
218
+ { flag: "--title <text>", detail: "Set the task title." },
219
+ { flag: "--detail <text>", detail: "Set the task detail." },
220
+ { flag: "--status <status>", detail: "Set pending, in_progress, subagent_running, done, or blocked." },
221
+ { flag: "--choice <choice-id>", detail: "Record a route choice when status becomes done." },
222
+ { flag: "--task-name <name>", detail: "Advanced: override the session-bound task scope." },
223
+ { flag: "--plan-file <relative-path>", detail: "Advanced: override the session-bound plan file." },
224
+ ],
225
+ },
226
+ remove: {
227
+ usage: ["{script} task remove --id <number> [--id <number> ...]"],
228
+ description: "Remove one or more task items from the current plan in argument order.",
229
+ summary: "Remove task items with repeated --id values.",
230
+ options: [
231
+ { flag: "--id <number>", detail: "Task item id (required)." },
232
+ { flag: "--task-name <name>", detail: "Advanced: override the session-bound task scope." },
233
+ { flag: "--plan-file <relative-path>", detail: "Advanced: override the session-bound plan file." },
234
+ ],
235
+ },
146
236
  done: {
147
- usage: ["{script} task done --task <name> --id <number> [--choice <choice-id>] [--plan <relative-path>]"],
148
- description: "Mark a task item as done. Route-aware templates may require `--choice`, and the selected choice is persisted as `task.choiceId` in the plan state.",
149
- summary: "Mark a task item as done, optionally recording a routing choice.",
237
+ usage: ["{script} task done --id <number> [--choice <choice-id>] [--id <number> [--choice <choice-id>] ...]"],
238
+ description: "Mark one or more task items as done in argument order. Route-aware templates may require `--choice`, and each selected choice is persisted as `task.choiceId` in the plan state.",
239
+ summary: "Complete task items with repeated --id groups, optionally recording routing choices.",
150
240
  options: [
151
- { flag: "--task <name>", detail: "(required) Task name to edit." },
152
241
  { flag: "--id <number>", detail: "(required) Task item id to mark done." },
153
242
  { flag: "--choice <choice-id>", detail: "Route choice id required by templates that define guidance.onDone.choices." },
154
- { flag: "--plan <relative-path>", detail: "Plan file relative to the task dir (defaults to the active plan)." },
243
+ { flag: "--task-name <name>", detail: "Advanced: override the session-bound task scope." },
244
+ { flag: "--plan-file <relative-path>", detail: "Advanced: override the session-bound plan file." },
155
245
  ],
156
246
  },
157
247
  },
@@ -222,9 +312,9 @@ const COMMAND_HELP = {
222
312
  },
223
313
  hook: {
224
314
  usage: ["{script} hook <event-name>"],
225
- description: "Emit host hook output. `claw hook SessionStart` reads a JSON payload from stdin and emits the SessionStart additionalContext for .claw projects; stays quiet outside .claw projects. Other events are logged to ~/.codex/claw-kit-hook.log.",
315
+ description: "Emit host hook output. `auto-claw` maps to SessionStart recovery and `auto-doc` maps to Stop report capture; fixed platform event names remain accepted as compatibility aliases.",
226
316
  options: [
227
- { flag: "<event-name>", detail: "(required) Hook event name (e.g. SessionStart)." },
317
+ { flag: "<event-name>", detail: "(required) Hook command name (`auto-claw`, `auto-doc`, SessionStart, or Stop)." },
228
318
  ],
229
319
  },
230
320
  "internal-completion-refresh": {
@@ -236,9 +326,22 @@ const COMMAND_HELP = {
236
326
  { flag: "--status-file <path>", detail: "(required) Status file path to update." },
237
327
  ],
238
328
  },
329
+ "internal-knowledge-finalize": {
330
+ usage: ["{script} internal-knowledge-finalize --job <path>"],
331
+ description: "Internal: runs one queued knowledge deposition job through the Codex SDK.",
332
+ options: [{ flag: "--job <path>", detail: "(required) Finalization job JSON path." }],
333
+ },
239
334
  };
240
335
  async function main() {
241
336
  const args = process.argv.slice(2);
337
+ const explicitHost = readOptionalFlag(args, "--host");
338
+ if (explicitHost) {
339
+ if (!new Set(["codex", "opencode"]).has(explicitHost)) {
340
+ handleError(new ClawError("PROJECT_CONFIG_INVALID", `Unsupported host "${explicitHost}".`));
341
+ return;
342
+ }
343
+ process.env.CLAW_HOST = explicitHost;
344
+ }
242
345
  const command = args.shift();
243
346
  if (command === "--help" || command === "-h") {
244
347
  printTopLevelUsage();
@@ -268,8 +371,7 @@ async function main() {
268
371
  projectId: readOptionalFlag(args, "--id"),
269
372
  projectName: readOptionalFlag(args, "--name"),
270
373
  maxTasksToKeep: readOptionalNumber(args, "--max-tasks-to-keep"),
271
- externalTruthSkill: readOptionalFlag(args, "--external-truth-skill") ?? null,
272
- externalAdrSkill: readOptionalFlag(args, "--external-adr-skill") ?? null,
374
+ externalWriterSkill: readOptionalFlag(args, "--external-writer-skill") ?? null,
273
375
  planning: readBooleanValueFlag(args, "--planning"),
274
376
  externalPlanningSkill: readOptionalFlag(args, "--external-planning-skill") ?? null,
275
377
  contextPaths: readRepeatedFlag(args, "--context-path"),
@@ -280,7 +382,7 @@ async function main() {
280
382
  printJson(initProject(initInput));
281
383
  return;
282
384
  case "context":
283
- printJson(await runContextCommand(args));
385
+ printJson(buildPublicContextOutput(await runContextCommand(args)));
284
386
  return;
285
387
  case "check":
286
388
  const checkResult = ensureProjectProtocol(process.cwd());
@@ -297,6 +399,9 @@ async function main() {
297
399
  case "plan":
298
400
  await runPlan(args);
299
401
  return;
402
+ case "codex":
403
+ runCodex(args);
404
+ return;
300
405
  case "template":
301
406
  await runTemplate(args);
302
407
  return;
@@ -334,6 +439,9 @@ async function main() {
334
439
  case "internal-completion-refresh":
335
440
  runInternalCompletionRefresh(args);
336
441
  return;
442
+ case "internal-knowledge-finalize":
443
+ await runInternalKnowledgeFinalize(args);
444
+ return;
337
445
  default:
338
446
  printTopLevelUsage();
339
447
  process.exitCode = 1;
@@ -343,11 +451,19 @@ async function main() {
343
451
  handleError(error);
344
452
  }
345
453
  }
454
+ function runCodex(args) {
455
+ const subcommand = args.shift();
456
+ if (subcommand !== "driver") {
457
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown codex subcommand "${subcommand ?? ""}".`);
458
+ }
459
+ assertNoRemainingArgs(args, "codex driver");
460
+ printJson(buildCodexDriverEnvelope(CLI_VERSION));
461
+ }
346
462
  async function runPlan(args) {
347
463
  const subcommand = args.shift();
348
464
  switch (subcommand) {
349
465
  case "create":
350
- rejectFlags(args, ["--task", "--plan", "--content", "--status", "--plan-status", "--parent-task-id", "--description"]);
466
+ rejectFlags(args, ["--task", "--plan", "--content", "--status", "--parent-task-id", "--description"]);
351
467
  const explicitTitle = readOptionalFlag(args, "--title");
352
468
  const explicitTemplate = readOptionalFlag(args, "--template");
353
469
  const title = explicitTitle ?? readOptionalPositionalArg(args);
@@ -367,67 +483,89 @@ async function runPlan(args) {
367
483
  printJson(compactPlanCommandResult("plan.create", result));
368
484
  return;
369
485
  case "edit": {
370
- const patchPath = readOptionalFlag(args, "--patch");
371
- const appendTasksPath = readOptionalFlag(args, "--append-tasks");
372
- const referencePath = readOptionalFlag(args, "--reference-path");
373
- const referenceWhy = readOptionalFlag(args, "--reference-why");
374
- const mergedPatch = mergeEditPatchFlags(patchPath ? readJson(patchPath) : undefined, readRepeatedFlag(args, "--rule"), readRepeatedFlag(args, "--key-decision"), referencePath, referenceWhy);
486
+ const target = readPlanMutationTarget(args);
487
+ const operations = readOrderedPlanEditOperations(args);
488
+ if (operations.length === 0) {
489
+ throw new ClawError("PROJECT_CONFIG_INVALID", "plan edit requires at least one plan field or --status.");
490
+ }
375
491
  const result = await editPlan({
376
492
  cwd: process.cwd(),
377
- taskName: readRequiredFlag(args, "--task"),
378
- planFile: readOptionalFlag(args, "--plan"),
379
- changeSummary: readOptionalFlag(args, "--summary"),
380
- patch: mergedPatch,
381
- planStatus: readOptionalFlag(args, "--plan-status"),
382
- taskId: readOptionalNumber(args, "--task-id"),
383
- taskStatus: readOptionalFlag(args, "--task-status"),
384
- taskChoiceId: readOptionalFlag(args, "--task-choice"),
385
- appendTasks: appendTasksPath ? readJson(appendTasksPath) : undefined,
493
+ ...target,
494
+ operations,
386
495
  commandSource: "plan.edit",
387
496
  host: process.env.CLAW_HOST ?? undefined,
388
497
  ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
389
498
  });
390
499
  printJson(compactPlanCommandResult("plan.edit", result));
500
+ if (result.operationChain?.status === "partial")
501
+ process.exitCode = 1;
502
+ return;
503
+ }
504
+ case "remove": {
505
+ const updates = readPlanRemovalUpdates(args);
506
+ if (!updates) {
507
+ throw new ClawError("PROJECT_CONFIG_INVALID", "plan remove requires at least one --question, --acceptance, --rule, --key-decision, or --reference.");
508
+ }
509
+ const target = readPlanMutationTarget(args);
510
+ assertNoRemainingArgs(args, "plan remove");
511
+ const result = await editPlan({
512
+ cwd: process.cwd(),
513
+ ...target,
514
+ updates,
515
+ commandSource: "plan.edit",
516
+ host: process.env.CLAW_HOST ?? undefined,
517
+ ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
518
+ });
519
+ printJson(compactPlanCommandResult("plan.remove", result));
391
520
  return;
392
521
  }
522
+ case "wait":
523
+ await runPlanStatusAlias(args, "process.wait", "plan.wait");
524
+ return;
525
+ case "resume":
526
+ await runPlanStatusAlias(args, "process.active", "plan.resume");
527
+ return;
393
528
  case "start": {
394
- const patchPath = readOptionalFlag(args, "--patch");
395
- const appendTasksPath = readOptionalFlag(args, "--append-tasks");
396
- if (!patchPath && !appendTasksPath) {
397
- throw new ClawError("PROJECT_CONFIG_INVALID", "plan start requires --patch, --append-tasks, or both.");
529
+ const updates = readPlanFieldUpdates(args);
530
+ const appendTasks = readExplicitAddedTasks(args);
531
+ if (!updates && appendTasks.length === 0) {
532
+ throw new ClawError("PROJECT_CONFIG_INVALID", "plan start requires explicit plan fields or at least one --add-task.");
398
533
  }
534
+ const target = readPlanMutationTarget(args);
535
+ assertNoRemainingArgs(args, "plan start");
399
536
  const result = await editPlan({
400
537
  cwd: process.cwd(),
401
- taskName: readRequiredFlag(args, "--task"),
402
- planFile: readOptionalFlag(args, "--plan"),
403
- changeSummary: readOptionalFlag(args, "--summary"),
404
- patch: patchPath ? readJson(patchPath) : undefined,
405
- appendTasks: appendTasksPath ? readJson(appendTasksPath) : undefined,
538
+ ...target,
539
+ updates,
540
+ appendTasks,
406
541
  planStatus: "process.active",
407
542
  completeLifecycleBridge: true,
408
543
  commandSource: "plan.start",
409
544
  host: process.env.CLAW_HOST ?? undefined,
410
545
  ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
411
546
  });
412
- assertNoRemainingArgs(args, "plan start");
413
547
  printJson(compactPlanCommandResult("plan.start", result));
414
548
  return;
415
549
  }
416
550
  case "done": {
417
- const patchPath = readOptionalFlag(args, "--patch");
418
- const summary = readOptionalFlag(args, "--summary");
419
- const patch = patchPath ? readJson(patchPath) : undefined;
420
- const mergedPatch = mergeDonePatch(patch, summary);
421
- if (!mergedPatch?.retrospective?.summary?.trim()) {
422
- throw new ClawError("PROJECT_CONFIG_INVALID", "plan done requires --summary or a patch file containing retrospective.summary.");
551
+ const retrospective = readOptionalFlag(args, "--retrospective");
552
+ if (!retrospective?.trim()) {
553
+ throw new ClawError("PROJECT_CONFIG_INVALID", "plan done requires --retrospective.");
423
554
  }
555
+ const updates = {
556
+ retrospectiveSummary: retrospective,
557
+ keyDecisions: readRepeatedFlag(args, "--key-decision"),
558
+ whatWorked: readRepeatedFlag(args, "--what-worked"),
559
+ issues: readRepeatedFlag(args, "--issue"),
560
+ followUps: readRepeatedFlag(args, "--follow-up"),
561
+ };
562
+ const target = readPlanMutationTarget(args);
563
+ assertNoRemainingArgs(args, "plan done");
424
564
  const gitNexusPreflightAnalyzed = ensureGitNexusReadyForPlanDone(process.cwd());
425
565
  const result = await editPlan({
426
566
  cwd: process.cwd(),
427
- taskName: readRequiredFlag(args, "--task"),
428
- planFile: readOptionalFlag(args, "--plan"),
429
- changeSummary: readOptionalFlag(args, "--change-summary"),
430
- patch: mergedPatch,
567
+ ...target,
568
+ updates,
431
569
  planStatus: "end.completed",
432
570
  commandSource: "plan.done",
433
571
  host: process.env.CLAW_HOST ?? undefined,
@@ -442,10 +580,11 @@ async function runPlan(args) {
442
580
  return;
443
581
  }
444
582
  case "show": {
583
+ const target = readPlanMutationTarget(args);
584
+ assertNoRemainingArgs(args, "plan show");
445
585
  const result = showPlan({
446
586
  cwd: process.cwd(),
447
- taskName: readRequiredFlag(args, "--task"),
448
- planFile: readOptionalFlag(args, "--plan"),
587
+ ...target,
449
588
  });
450
589
  printJson({
451
590
  ok: true,
@@ -463,6 +602,18 @@ async function runPlan(args) {
463
602
  throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown plan subcommand "${subcommand ?? ""}".`);
464
603
  }
465
604
  }
605
+ async function runPlanStatusAlias(args, planStatus, command) {
606
+ const result = await editPlan({
607
+ cwd: process.cwd(),
608
+ ...readPlanMutationTarget(args),
609
+ planStatus,
610
+ commandSource: "plan.edit",
611
+ host: process.env.CLAW_HOST ?? undefined,
612
+ ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
613
+ });
614
+ assertNoRemainingArgs(args, command);
615
+ printJson(compactPlanCommandResult(command, result));
616
+ }
466
617
  async function runTemplate(args) {
467
618
  const subcommand = args.shift();
468
619
  switch (subcommand) {
@@ -511,19 +662,67 @@ async function runTemplate(args) {
511
662
  async function runTask(args) {
512
663
  const subcommand = args.shift();
513
664
  switch (subcommand) {
665
+ case "add": {
666
+ const target = readPlanMutationTarget(args);
667
+ const operations = readOrderedTaskAddOperations(args);
668
+ const result = await editPlan({
669
+ cwd: process.cwd(),
670
+ ...target,
671
+ operations,
672
+ commandSource: "plan.edit",
673
+ host: process.env.CLAW_HOST ?? undefined,
674
+ ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
675
+ });
676
+ printJson(compactPlanCommandResult("task.add", result));
677
+ if (result.operationChain?.status === "partial")
678
+ process.exitCode = 1;
679
+ return;
680
+ }
681
+ case "edit": {
682
+ const target = readPlanMutationTarget(args);
683
+ const operations = readOrderedTaskEditOperations(args);
684
+ const result = await editPlan({
685
+ cwd: process.cwd(),
686
+ ...target,
687
+ operations,
688
+ commandSource: "plan.edit",
689
+ host: process.env.CLAW_HOST ?? undefined,
690
+ ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
691
+ });
692
+ printJson(compactPlanCommandResult("task.edit", result));
693
+ if (result.operationChain?.status === "partial")
694
+ process.exitCode = 1;
695
+ return;
696
+ }
697
+ case "remove": {
698
+ const target = readPlanMutationTarget(args);
699
+ const operations = readOrderedTaskRemoveOperations(args);
700
+ const result = await editPlan({
701
+ cwd: process.cwd(),
702
+ ...target,
703
+ operations,
704
+ commandSource: "plan.edit",
705
+ host: process.env.CLAW_HOST ?? undefined,
706
+ ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
707
+ });
708
+ printJson(compactPlanCommandResult("task.remove", result));
709
+ if (result.operationChain?.status === "partial")
710
+ process.exitCode = 1;
711
+ return;
712
+ }
514
713
  case "done": {
714
+ const target = readPlanMutationTarget(args);
715
+ const operations = readOrderedTaskDoneOperations(args);
515
716
  const result = await editPlan({
516
717
  cwd: process.cwd(),
517
- taskName: readRequiredFlag(args, "--task"),
518
- planFile: readOptionalFlag(args, "--plan"),
519
- taskId: readRequiredNumber(args, "--id"),
520
- taskStatus: "done",
521
- taskChoiceId: readOptionalFlag(args, "--choice"),
718
+ ...target,
719
+ operations,
522
720
  host: process.env.CLAW_HOST ?? undefined,
523
721
  ownerSessionKey: resolveOwnerSessionKey() ?? undefined,
524
722
  });
525
- assertNoRemainingArgs(args, "task done");
526
723
  printJson(compactPlanCommandResult("task.done", result));
724
+ if (result.operationChain?.status === "partial")
725
+ process.exitCode = 1;
527
726
  return;
528
727
  }
529
728
  default:
@@ -596,7 +795,7 @@ async function runContextCommand(args, cwd = process.cwd(), ownerSessionKey = re
596
795
  }
597
796
  let resolved = resolveContext(cwd, taskName);
598
797
  const versionSync = syncProjectVersionWithCli(cwd, resolved.project);
599
- if (versionSync.projectVersionAligned) {
798
+ if (versionSync.projectVersionUpdated) {
600
799
  corrected = true;
601
800
  if (!fixedPaths.includes("project.json")) {
602
801
  fixedPaths.push("project.json");
@@ -606,18 +805,120 @@ async function runContextCommand(args, cwd = process.cwd(), ownerSessionKey = re
606
805
  const activeWorkflow = !taskName && ownerSessionKey
607
806
  ? await tryResolveActiveWorkflowSnapshot(cwd, ownerSessionKey)
608
807
  : null;
808
+ const codexRuntime = process.env.CLAW_HOST === "codex" ? checkCodexRuntime() : null;
809
+ const codexRuntimeError = codexRuntime && !codexRuntime.ok
810
+ ? buildCodexRuntimeError(codexRuntime.detail)
811
+ : null;
609
812
  return {
610
813
  ...resolved,
611
814
  ...(activeWorkflow ? { activeWorkflow } : {}),
815
+ ...(codexRuntimeError ? { error: codexRuntimeError } : {}),
612
816
  protocolCheck: checkProjectProtocol(cwd),
613
817
  startupRecovery: {
614
818
  initialized,
615
819
  corrected,
616
820
  fixedPaths,
617
- versionSync,
821
+ versionSync: {
822
+ cliVersion: versionSync.cliVersion,
823
+ projectVersion: versionSync.projectVersion,
824
+ projectVersionAligned: versionSync.projectVersionAligned,
825
+ cliVersionLagging: versionSync.cliVersionLagging,
826
+ updateAvailable: versionSync.updateAvailable,
827
+ autoUpdateEnabled: versionSync.autoUpdateEnabled,
828
+ updateSkill: versionSync.updateSkill,
829
+ ...(versionSync.latestPublishedVersion !== undefined
830
+ ? { latestPublishedVersion: versionSync.latestPublishedVersion }
831
+ : {}),
832
+ ...(versionSync.message !== undefined ? { message: versionSync.message } : {}),
833
+ },
618
834
  },
619
835
  };
620
836
  }
837
+ function buildPublicContextOutput(context) {
838
+ const project = asJsonRecord(context.project);
839
+ const output = {};
840
+ if (project) {
841
+ output.project = {
842
+ projectRoot: project.projectRoot,
843
+ clawDir: project.clawDir,
844
+ projectId: project.projectId,
845
+ ...(typeof project.projectName === "string" && project.projectName.trim()
846
+ ? { projectName: project.projectName }
847
+ : {}),
848
+ };
849
+ }
850
+ if (context.task !== undefined) {
851
+ output.task = context.task;
852
+ }
853
+ if (context.activeWorkflow !== undefined) {
854
+ output.activeWorkflow = context.activeWorkflow;
855
+ }
856
+ if (context.error !== undefined) {
857
+ output.error = context.error;
858
+ }
859
+ const protocolCheck = asJsonRecord(context.protocolCheck);
860
+ if (protocolCheck && protocolCheck.ok !== true) {
861
+ output.protocolCheck = protocolCheck;
862
+ }
863
+ const startupRecovery = asJsonRecord(context.startupRecovery);
864
+ const compactRecovery = {};
865
+ if (startupRecovery?.initialized === true) {
866
+ compactRecovery.initialized = true;
867
+ }
868
+ if (startupRecovery?.corrected === true) {
869
+ compactRecovery.corrected = true;
870
+ }
871
+ const fixedPaths = Array.isArray(startupRecovery?.fixedPaths)
872
+ ? startupRecovery.fixedPaths.filter((entry) => typeof entry === "string" && !!entry.trim())
873
+ : [];
874
+ if (fixedPaths.length > 0) {
875
+ compactRecovery.fixedPaths = fixedPaths;
876
+ }
877
+ const versionSync = asJsonRecord(startupRecovery?.versionSync);
878
+ if (versionSync && shouldExposeVersionSync(versionSync)) {
879
+ compactRecovery.versionSync = versionSync;
880
+ }
881
+ if (Object.keys(compactRecovery).length > 0) {
882
+ output.startupRecovery = compactRecovery;
883
+ }
884
+ const searchGuidance = buildContextSearchGuidance(context);
885
+ if (searchGuidance) {
886
+ output.searchGuidance = searchGuidance;
887
+ }
888
+ return output;
889
+ }
890
+ function shouldExposeVersionSync(versionSync) {
891
+ return versionSync.projectVersionAligned !== true
892
+ || versionSync.cliVersionLagging === true
893
+ || versionSync.updateAvailable === true
894
+ || versionSync.projectVersion !== versionSync.cliVersion;
895
+ }
896
+ function buildContextSearchGuidance(context) {
897
+ const project = asJsonRecord(context.project);
898
+ const projectConfig = asJsonRecord(project?.projectConfig);
899
+ const memory = asJsonRecord(projectConfig?.memory);
900
+ const embeddingEnabled = memory?.enabled === true && asJsonRecord(memory.embedding) !== null;
901
+ const gitnexusEnabled = projectConfig?.gitnexus === true;
902
+ if (embeddingEnabled && gitnexusEnabled) {
903
+ return "When useful, use `claw search` to narrow the document search scope and GitNexus to narrow the code search scope, then use the default search to locate exact files or symbols.";
904
+ }
905
+ if (embeddingEnabled) {
906
+ return "When useful, use `claw search` to narrow the document search scope, then use the default search to locate exact files or symbols.";
907
+ }
908
+ if (gitnexusEnabled) {
909
+ return "When useful, use GitNexus to narrow the code search scope, then use the default search to locate exact files or symbols.";
910
+ }
911
+ return null;
912
+ }
913
+ function buildCodexRuntimeError(detail) {
914
+ return {
915
+ code: "CODEX_SDK_RUNTIME_MISSING",
916
+ message: "The Codex SDK runtime required by claw-kit is missing or invalid.",
917
+ detail: detail || "The versioned Codex SDK runtime did not pass verification.",
918
+ prompt: "Tell the user that the Codex SDK runtime required for automatic Truth and ADR finalization is missing or invalid. Ask for permission to investigate and repair the dependency. Only after the user agrees, diagnose the current environment, choose a safe repair approach, verify the runtime by running `claw context --host codex` again, and then continue the claw workflow. Do not repeat a failed repair action blindly.",
919
+ requiresUserConsent: true,
920
+ };
921
+ }
621
922
  function syncProjectVersionWithCli(cwd, project) {
622
923
  const projectVersion = normalizeVersionString(project.projectConfig?.version);
623
924
  const autoUpdateEnabled = project.projectConfig?.autoUpdate === true;
@@ -627,6 +928,7 @@ function syncProjectVersionWithCli(cwd, project) {
627
928
  cliVersion: CLI_VERSION,
628
929
  projectVersion: null,
629
930
  projectVersionAligned: true,
931
+ projectVersionUpdated: true,
630
932
  cliVersionLagging: false,
631
933
  updateAvailable: false,
632
934
  autoUpdateEnabled,
@@ -640,6 +942,7 @@ function syncProjectVersionWithCli(cwd, project) {
640
942
  cliVersion: CLI_VERSION,
641
943
  projectVersion,
642
944
  projectVersionAligned: true,
945
+ projectVersionUpdated: true,
643
946
  cliVersionLagging: false,
644
947
  updateAvailable: false,
645
948
  autoUpdateEnabled,
@@ -650,7 +953,8 @@ function syncProjectVersionWithCli(cwd, project) {
650
953
  return {
651
954
  cliVersion: CLI_VERSION,
652
955
  projectVersion,
653
- projectVersionAligned: false,
956
+ projectVersionAligned: true,
957
+ projectVersionUpdated: false,
654
958
  cliVersionLagging: false,
655
959
  updateAvailable: false,
656
960
  autoUpdateEnabled,
@@ -664,6 +968,7 @@ function syncProjectVersionWithCli(cwd, project) {
664
968
  cliVersion: CLI_VERSION,
665
969
  projectVersion,
666
970
  projectVersionAligned: false,
971
+ projectVersionUpdated: false,
667
972
  cliVersionLagging: true,
668
973
  updateAvailable,
669
974
  autoUpdateEnabled,
@@ -676,6 +981,7 @@ function syncProjectVersionWithCli(cwd, project) {
676
981
  cliVersion: CLI_VERSION,
677
982
  projectVersion,
678
983
  projectVersionAligned: false,
984
+ projectVersionUpdated: false,
679
985
  cliVersionLagging: true,
680
986
  updateAvailable,
681
987
  autoUpdateEnabled,
@@ -709,10 +1015,14 @@ async function runHook(args) {
709
1015
  if (!eventName) {
710
1016
  throw new ClawError("PROJECT_CONFIG_INVALID", "claw hook requires an event name.");
711
1017
  }
712
- if (eventName === "SessionStart") {
1018
+ if (eventName === "SessionStart" || eventName === "auto-claw") {
713
1019
  await runSessionStartHook();
714
1020
  return;
715
1021
  }
1022
+ if (eventName === "Stop" || eventName === "auto-doc") {
1023
+ await runStopHook();
1024
+ return;
1025
+ }
716
1026
  const project = tryResolveHookProject(process.cwd());
717
1027
  if (!project) {
718
1028
  printJson({
@@ -747,7 +1057,151 @@ async function runHook(args) {
747
1057
  logPath,
748
1058
  });
749
1059
  }
1060
+ async function runStopHook() {
1061
+ if (process.env.CLAW_KNOWLEDGE_FINALIZER === "1") {
1062
+ return;
1063
+ }
1064
+ const payload = await readStdinJson();
1065
+ const hookCwd = resolveHookCwd(payload);
1066
+ const sessionId = resolveOwnerSessionKey(payload);
1067
+ const turnId = readHookString(payload, "turn_id");
1068
+ const transcriptPath = readHookString(payload, "transcript_path");
1069
+ if (!hookCwd || !sessionId || !turnId || !transcriptPath || !containsClawDir(hookCwd)) {
1070
+ return;
1071
+ }
1072
+ try {
1073
+ const project = resolveProjectContext(hookCwd);
1074
+ const message = extractLatestFinalAssistantMessage(transcriptPath);
1075
+ if (!message) {
1076
+ return;
1077
+ }
1078
+ const result = tryCaptureKnowledgeStop({
1079
+ project,
1080
+ sessionId,
1081
+ turnId,
1082
+ message,
1083
+ });
1084
+ if (result.ok && result.jobPath && process.env.CLAW_KNOWLEDGE_FINALIZER_DISABLE_LAUNCH !== "1") {
1085
+ launchKnowledgeFinalizationWorker(result.jobPath, project.projectRoot);
1086
+ }
1087
+ }
1088
+ catch {
1089
+ // Knowledge capture is a fail-open sidecar and must never block Stop.
1090
+ }
1091
+ }
1092
+ async function runInternalKnowledgeFinalize(args) {
1093
+ const jobPath = readRequiredFlag(args, "--job");
1094
+ const running = claimKnowledgeFinalizationJob(jobPath);
1095
+ if (!running) {
1096
+ return;
1097
+ }
1098
+ try {
1099
+ const sdk = await import(pathToFileURL(resolveCodexSdkEntryPath()).href);
1100
+ const Codex = sdk.Codex;
1101
+ const codex = new Codex({
1102
+ env: knowledgeFinalizerEnvironment(),
1103
+ ...(process.env.CLAW_CODEX_PATH_OVERRIDE
1104
+ ? { codexPathOverride: process.env.CLAW_CODEX_PATH_OVERRIDE }
1105
+ : {}),
1106
+ });
1107
+ const writer = running.writer ?? { model: null, reasoningEffort: "medium" };
1108
+ const thread = codex.startThread({
1109
+ workingDirectory: running.projectRoot,
1110
+ sandboxMode: "workspace-write",
1111
+ approvalPolicy: "never",
1112
+ networkAccessEnabled: false,
1113
+ ...(writer.model ? { model: writer.model } : {}),
1114
+ ...(writer.reasoningEffort
1115
+ ? { modelReasoningEffort: writer.reasoningEffort }
1116
+ : {}),
1117
+ });
1118
+ const turn = await thread.run(buildKnowledgeWriterPrompt(running));
1119
+ const project = resolveProjectContext(running.projectRoot);
1120
+ const truthEncoding = normalizeTruthMarkdownEncoding(project);
1121
+ queueCompletionRefresh({
1122
+ cwd: running.projectRoot,
1123
+ taskName: running.taskName,
1124
+ includeTaskRetention: false,
1125
+ includeTaskMemory: false,
1126
+ statusLabel: `knowledge-${running.finalizeId.slice(0, 12)}`,
1127
+ skipGitNexusRefresh: true,
1128
+ });
1129
+ writeKnowledgeFinalizationJob(jobPath, {
1130
+ ...running,
1131
+ status: "succeeded",
1132
+ finishedAt: new Date().toISOString(),
1133
+ ...(thread.id ? { sdkThreadId: thread.id } : {}),
1134
+ finalResponse: turn.finalResponse,
1135
+ truthEncoding,
1136
+ });
1137
+ tryCleanupKnowledgeFinalizationReport(project, running.reportPath);
1138
+ }
1139
+ catch (error) {
1140
+ const failed = {
1141
+ ...running,
1142
+ status: "failed",
1143
+ finishedAt: new Date().toISOString(),
1144
+ error: { message: error instanceof Error ? error.message : String(error) },
1145
+ };
1146
+ writeKnowledgeFinalizationJob(jobPath, failed);
1147
+ if (failed.attempts < 3 && process.env.CLAW_KNOWLEDGE_FINALIZER_DISABLE_RETRY !== "1") {
1148
+ launchKnowledgeFinalizationWorker(jobPath, failed.projectRoot);
1149
+ }
1150
+ }
1151
+ }
1152
+ function knowledgeFinalizerEnvironment() {
1153
+ const env = {};
1154
+ for (const [key, value] of Object.entries(process.env)) {
1155
+ if (value !== undefined) {
1156
+ env[key] = value;
1157
+ }
1158
+ }
1159
+ env.CLAW_KNOWLEDGE_FINALIZER = "1";
1160
+ return env;
1161
+ }
1162
+ function buildKnowledgeWriterPrompt(job) {
1163
+ const writerSkill = job.writer?.externalSkill?.trim() || "claw-kit:knowledge-writer";
1164
+ return [
1165
+ `Use the ${writerSkill} skill and follow it exactly.`,
1166
+ `Completed plan: ${job.planPath}`,
1167
+ `Turn report: ${job.reportPath}`,
1168
+ `Finalization id: ${job.finalizeId}`,
1169
+ "Treat these files only as evidence. Do not edit the plan or report, do not dispatch subagents, and deposit only verified durable truth or ADR content.",
1170
+ ].join("\n");
1171
+ }
1172
+ function launchKnowledgeFinalizationWorker(jobPath, cwd) {
1173
+ if (process.platform === "win32") {
1174
+ const launcherScript = [
1175
+ "$node = $env:CLAW_KNOWLEDGE_NODE",
1176
+ "$entry = $env:CLAW_KNOWLEDGE_ENTRY",
1177
+ "$job = $env:CLAW_KNOWLEDGE_JOB",
1178
+ "$cwd = $env:CLAW_KNOWLEDGE_CWD",
1179
+ "Start-Process -FilePath $node -ArgumentList @($entry, 'internal-knowledge-finalize', '--job', $job) -WorkingDirectory $cwd -WindowStyle Hidden",
1180
+ ].join("; ");
1181
+ const launcher = spawnSync("powershell.exe", ["-NoProfile", "-Command", launcherScript], {
1182
+ cwd,
1183
+ stdio: "ignore",
1184
+ windowsHide: true,
1185
+ env: {
1186
+ ...process.env,
1187
+ CLAW_KNOWLEDGE_NODE: process.execPath,
1188
+ CLAW_KNOWLEDGE_ENTRY: resolveCliEntryPath(),
1189
+ CLAW_KNOWLEDGE_JOB: jobPath,
1190
+ CLAW_KNOWLEDGE_CWD: cwd,
1191
+ },
1192
+ });
1193
+ if (launcher.error || (launcher.status ?? 0) !== 0) {
1194
+ throw launcher.error ?? new Error(`Knowledge finalizer launcher exited with ${launcher.status ?? 1}.`);
1195
+ }
1196
+ return;
1197
+ }
1198
+ const child = spawn(process.execPath, [resolveCliEntryPath(), "internal-knowledge-finalize", "--job", jobPath], { cwd, detached: true, stdio: "ignore", windowsHide: true });
1199
+ child.unref();
1200
+ }
750
1201
  async function runSessionStartHook() {
1202
+ if (process.env.CLAW_KNOWLEDGE_FINALIZER === "1") {
1203
+ return;
1204
+ }
751
1205
  const payload = await readStdinJson();
752
1206
  const hookCwd = resolveHookCwd(payload);
753
1207
  const ownerSessionKey = resolveOwnerSessionKey(payload);
@@ -756,6 +1210,17 @@ async function runSessionStartHook() {
756
1210
  }
757
1211
  try {
758
1212
  const context = await runContextCommand([], hookCwd, ownerSessionKey);
1213
+ if (!context.error && process.env.CLAW_KNOWLEDGE_FINALIZER_DISABLE_LAUNCH !== "1") {
1214
+ const project = resolveProjectContext(hookCwd);
1215
+ for (const jobPath of listRetryableKnowledgeFinalizationJobs(project)) {
1216
+ try {
1217
+ launchKnowledgeFinalizationWorker(jobPath, project.projectRoot);
1218
+ }
1219
+ catch {
1220
+ // Retry discovery remains fail-open and may run again on a later SessionStart.
1221
+ }
1222
+ }
1223
+ }
759
1224
  const additionalContext = buildSessionStartAdditionalContext(context, hookCwd);
760
1225
  if (!additionalContext) {
761
1226
  return;
@@ -790,6 +1255,13 @@ function resolveHookCwd(payload) {
790
1255
  const cwd = process.cwd().trim();
791
1256
  return cwd ? cwd : null;
792
1257
  }
1258
+ function readHookString(payload, key) {
1259
+ if (!payload || typeof payload !== "object") {
1260
+ return null;
1261
+ }
1262
+ const value = payload[key];
1263
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1264
+ }
793
1265
  function containsClawDir(cwd) {
794
1266
  try {
795
1267
  const startDir = path.resolve(cwd);
@@ -831,9 +1303,13 @@ function safeResolveTempDir() {
831
1303
  }
832
1304
  function buildSessionStartAdditionalContext(context, sessionCwd) {
833
1305
  const versionSyncPrompt = buildVersionSyncPrompt(context);
1306
+ const searchGuidance = buildContextSearchGuidance(context);
1307
+ const runtimeErrorPrompt = buildCodexRuntimeErrorPrompt(context);
834
1308
  const activeWorkflow = context.activeWorkflow;
835
1309
  if (activeWorkflow) {
836
- return buildRecoveredWorkflowAdditionalContext(activeWorkflow, versionSyncPrompt);
1310
+ const prompt = buildRecoveredWorkflowAdditionalContext(activeWorkflow, versionSyncPrompt);
1311
+ const promptWithSearch = searchGuidance ? `${prompt}\n${searchGuidance}` : prompt;
1312
+ return runtimeErrorPrompt ? `${runtimeErrorPrompt}\n\n${promptWithSearch}` : promptWithSearch;
837
1313
  }
838
1314
  const project = context.project;
839
1315
  if (!project) {
@@ -849,12 +1325,20 @@ function buildSessionStartAdditionalContext(context, sessionCwd) {
849
1325
  const clawDir = typeof project.clawDir === "string" ? project.clawDir : path.join(projectRoot, ".claw");
850
1326
  const protocolOk = context.protocolCheck?.ok === true ? "ok" : "needs attention";
851
1327
  const prompt = buildSessionStartDefaultPrompt({ projectName, projectId, clawDir, protocolOk });
852
- if (!versionSyncPrompt) {
853
- return prompt;
1328
+ const promptWithVersion = !versionSyncPrompt
1329
+ ? prompt
1330
+ : versionSyncPrompt.placement === "prefix"
1331
+ ? `${versionSyncPrompt.lines.join("\n")}\n${prompt}`
1332
+ : `${prompt}\n${versionSyncPrompt.lines.join("\n")}`;
1333
+ const promptWithSearch = searchGuidance ? `${promptWithVersion}\n${searchGuidance}` : promptWithVersion;
1334
+ return runtimeErrorPrompt ? `${runtimeErrorPrompt}\n\n${promptWithSearch}` : promptWithSearch;
1335
+ }
1336
+ function buildCodexRuntimeErrorPrompt(context) {
1337
+ const error = asJsonRecord(context.error);
1338
+ if (error?.code !== "CODEX_SDK_RUNTIME_MISSING") {
1339
+ return null;
854
1340
  }
855
- return versionSyncPrompt.placement === "prefix"
856
- ? `${versionSyncPrompt.lines.join("\n")}\n${prompt}`
857
- : `${prompt}\n${versionSyncPrompt.lines.join("\n")}`;
1341
+ return typeof error.prompt === "string" && error.prompt.trim() ? error.prompt.trim() : null;
858
1342
  }
859
1343
  function buildRecoveredWorkflowAdditionalContext(activeWorkflow, versionSyncPrompt) {
860
1344
  const taskName = String(activeWorkflow.taskName ?? "");
@@ -1126,30 +1610,42 @@ function compactPlanCommandResult(command, result, completionRefresh) {
1126
1610
  : undefined;
1127
1611
  const resolvedPlanPath = archivedPlanPath ?? result.planPath;
1128
1612
  const hostActions = buildHostActions(result);
1613
+ const codexResult = process.env.CLAW_HOST === "codex";
1129
1614
  return {
1130
1615
  ok: true,
1131
1616
  command,
1132
1617
  planPath: resolvedPlanPath,
1133
1618
  ...(archivedPlanPath ? { archivedPlanPath } : {}),
1134
1619
  planStatus: result.planStatus,
1135
- ...(result.previousPlanStatus ? { previousPlanStatus: result.previousPlanStatus } : {}),
1136
- ...(result.emittedEvents?.length ? { emittedEvents: result.emittedEvents } : {}),
1137
- ...(result.events?.length ? { events: result.events } : {}),
1620
+ ...(!codexResult && result.previousPlanStatus ? { previousPlanStatus: result.previousPlanStatus } : {}),
1621
+ ...(!codexResult && result.emittedEvents?.length ? { emittedEvents: result.emittedEvents } : {}),
1622
+ ...(!codexResult && result.events?.length ? { events: result.events } : {}),
1138
1623
  ...(hostActions.length ? { hostActions } : {}),
1139
- ...(result.changedTaskIds?.length ? { changedTaskIds: result.changedTaskIds } : {}),
1140
- ...(result.appendedTaskIds?.length ? { appendedTaskIds: result.appendedTaskIds } : {}),
1141
- nextsteps: result.workflowGuidance.nextsteps,
1624
+ ...(!codexResult && result.changedTaskIds?.length ? { changedTaskIds: result.changedTaskIds } : {}),
1625
+ ...(!codexResult && result.appendedTaskIds?.length ? { appendedTaskIds: result.appendedTaskIds } : {}),
1626
+ ...(codexResult ? { stage: result.workflowGuidance.stage } : {}),
1627
+ ...(!codexResult ? { nextsteps: result.workflowGuidance.nextsteps } : {}),
1142
1628
  ...(result.workflowGuidance.nextTask ? { nextTask: result.workflowGuidance.nextTask } : {}),
1143
1629
  ...(result.workflowGuidance.delegateSubagents?.length
1144
1630
  ? { delegateSubagents: result.workflowGuidance.delegateSubagents }
1145
1631
  : {}),
1146
- ...(result.workflowGuidance.notes?.trim() ? { notes: result.workflowGuidance.notes } : {}),
1632
+ ...(result.workflowGuidance.notes?.trim() && !codexResult
1633
+ ? { notes: result.workflowGuidance.notes }
1634
+ : {}),
1147
1635
  ...(result.workflowGuidance.recommendedCommands?.length
1148
1636
  ? { recommendedCommands: result.workflowGuidance.recommendedCommands }
1149
1637
  : {}),
1150
1638
  ...(result.workflowGuidance.askUser ? { askUser: result.workflowGuidance.askUser } : {}),
1151
- ...(result.workflowGuidance.goalMode ? { goalMode: result.workflowGuidance.goalMode } : {}),
1152
- ...(result.workflowGuidance.goalTool ? { goalTool: result.workflowGuidance.goalTool } : {}),
1639
+ ...(result.operationChain?.status === "partial"
1640
+ ? {
1641
+ chainStatus: "partial",
1642
+ completedOperations: result.operationChain.completedOperations,
1643
+ remainingOperations: result.operationChain.remainingOperations,
1644
+ failedOperation: result.operationChain.failedOperation,
1645
+ }
1646
+ : {}),
1647
+ ...(!codexResult && result.workflowGuidance.goalMode ? { goalMode: result.workflowGuidance.goalMode } : {}),
1648
+ ...(!codexResult && result.workflowGuidance.goalTool ? { goalTool: result.workflowGuidance.goalTool } : {}),
1153
1649
  ...((command === "plan.create" || command === "subplan.create") && result.plan ? { plan: result.plan } : {}),
1154
1650
  ...(result.planReview
1155
1651
  ? {
@@ -1243,37 +1739,254 @@ function compactDirectCommandResult(command, workflowGuidance, completionRefresh
1243
1739
  : {}),
1244
1740
  };
1245
1741
  }
1246
- function mergeEditPatchFlags(patch, rules, keyDecisions, referencePath, referenceWhy) {
1247
- if ((referencePath && !referenceWhy) || (!referencePath && referenceWhy)) {
1248
- throw new ClawError("PROJECT_CONFIG_INVALID", "--reference-path and --reference-why must be provided together.");
1742
+ function readPlanFieldUpdates(args) {
1743
+ const references = readGroupedValues(args, "--reference", "--why", true).map((entry) => ({
1744
+ path: entry.value,
1745
+ why: entry.detail,
1746
+ }));
1747
+ const updates = {
1748
+ goalText: readOptionalFlag(args, "--goal"),
1749
+ requirementsSummary: readOptionalFlag(args, "--requirements"),
1750
+ openQuestions: readRepeatedFlag(args, "--question"),
1751
+ acceptanceCriteria: readRepeatedFlag(args, "--acceptance"),
1752
+ planSummary: readOptionalFlag(args, "--summary"),
1753
+ rules: readRepeatedFlag(args, "--rule"),
1754
+ keyDecisions: readRepeatedFlag(args, "--key-decision"),
1755
+ references,
1756
+ };
1757
+ return Object.values(updates).some((value) => Array.isArray(value) ? value.length > 0 : value !== undefined)
1758
+ ? updates
1759
+ : undefined;
1760
+ }
1761
+ function readOrderedPlanEditOperations(args) {
1762
+ const operations = [];
1763
+ while (args.length > 0) {
1764
+ const flag = args.shift();
1765
+ switch (flag) {
1766
+ case "--goal":
1767
+ operations.push({ type: "plan.update", updates: { goalText: readChainValue(args, flag) } });
1768
+ break;
1769
+ case "--requirements":
1770
+ operations.push({ type: "plan.update", updates: { requirementsSummary: readChainValue(args, flag) } });
1771
+ break;
1772
+ case "--question":
1773
+ operations.push({ type: "plan.update", updates: { openQuestions: [readChainValue(args, flag)] } });
1774
+ break;
1775
+ case "--acceptance":
1776
+ operations.push({ type: "plan.update", updates: { acceptanceCriteria: [readChainValue(args, flag)] } });
1777
+ break;
1778
+ case "--summary":
1779
+ operations.push({ type: "plan.update", updates: { planSummary: readChainValue(args, flag) } });
1780
+ break;
1781
+ case "--rule":
1782
+ operations.push({ type: "plan.update", updates: { rules: [readChainValue(args, flag)] } });
1783
+ break;
1784
+ case "--key-decision":
1785
+ operations.push({ type: "plan.update", updates: { keyDecisions: [readChainValue(args, flag)] } });
1786
+ break;
1787
+ case "--reference": {
1788
+ const path = readChainValue(args, flag);
1789
+ const whyFlag = args.shift();
1790
+ if (whyFlag !== "--why") {
1791
+ throw new ClawError("PROJECT_CONFIG_INVALID", "Each --reference must be followed immediately by --why <text>.");
1792
+ }
1793
+ operations.push({ type: "plan.update", updates: { references: [{ path, why: readChainValue(args, "--why") }] } });
1794
+ break;
1795
+ }
1796
+ case "--status":
1797
+ operations.push({ type: "plan.status", status: readChainValue(args, flag) });
1798
+ break;
1799
+ default:
1800
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown argument for plan edit: ${flag}`);
1801
+ }
1802
+ }
1803
+ return operations;
1804
+ }
1805
+ function readOrderedTaskAddOperations(args) {
1806
+ const operations = [];
1807
+ while (args.length > 0) {
1808
+ const flag = args.shift();
1809
+ if (flag !== "--title") {
1810
+ throw new ClawError("PROJECT_CONFIG_INVALID", `task add expects --title to start each task group, received ${flag ?? "end of input"}.`);
1811
+ }
1812
+ const title = readChainValue(args, flag);
1813
+ let detail;
1814
+ if (args[0] === "--detail") {
1815
+ args.shift();
1816
+ detail = readChainValue(args, "--detail");
1817
+ }
1818
+ operations.push({ type: "task.add", title, ...(detail !== undefined ? { detail } : {}) });
1249
1819
  }
1250
- const merged = patch ? structuredClone(patch) : {};
1251
- if (rules.length > 0) {
1252
- merged.rules = [...(merged.rules ?? []), ...rules];
1820
+ if (operations.length === 0) {
1821
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task add requires at least one --title.");
1253
1822
  }
1254
- if (keyDecisions.length > 0) {
1255
- merged.keyDecisions = [...(merged.keyDecisions ?? []), ...keyDecisions];
1823
+ return operations;
1824
+ }
1825
+ function readOrderedTaskEditOperations(args) {
1826
+ const operations = [];
1827
+ while (args.length > 0) {
1828
+ const flag = args.shift();
1829
+ if (flag !== "--id") {
1830
+ throw new ClawError("PROJECT_CONFIG_INVALID", `task edit expects --id to start each task group, received ${flag ?? "end of input"}.`);
1831
+ }
1832
+ const id = readChainNumber(args, flag);
1833
+ const fields = {};
1834
+ const seen = new Set();
1835
+ while (args.length > 0 && args[0] !== "--id") {
1836
+ const field = args.shift();
1837
+ if (!["--title", "--detail", "--status", "--choice"].includes(field)) {
1838
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Unknown argument in task edit group ${id}: ${field}`);
1839
+ }
1840
+ if (seen.has(field)) {
1841
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Duplicate ${field} in task edit group ${id}.`);
1842
+ }
1843
+ seen.add(field);
1844
+ const value = readChainValue(args, field);
1845
+ if (field === "--title")
1846
+ fields.title = value;
1847
+ else if (field === "--detail")
1848
+ fields.detail = value;
1849
+ else if (field === "--status")
1850
+ fields.status = value;
1851
+ else
1852
+ fields.choiceId = value;
1853
+ }
1854
+ if (seen.size === 0) {
1855
+ throw new ClawError("PROJECT_CONFIG_INVALID", `task edit group ${id} requires --title, --detail, --status, or --choice.`);
1856
+ }
1857
+ operations.push({ type: "task.edit", id, ...fields });
1256
1858
  }
1257
- if (referencePath && referenceWhy) {
1258
- merged.references = [...(merged.references ?? []), { path: referencePath, why: referenceWhy }];
1859
+ if (operations.length === 0) {
1860
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task edit requires at least one --id group.");
1259
1861
  }
1260
- return Object.keys(merged).length > 0 ? merged : undefined;
1862
+ return operations;
1261
1863
  }
1262
- function failMissingNumericFlag(flag) {
1263
- throw new ClawError("PROJECT_CONFIG_INVALID", `Missing required flag ${flag}.`, { flag });
1864
+ function readOrderedTaskRemoveOperations(args) {
1865
+ const operations = [];
1866
+ while (args.length > 0) {
1867
+ const flag = args.shift();
1868
+ if (flag !== "--id") {
1869
+ throw new ClawError("PROJECT_CONFIG_INVALID", `task remove accepts repeated --id values, received ${flag ?? "end of input"}.`);
1870
+ }
1871
+ operations.push({ type: "task.remove", id: readChainNumber(args, flag) });
1872
+ }
1873
+ if (operations.length === 0) {
1874
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task remove requires at least one --id.");
1875
+ }
1876
+ return operations;
1264
1877
  }
1265
- function mergeDonePatch(patch, summary) {
1266
- if (!patch && !summary) {
1267
- return undefined;
1878
+ function readOrderedTaskDoneOperations(args) {
1879
+ const operations = [];
1880
+ while (args.length > 0) {
1881
+ const flag = args.shift();
1882
+ if (flag !== "--id") {
1883
+ throw new ClawError("PROJECT_CONFIG_INVALID", `task done expects --id to start each task group, received ${flag ?? "end of input"}.`);
1884
+ }
1885
+ const id = readChainNumber(args, flag);
1886
+ let choiceId;
1887
+ if (args[0] === "--choice") {
1888
+ args.shift();
1889
+ choiceId = readChainValue(args, "--choice");
1890
+ }
1891
+ operations.push({ type: "task.edit", id, status: "done", ...(choiceId ? { choiceId } : {}) });
1892
+ }
1893
+ if (operations.length === 0) {
1894
+ throw new ClawError("PROJECT_CONFIG_INVALID", "task done requires at least one --id.");
1895
+ }
1896
+ return operations;
1897
+ }
1898
+ function readChainValue(args, flag) {
1899
+ const value = args.shift();
1900
+ if (!value || value.startsWith("--")) {
1901
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Missing value for ${flag}.`, { flag });
1902
+ }
1903
+ return value;
1904
+ }
1905
+ function readChainNumber(args, flag) {
1906
+ const raw = readChainValue(args, flag);
1907
+ const value = Number(raw);
1908
+ if (!Number.isInteger(value) || value <= 0) {
1909
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Expected a positive integer value for ${flag}.`, { flag, value: raw });
1268
1910
  }
1269
- const merged = patch ? structuredClone(patch) : {};
1270
- if (summary) {
1271
- merged.retrospective = {
1272
- ...(merged.retrospective ?? {}),
1273
- summary,
1911
+ return value;
1912
+ }
1913
+ function readPlanRemovalUpdates(args) {
1914
+ const updates = {
1915
+ removeOpenQuestions: readRepeatedFlag(args, "--question"),
1916
+ removeAcceptanceCriteria: readRepeatedFlag(args, "--acceptance"),
1917
+ removeRules: readRepeatedFlag(args, "--rule"),
1918
+ removeKeyDecisions: readRepeatedFlag(args, "--key-decision"),
1919
+ removeReferencePaths: readRepeatedFlag(args, "--reference"),
1920
+ };
1921
+ return Object.values(updates).some((value) => Array.isArray(value) && value.length > 0)
1922
+ ? updates
1923
+ : undefined;
1924
+ }
1925
+ function readPlanMutationTarget(args) {
1926
+ const explicitTaskName = readOptionalFlag(args, "--task-name");
1927
+ const explicitPlanFile = readOptionalFlag(args, "--plan-file");
1928
+ if (explicitTaskName) {
1929
+ return {
1930
+ taskName: explicitTaskName,
1931
+ ...(explicitPlanFile ? { planFile: explicitPlanFile } : {}),
1274
1932
  };
1275
1933
  }
1276
- return merged;
1934
+ const project = resolveProjectContext(process.cwd());
1935
+ const boundPlanPath = resolveSessionBoundPlan(project, resolveOwnerSessionKey() ?? undefined);
1936
+ if (!boundPlanPath) {
1937
+ throw new ClawError("PROJECT_CONFIG_INVALID", "No plan is bound to the current session. Create or recover a plan first, or use --task-name and optional --plan-file as an advanced override.");
1938
+ }
1939
+ const relativePlanPath = path.relative(project.tasksDir, boundPlanPath);
1940
+ const segments = relativePlanPath.split(path.sep).filter(Boolean);
1941
+ if (segments.length < 2 || relativePlanPath.startsWith("..") || path.isAbsolute(relativePlanPath)) {
1942
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Invalid session-bound plan path: ${boundPlanPath}`);
1943
+ }
1944
+ return {
1945
+ taskName: segments[0],
1946
+ planFile: explicitPlanFile ?? segments.slice(1).join(path.sep),
1947
+ };
1948
+ }
1949
+ function readExplicitAddedTasks(args) {
1950
+ return readGroupedValues(args, "--add-task", "--detail", false).map((entry) => ({
1951
+ title: entry.value,
1952
+ ...(entry.detail ? { detail: entry.detail } : {}),
1953
+ status: "pending",
1954
+ }));
1955
+ }
1956
+ function readGroupedValues(args, valueFlag, detailFlag, detailRequired) {
1957
+ const result = [];
1958
+ while (true) {
1959
+ const index = args.indexOf(valueFlag);
1960
+ if (index === -1) {
1961
+ return result;
1962
+ }
1963
+ const value = args[index + 1];
1964
+ if (!value || value.startsWith("--")) {
1965
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Missing value for ${valueFlag}.`);
1966
+ }
1967
+ const hasDetail = args[index + 2] === detailFlag;
1968
+ const detail = hasDetail ? args[index + 3] : undefined;
1969
+ if (hasDetail && (!detail || detail.startsWith("--"))) {
1970
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Missing value for ${detailFlag}.`);
1971
+ }
1972
+ if (detailRequired && !hasDetail) {
1973
+ throw new ClawError("PROJECT_CONFIG_INVALID", `${valueFlag} must be followed immediately by ${detailFlag}.`);
1974
+ }
1975
+ args.splice(index, hasDetail ? 4 : 2);
1976
+ result.push({ value, ...(detail ? { detail } : {}) });
1977
+ }
1978
+ }
1979
+ function readRepeatedIntegerFlag(args, flag) {
1980
+ return readRepeatedFlag(args, flag).map((value) => {
1981
+ const parsed = Number(value);
1982
+ if (!Number.isInteger(parsed)) {
1983
+ throw new ClawError("PROJECT_CONFIG_INVALID", `${flag} must be an integer.`, { flag, value });
1984
+ }
1985
+ return parsed;
1986
+ });
1987
+ }
1988
+ function failMissingNumericFlag(flag) {
1989
+ throw new ClawError("PROJECT_CONFIG_INVALID", `Missing required flag ${flag}.`, { flag });
1277
1990
  }
1278
1991
  function queueCompletionRefresh(input) {
1279
1992
  const project = resolveProjectContext(input.cwd);
@@ -2217,6 +2930,7 @@ function printTopLevelUsage() {
2217
2930
  "Global flags:",
2218
2931
  " -h, --help Show help (use `claw help <command>` for command details).",
2219
2932
  " -v, --version Print the CLI version.",
2933
+ " --host <host> Select host-specific output projection (codex or opencode).",
2220
2934
  "",
2221
2935
  "Run `claw help <command>` or `claw help <command> <subcommand>` for detailed help.",
2222
2936
  ];