cclaw-cli 0.51.23 → 0.51.25

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.
Files changed (42) hide show
  1. package/README.md +135 -414
  2. package/dist/artifact-linter.js +10 -6
  3. package/dist/config.d.ts +1 -1
  4. package/dist/config.js +28 -3
  5. package/dist/content/core-agents.d.ts +128 -2
  6. package/dist/content/core-agents.js +291 -13
  7. package/dist/content/examples.js +21 -10
  8. package/dist/content/next-command.js +10 -6
  9. package/dist/content/reference-patterns.d.ts +18 -0
  10. package/dist/content/reference-patterns.js +391 -0
  11. package/dist/content/seed-shelf.js +73 -8
  12. package/dist/content/skills.js +39 -34
  13. package/dist/content/stage-common-guidance.js +19 -3
  14. package/dist/content/stage-schema.d.ts +12 -0
  15. package/dist/content/stage-schema.js +224 -24
  16. package/dist/content/stages/_lint-metadata/index.js +3 -2
  17. package/dist/content/stages/brainstorm.js +27 -18
  18. package/dist/content/stages/design.js +27 -18
  19. package/dist/content/stages/review.js +20 -9
  20. package/dist/content/stages/schema-types.d.ts +9 -2
  21. package/dist/content/stages/scope.js +21 -10
  22. package/dist/content/stages/ship.js +3 -2
  23. package/dist/content/stages/tdd.js +18 -13
  24. package/dist/content/start-command.js +3 -2
  25. package/dist/content/status-command.js +9 -4
  26. package/dist/content/subagents.js +336 -38
  27. package/dist/content/templates.js +182 -25
  28. package/dist/delegation.d.ts +2 -0
  29. package/dist/delegation.js +27 -6
  30. package/dist/doctor.js +167 -25
  31. package/dist/flow-state.d.ts +1 -0
  32. package/dist/flow-state.js +1 -0
  33. package/dist/gate-evidence.js +25 -2
  34. package/dist/install.js +72 -8
  35. package/dist/internal/advance-stage.js +179 -26
  36. package/dist/knowledge-store.js +30 -6
  37. package/dist/run-archive.js +11 -0
  38. package/dist/run-persistence.js +35 -10
  39. package/dist/tdd-verification-evidence.d.ts +17 -0
  40. package/dist/tdd-verification-evidence.js +43 -0
  41. package/dist/types.d.ts +10 -0
  42. package/package.json +1 -1
package/dist/doctor.js CHANGED
@@ -24,6 +24,7 @@ import { resolveTrackFromPrompt } from "./track-heuristics.js";
24
24
  import { classifyCodexHooksFlag, codexConfigPath, readCodexConfig } from "./codex-feature-flag.js";
25
25
  import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LEGACY_LANGUAGE_RULE_PACK_FOLDERS } from "./content/utility-skills.js";
26
26
  import { validateHookDocument } from "./hook-schema.js";
27
+ import { HOOK_EVENTS_BY_HARNESS } from "./content/hook-events.js";
27
28
  import { validateKnowledgeEntry } from "./knowledge-store.js";
28
29
  import { readSeedShelf } from "./content/seed-shelf.js";
29
30
  import { evaluateRetroGate } from "./retro-gate.js";
@@ -130,6 +131,20 @@ async function generatedCliEntrypointsOk(projectRoot) {
130
131
  : "local CLI entrypoint check skipped because generated hook scripts are absent"
131
132
  };
132
133
  }
134
+ function expectedArtifactPrefix(stage) {
135
+ const index = FLOW_STAGES.indexOf(stage);
136
+ return `${String(index + 1).padStart(2, "0")}-`;
137
+ }
138
+ function artifactStageFromFileName(fileName) {
139
+ if (!fileName.endsWith(".md"))
140
+ return null;
141
+ for (const stage of FLOW_STAGES) {
142
+ if (fileName.startsWith(expectedArtifactPrefix(stage))) {
143
+ return stage;
144
+ }
145
+ }
146
+ return null;
147
+ }
133
148
  function extractUserPromptFromIdeaArtifact(markdown) {
134
149
  const normalized = markdown.replace(/\r\n?/gu, "\n");
135
150
  const heading = /^##\s+User prompt\s*$/imu.exec(normalized);
@@ -295,8 +310,12 @@ function opencodeConfigCandidates(projectRoot) {
295
310
  return [
296
311
  path.join(projectRoot, "opencode.json"),
297
312
  path.join(projectRoot, "opencode.jsonc"),
313
+ path.join(projectRoot, "oh-my-opencode.jsonc"),
314
+ path.join(projectRoot, "oh-my-openagent.jsonc"),
298
315
  path.join(projectRoot, ".opencode/opencode.json"),
299
- path.join(projectRoot, ".opencode/opencode.jsonc")
316
+ path.join(projectRoot, ".opencode/opencode.jsonc"),
317
+ path.join(projectRoot, ".opencode/oh-my-opencode.jsonc"),
318
+ path.join(projectRoot, ".opencode/oh-my-openagent.jsonc")
300
319
  ];
301
320
  }
302
321
  function openCodeConfigRegistersPlugin(parsed) {
@@ -360,6 +379,87 @@ function opencodeQuestionEnvCheck() {
360
379
  details: "Set OPENCODE_ENABLE_QUESTION_TOOL=1 for OpenCode ACP clients so permission-gated structured questions can use the question tool."
361
380
  };
362
381
  }
382
+ function codexFlagInactiveDetail(configPath, state, error) {
383
+ if (state === "enabled") {
384
+ return `codex_hooks feature flag is enabled in ${configPath}; Codex hooks are active.`;
385
+ }
386
+ if (state === "read-error") {
387
+ return `Codex hooks are inactive: could not read ${configPath} (${error instanceof Error ? error.message : String(error)}).`;
388
+ }
389
+ if (state === "missing-file") {
390
+ return `Codex hooks are inactive: ${configPath} does not exist; .codex/hooks.json is ignored until [features] codex_hooks = true is configured.`;
391
+ }
392
+ if (state === "missing-section") {
393
+ return `Codex hooks are inactive: ${configPath} has no [features] section; add codex_hooks = true to activate configured hooks.`;
394
+ }
395
+ if (state === "missing-key") {
396
+ return `Codex hooks are inactive: ${configPath} is missing codex_hooks under [features]; add codex_hooks = true to activate configured hooks.`;
397
+ }
398
+ return `Codex hooks are inactive: ${configPath} sets codex_hooks to a non-true value; set codex_hooks = true under [features].`;
399
+ }
400
+ function hookCommandsWithMatchers(value) {
401
+ if (!Array.isArray(value)) {
402
+ return [];
403
+ }
404
+ const out = [];
405
+ for (const item of value) {
406
+ const obj = toObject(item);
407
+ if (!obj)
408
+ continue;
409
+ const matcher = typeof obj.matcher === "string" ? obj.matcher : undefined;
410
+ if (typeof obj.command === "string") {
411
+ out.push({ command: obj.command, matcher });
412
+ }
413
+ const nested = hookCommandsWithMatchers(obj.hooks);
414
+ for (const child of nested) {
415
+ out.push({ ...child, matcher: child.matcher ?? matcher });
416
+ }
417
+ }
418
+ return out;
419
+ }
420
+ function commandHasHandler(entries, handler) {
421
+ return entries.some((entry) => entry.command.includes(`run-hook.cmd ${handler}`) || entry.command.includes(`run-hook.mjs ${handler}`));
422
+ }
423
+ function codexBashOnly(entries, handler) {
424
+ const matches = entries.filter((entry) => entry.command.includes(`run-hook.cmd ${handler}`) || entry.command.includes(`run-hook.mjs ${handler}`));
425
+ return matches.length > 0 && matches.every((entry) => entry.matcher === "Bash|bash");
426
+ }
427
+ function codexStructuralWiringCheck(codexHooks) {
428
+ const problems = [];
429
+ const expectedSession = HOOK_EVENTS_BY_HARNESS.codex.session_rehydrate;
430
+ if (expectedSession !== "SessionStart matcher=startup|resume") {
431
+ problems.push("semantic session_rehydrate mapping must remain SessionStart matcher=startup|resume");
432
+ }
433
+ const session = hookCommandsWithMatchers(codexHooks.SessionStart);
434
+ if (!commandHasHandler(session, "session-start") || !session.some((entry) => entry.matcher === "startup|resume")) {
435
+ problems.push("SessionStart must run session-start with matcher startup|resume");
436
+ }
437
+ const userPrompt = hookCommandsWithMatchers(codexHooks.UserPromptSubmit);
438
+ if (!commandHasHandler(userPrompt, "prompt-guard")) {
439
+ problems.push("UserPromptSubmit must run prompt-guard");
440
+ }
441
+ if (!commandHasHandler(userPrompt, "verify-current-state")) {
442
+ problems.push("UserPromptSubmit must run verify-current-state");
443
+ }
444
+ const pre = hookCommandsWithMatchers(codexHooks.PreToolUse);
445
+ if (!codexBashOnly(pre, "prompt-guard")) {
446
+ problems.push("PreToolUse prompt-guard must be Bash-only matcher Bash|bash");
447
+ }
448
+ if (!codexBashOnly(pre, "workflow-guard")) {
449
+ problems.push("PreToolUse workflow-guard must be Bash-only matcher Bash|bash");
450
+ }
451
+ const post = hookCommandsWithMatchers(codexHooks.PostToolUse);
452
+ if (!codexBashOnly(post, "context-monitor")) {
453
+ problems.push("PostToolUse context-monitor must be Bash-only matcher Bash|bash");
454
+ }
455
+ const stop = hookCommandsWithMatchers(codexHooks.Stop);
456
+ if (!commandHasHandler(stop, "stop-handoff")) {
457
+ problems.push("Stop must run stop-handoff");
458
+ }
459
+ return problems.length === 0
460
+ ? { ok: true, details: "Codex hook events, matchers, and manifest semantic mappings are structurally valid" }
461
+ : { ok: false, details: problems.join("; ") };
462
+ }
363
463
  async function initRecoveryCheck(projectRoot) {
364
464
  const sentinelPath = path.join(projectRoot, RUNTIME_ROOT, "state", ".init-in-progress");
365
465
  if (!(await exists(sentinelPath))) {
@@ -1003,34 +1103,48 @@ export async function doctorChecks(projectRoot, options = {}) {
1003
1103
  ok: codexWiringOk,
1004
1104
  details: `${codexHooksFile} must wire SessionStart, UserPromptSubmit(prompt/verify-current-state), Bash-only PreToolUse(prompt/workflow), Bash-only PostToolUse(context-monitor), and Stop(stop-handoff). Codex workflow-guard is intentionally strict Bash-only.`
1005
1105
  });
1006
- // Feature flag warning: Codex ignores `.codex/hooks.json` unless the
1007
- // user has `[features] codex_hooks = true` in `~/.codex/config.toml`.
1008
- // Advisory warning — not a hard failure, because the skills still
1009
- // work without the flag.
1106
+ const codexStructural = codexStructuralWiringCheck(codexHooks);
1107
+ checks.push({
1108
+ name: "hook:wiring:codex:structure",
1109
+ ok: codexStructural.ok,
1110
+ details: codexStructural.details
1111
+ });
1112
+ // Codex ignores `.codex/hooks.json` unless the user has
1113
+ // `[features] codex_hooks = true` in `~/.codex/config.toml`.
1010
1114
  const codexConfig = codexConfigPath();
1011
- let featureFlagNote = "";
1115
+ let codexFlagState = "read-error";
1116
+ let codexFlagReadError;
1012
1117
  try {
1013
1118
  const content = await readCodexConfig(codexConfig);
1014
- const state = classifyCodexHooksFlag(content);
1015
- featureFlagNote =
1016
- state === "enabled"
1017
- ? `codex_hooks feature flag is enabled in ${codexConfig}`
1018
- : state === "missing-file"
1019
- ? `warning: ${codexConfig} does not exist; .codex/hooks.json will be ignored until you create it with \`[features]\\ncodex_hooks = true\\n\`.`
1020
- : state === "missing-section"
1021
- ? `warning: ${codexConfig} has no [features] section; add \`[features]\\ncodex_hooks = true\\n\` to enable cclaw hooks.`
1022
- : state === "missing-key"
1023
- ? `warning: ${codexConfig} is missing the codex_hooks key under [features]. Add \`codex_hooks = true\` to enable cclaw hooks.`
1024
- : `warning: ${codexConfig} sets codex_hooks to a non-true value; set \`codex_hooks = true\` under [features] to enable cclaw hooks.`;
1119
+ codexFlagState = classifyCodexHooksFlag(content);
1025
1120
  }
1026
1121
  catch (err) {
1027
- featureFlagNote = `warning: could not read ${codexConfig}: ${err instanceof Error ? err.message : String(err)}`;
1122
+ codexFlagReadError = err;
1028
1123
  }
1124
+ const featureFlagNote = codexFlagInactiveDetail(codexConfig, codexFlagState, codexFlagReadError);
1125
+ const featureFlagOk = codexFlagState === "enabled";
1029
1126
  checks.push({
1030
1127
  name: "warning:codex:feature_flag",
1031
- ok: true,
1032
- details: featureFlagNote
1128
+ ok: featureFlagOk,
1129
+ details: featureFlagNote,
1130
+ summary: featureFlagOk
1131
+ ? "Codex hooks are active."
1132
+ : "Codex hooks are inactive; configured hooks will be ignored.",
1133
+ fix: "Set `[features] codex_hooks = true` in the Codex config or run cclaw init/sync with Codex flag repair.",
1134
+ docRef: "docs/harnesses.md"
1033
1135
  });
1136
+ if (parsedConfig?.strictness === "strict") {
1137
+ checks.push({
1138
+ name: "hook:codex:feature_flag_active",
1139
+ ok: featureFlagOk,
1140
+ details: featureFlagNote,
1141
+ summary: featureFlagOk
1142
+ ? "Codex hooks are active for strict runtime enforcement."
1143
+ : "Codex hooks are inactive; strict Codex hook enforcement is not ready.",
1144
+ fix: "Set `[features] codex_hooks = true` in the Codex config so strict Codex hooks can run.",
1145
+ docRef: "docs/harnesses.md"
1146
+ });
1147
+ }
1034
1148
  // Legacy `.codex/commands/*` must not linger from older cclaw installs.
1035
1149
  // (The `.codex/hooks.json` path is now managed and is validated above,
1036
1150
  // so there is no longer a legacy_hooks_json warning.)
@@ -1530,13 +1644,21 @@ export async function doctorChecks(projectRoot, options = {}) {
1530
1644
  });
1531
1645
  const artifactsRoot = path.join(projectRoot, RUNTIME_ROOT, "artifacts");
1532
1646
  let artifactPlaceholderHits = [];
1647
+ let duplicateArtifactGroups = [];
1533
1648
  if (await exists(artifactsRoot)) {
1534
1649
  try {
1535
1650
  const entries = await fs.readdir(artifactsRoot, { withFileTypes: true });
1536
1651
  const placeholderPattern = /\b(?:TODO|TBD|FIXME)\b|<fill-in>|<your-.*-here>/giu;
1652
+ const stageArtifactFiles = new Map();
1537
1653
  for (const entry of entries) {
1538
1654
  if (!entry.isFile() || !entry.name.endsWith(".md"))
1539
1655
  continue;
1656
+ const stageForArtifact = artifactStageFromFileName(entry.name);
1657
+ if (stageForArtifact) {
1658
+ const files = stageArtifactFiles.get(stageForArtifact) ?? [];
1659
+ files.push(entry.name);
1660
+ stageArtifactFiles.set(stageForArtifact, files);
1661
+ }
1540
1662
  const filePath = path.join(artifactsRoot, entry.name);
1541
1663
  const content = await fs.readFile(filePath, "utf8");
1542
1664
  const matchCount = (content.match(placeholderPattern) ?? []).length;
@@ -1544,9 +1666,13 @@ export async function doctorChecks(projectRoot, options = {}) {
1544
1666
  artifactPlaceholderHits.push(`${entry.name}:${matchCount}`);
1545
1667
  }
1546
1668
  }
1669
+ duplicateArtifactGroups = [...stageArtifactFiles.entries()]
1670
+ .filter(([, files]) => files.length > 1)
1671
+ .map(([stageName, files]) => `${stageName}: ${files.sort().join(", ")}`);
1547
1672
  }
1548
1673
  catch {
1549
1674
  artifactPlaceholderHits = [];
1675
+ duplicateArtifactGroups = [];
1550
1676
  }
1551
1677
  }
1552
1678
  checks.push({
@@ -1556,13 +1682,20 @@ export async function doctorChecks(projectRoot, options = {}) {
1556
1682
  ? "no TODO/TBD/FIXME placeholder markers found in active artifacts"
1557
1683
  : `warning: placeholder markers detected in active artifacts (${artifactPlaceholderHits.join(", ")}). Clear before marking completion.`
1558
1684
  });
1685
+ checks.push({
1686
+ name: "warning:artifacts:duplicate_stage_artifacts",
1687
+ ok: duplicateArtifactGroups.length === 0,
1688
+ details: duplicateArtifactGroups.length === 0
1689
+ ? "no duplicate stage artifacts detected in active artifacts"
1690
+ : `warning: duplicate stage artifacts detected (${duplicateArtifactGroups.join("; ")}). The resolver uses the newest matching file; archive or rename stale copies to avoid ambiguous operator handoff.`
1691
+ });
1559
1692
  const staleStages = Object.keys(flowState.staleStages).filter((value) => FLOW_STAGES.includes(value));
1560
1693
  checks.push({
1561
1694
  name: "state:stale_stages_resolved",
1562
1695
  ok: staleStages.length === 0,
1563
1696
  details: staleStages.length === 0
1564
1697
  ? "no stale stages pending acknowledgement"
1565
- : `stale stages pending acknowledgement: ${staleStages.join(", ")}`
1698
+ : `stale stages pending acknowledgement: ${staleStages.join(", ")}. Re-run the current stale stage, then clear it with cclaw internal rewind --ack ${flowState.currentStage}.`
1566
1699
  });
1567
1700
  const retroGateStatus = await evaluateRetroGate(projectRoot, flowState);
1568
1701
  checks.push({
@@ -1632,18 +1765,27 @@ export async function doctorChecks(projectRoot, options = {}) {
1632
1765
  ok: archiveIntegrity.ok,
1633
1766
  details: archiveIntegrity.details
1634
1767
  });
1768
+ const currentGateState = flowState.stageGateCatalog[flowState.currentStage];
1769
+ const currentStageUntouched = flowState.completedStages.length === 0 &&
1770
+ flowState.rewinds.length === 0 &&
1771
+ Object.keys(flowState.guardEvidence).length === 0 &&
1772
+ (currentGateState?.passed.length ?? 0) === 0 &&
1773
+ (currentGateState?.blocked.length ?? 0) === 0;
1635
1774
  const delegation = await checkMandatoryDelegations(projectRoot, flowState.currentStage, {
1636
1775
  repairFeatureSystem: false
1637
1776
  });
1777
+ const delegationSatisfiedForDoctor = currentStageUntouched || delegation.satisfied;
1638
1778
  const missingEvidenceNote = delegation.missingEvidence && delegation.missingEvidence.length > 0
1639
1779
  ? ` (role-switch rows without evidenceRefs: ${delegation.missingEvidence.join(", ")})`
1640
1780
  : "";
1641
1781
  checks.push({
1642
1782
  name: "delegation:mandatory:current_stage",
1643
- ok: delegation.satisfied,
1644
- details: delegation.satisfied
1645
- ? `All mandatory delegations satisfied for stage "${flowState.currentStage}" (mode: ${delegation.expectedMode})`
1646
- : `Missing mandatory delegations for stage "${flowState.currentStage}": ${delegation.missing.join(", ")}${missingEvidenceNote}`
1783
+ ok: delegationSatisfiedForDoctor,
1784
+ details: currentStageUntouched
1785
+ ? `mandatory delegation check deferred for untouched stage "${flowState.currentStage}"; stage-complete enforces it when work begins`
1786
+ : delegation.satisfied
1787
+ ? `All mandatory delegations satisfied for stage "${flowState.currentStage}" (mode: ${delegation.expectedMode})`
1788
+ : `Missing mandatory delegations for stage "${flowState.currentStage}": ${delegation.missing.join(", ")}${missingEvidenceNote}`
1647
1789
  });
1648
1790
  checks.push({
1649
1791
  name: "warning:delegation:waived",
@@ -63,6 +63,7 @@ export interface CloseoutState {
63
63
  retroSkipReason?: string;
64
64
  compoundCompletedAt?: string;
65
65
  compoundSkipped?: boolean;
66
+ compoundSkipReason?: string;
66
67
  compoundPromoted: number;
67
68
  }
68
69
  export declare function createInitialCloseoutState(): CloseoutState;
@@ -42,6 +42,7 @@ export function createInitialCloseoutState() {
42
42
  retroSkipReason: undefined,
43
43
  compoundCompletedAt: undefined,
44
44
  compoundSkipped: undefined,
45
+ compoundSkipReason: undefined,
45
46
  compoundPromoted: 0
46
47
  };
47
48
  }
@@ -9,6 +9,7 @@ import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
9
9
  import { detectPublicApiChanges } from "./internal/detect-public-api-changes.js";
10
10
  import { readFlowState, writeFlowState } from "./runs.js";
11
11
  import { parseTddCycleLog, validateTddCycleOrder } from "./tdd-cycle.js";
12
+ import { validateTddVerificationEvidence } from "./tdd-verification-evidence.js";
12
13
  import { buildTraceMatrix } from "./trace-matrix.js";
13
14
  import { FLOW_STAGES } from "./types.js";
14
15
  async function currentStageArtifactExists(projectRoot, stage, track) {
@@ -36,6 +37,22 @@ async function readArtifactMarkdown(projectRoot, artifactFile) {
36
37
  }
37
38
  return null;
38
39
  }
40
+ async function readStageArtifactMarkdown(projectRoot, stage, track) {
41
+ const resolved = await resolveArtifactPath(stage, {
42
+ projectRoot,
43
+ track,
44
+ intent: "read"
45
+ });
46
+ if (!(await exists(resolved.absPath))) {
47
+ return null;
48
+ }
49
+ try {
50
+ return await fs.readFile(resolved.absPath, "utf8");
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
39
56
  function unique(values) {
40
57
  return [...new Set(values)];
41
58
  }
@@ -263,6 +280,12 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
263
280
  issues.push(`passed gate "${gateId}" is missing guardEvidence entry.`);
264
281
  continue;
265
282
  }
283
+ if (stage === "tdd" && gateId === "tdd_verified_before_complete") {
284
+ const verification = await validateTddVerificationEvidence(projectRoot, evidence);
285
+ if (!verification.ok) {
286
+ issues.push(`tdd verification gate blocked (${gateId}): ${verification.issues.join(" ")}`);
287
+ }
288
+ }
266
289
  const discoveredCommandIssue = await verifyDiscoveredCommandEvidence(projectRoot, stage, gateId, flowState);
267
290
  if (discoveredCommandIssue) {
268
291
  issues.push(discoveredCommandIssue);
@@ -336,7 +359,7 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
336
359
  if (stage === "design") {
337
360
  const researchGateRequired = schema.requiredGates.some((gate) => gate.id === "design_research_complete" && gate.tier === "required");
338
361
  if (researchGateRequired) {
339
- const designMarkdown = await readArtifactMarkdown(projectRoot, "03-design.md");
362
+ const designMarkdown = await readStageArtifactMarkdown(projectRoot, "design", flowState.track);
340
363
  const inlineResearchBody = designMarkdown
341
364
  ? extractMarkdownSectionBody(designMarkdown, "Research Fleet Synthesis")
342
365
  : null;
@@ -354,7 +377,7 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
354
377
  const inlineResearchComplete = inlineResearchLines.length > 0;
355
378
  const researchMarkdown = await readArtifactMarkdown(projectRoot, "02a-research.md");
356
379
  if (!inlineResearchComplete && !researchMarkdown) {
357
- issues.push("design research gate blocked (design_research_complete): fill `Research Fleet Synthesis` in `.cclaw/artifacts/03-design.md`, or write `.cclaw/artifacts/02a-research.md` for deep/high-risk research.");
380
+ issues.push("design research gate blocked (design_research_complete): fill `Research Fleet Synthesis` in the active design artifact, or write `.cclaw/artifacts/02a-research.md` for deep/high-risk research.");
358
381
  }
359
382
  else if (researchMarkdown) {
360
383
  const missingSections = [];
package/dist/install.js CHANGED
@@ -23,6 +23,7 @@ import { stageSkillFolder, stageSkillMarkdown } from "./content/skills.js";
23
23
  import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LANGUAGE_RULE_PACK_GENERATORS, LEGACY_LANGUAGE_RULE_PACK_FOLDERS } from "./content/utility-skills.js";
24
24
  import { RESEARCH_PLAYBOOKS } from "./content/research-playbooks.js";
25
25
  import { SUBAGENT_CONTEXT_SKILLS } from "./content/subagent-context-skills.js";
26
+ import { CCLAW_AGENTS } from "./content/core-agents.js";
26
27
  import { createInitialFlowState } from "./flow-state.js";
27
28
  import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
28
29
  import { ensureGitignore, removeGitignorePatterns } from "./gitignore.js";
@@ -191,15 +192,84 @@ function resolveRepoRoot() {
191
192
  return process.cwd();
192
193
  }
193
194
 
195
+ function isZeroSha(value) {
196
+ return /^0{40,64}$/u.test(value);
197
+ }
198
+
199
+ function readStdin() {
200
+ try {
201
+ return fs.readFileSync(0, "utf8");
202
+ } catch {
203
+ return "";
204
+ }
205
+ }
206
+
207
+ function uniqueLines(chunks) {
208
+ return [...new Set(chunks
209
+ .join("\n")
210
+ .split(/\r?\n/gu)
211
+ .map((line) => line.trim())
212
+ .filter((line) => line.length > 0))].join("\n");
213
+ }
214
+
215
+ function diffNames(root, range) {
216
+ const result = runGit(["diff", "--name-only", range], root);
217
+ return result.status === 0 ? result.stdout : "";
218
+ }
219
+
220
+ function changedFilesFromUnpushedCommits(root, localSha = "HEAD") {
221
+ const revList = runGit(["rev-list", "--reverse", localSha, "--not", "--remotes"], root);
222
+ if (revList.status !== 0 || revList.stdout.trim().length === 0) {
223
+ return "";
224
+ }
225
+ const chunks = [];
226
+ for (const commit of revList.stdout.split(/\r?\n/gu).map((line) => line.trim()).filter(Boolean)) {
227
+ const diffTree = runGit(["diff-tree", "--no-commit-id", "--name-only", "-r", "--root", commit], root);
228
+ if (diffTree.status === 0) chunks.push(diffTree.stdout);
229
+ }
230
+ return uniqueLines(chunks);
231
+ }
232
+
233
+ function changedFilesFromPrePushStdin(root, stdin) {
234
+ const chunks = [];
235
+ for (const rawLine of stdin.split(/\r?\n/gu)) {
236
+ const parts = rawLine.trim().split(/\s+/u);
237
+ if (parts.length < 4) continue;
238
+ const [localRef, localSha, remoteRef, remoteSha] = parts;
239
+ void localRef;
240
+ void remoteRef;
241
+ if (!localSha || isZeroSha(localSha)) continue;
242
+ if (remoteSha && !isZeroSha(remoteSha)) {
243
+ chunks.push(diffNames(root, remoteSha + ".." + localSha));
244
+ continue;
245
+ }
246
+ const upstream = runGit(["rev-parse", "--verify", "--quiet", "@{upstream}"], root);
247
+ if (upstream.status === 0 && upstream.stdout.trim().length > 0) {
248
+ chunks.push(diffNames(root, upstream.stdout.trim() + ".." + localSha));
249
+ continue;
250
+ }
251
+ chunks.push(changedFilesFromUnpushedCommits(root, localSha));
252
+ }
253
+ return uniqueLines(chunks);
254
+ }
255
+
194
256
  function resolveChangedFiles(root) {
195
257
  if (HOOK_NAME === "pre-commit") {
196
258
  const result = runGit(["diff", "--cached", "--name-only"], root);
197
259
  return result.status === 0 ? result.stdout : "";
198
260
  }
199
- const upstreamResult = runGit(["diff", "--name-only", "@{upstream}...HEAD"], root);
261
+ const stdinChanged = changedFilesFromPrePushStdin(root, readStdin());
262
+ if (stdinChanged.length > 0) {
263
+ return stdinChanged;
264
+ }
265
+ const upstreamResult = runGit(["diff", "--name-only", "@{upstream}..HEAD"], root);
200
266
  if (upstreamResult.status === 0) {
201
267
  return upstreamResult.stdout;
202
268
  }
269
+ const unpushed = changedFilesFromUnpushedCommits(root);
270
+ if (unpushed.length > 0) {
271
+ return unpushed;
272
+ }
203
273
  const fallback = runGit(["diff", "--name-only", "HEAD~1...HEAD"], root);
204
274
  return fallback.status === 0 ? fallback.stdout : "";
205
275
  }
@@ -1270,13 +1340,7 @@ export async function uninstallCclaw(projectRoot) {
1270
1340
  }
1271
1341
  await removeIfEmpty(codexSkillsRoot);
1272
1342
  await removeIfEmpty(path.join(projectRoot, ".agents"));
1273
- const managedAgentNames = [
1274
- "planner",
1275
- "reviewer",
1276
- "security-reviewer",
1277
- "test-author",
1278
- "doc-updater"
1279
- ];
1343
+ const managedAgentNames = CCLAW_AGENTS.map((agent) => agent.name);
1280
1344
  for (const agentName of managedAgentNames) {
1281
1345
  await removeBestEffort(path.join(projectRoot, ".opencode/agents", `${agentName}.md`));
1282
1346
  await removeBestEffort(path.join(projectRoot, ".codex/agents", `${agentName}.toml`));