cclaw-cli 0.51.22 → 0.51.24

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/install.js CHANGED
@@ -6,6 +6,7 @@ import { CCLAW_VERSION, FLOW_VERSION, REQUIRED_DIRS, RUNTIME_ROOT } from "./cons
6
6
  import { writeConfig, createDefaultConfig, readConfig, configPath, detectLanguageRulePacks, detectAdvancedKeys } from "./config.js";
7
7
  import { learnSkillMarkdown } from "./content/learnings.js";
8
8
  import { nextCommandContract, nextCommandSkillMarkdown } from "./content/next-command.js";
9
+ import { stageCommandShimMarkdown } from "./content/stage-command.js";
9
10
  import { ideateCommandContract, ideateCommandSkillMarkdown } from "./content/ideate-command.js";
10
11
  import { startCommandContract, startCommandSkillMarkdown } from "./content/start-command.js";
11
12
  import { viewCommandContract, viewCommandSkillMarkdown } from "./content/view-command.js";
@@ -190,15 +191,84 @@ function resolveRepoRoot() {
190
191
  return process.cwd();
191
192
  }
192
193
 
194
+ function isZeroSha(value) {
195
+ return /^0{40,64}$/u.test(value);
196
+ }
197
+
198
+ function readStdin() {
199
+ try {
200
+ return fs.readFileSync(0, "utf8");
201
+ } catch {
202
+ return "";
203
+ }
204
+ }
205
+
206
+ function uniqueLines(chunks) {
207
+ return [...new Set(chunks
208
+ .join("\n")
209
+ .split(/\r?\n/gu)
210
+ .map((line) => line.trim())
211
+ .filter((line) => line.length > 0))].join("\n");
212
+ }
213
+
214
+ function diffNames(root, range) {
215
+ const result = runGit(["diff", "--name-only", range], root);
216
+ return result.status === 0 ? result.stdout : "";
217
+ }
218
+
219
+ function changedFilesFromUnpushedCommits(root, localSha = "HEAD") {
220
+ const revList = runGit(["rev-list", "--reverse", localSha, "--not", "--remotes"], root);
221
+ if (revList.status !== 0 || revList.stdout.trim().length === 0) {
222
+ return "";
223
+ }
224
+ const chunks = [];
225
+ for (const commit of revList.stdout.split(/\r?\n/gu).map((line) => line.trim()).filter(Boolean)) {
226
+ const diffTree = runGit(["diff-tree", "--no-commit-id", "--name-only", "-r", "--root", commit], root);
227
+ if (diffTree.status === 0) chunks.push(diffTree.stdout);
228
+ }
229
+ return uniqueLines(chunks);
230
+ }
231
+
232
+ function changedFilesFromPrePushStdin(root, stdin) {
233
+ const chunks = [];
234
+ for (const rawLine of stdin.split(/\r?\n/gu)) {
235
+ const parts = rawLine.trim().split(/\s+/u);
236
+ if (parts.length < 4) continue;
237
+ const [localRef, localSha, remoteRef, remoteSha] = parts;
238
+ void localRef;
239
+ void remoteRef;
240
+ if (!localSha || isZeroSha(localSha)) continue;
241
+ if (remoteSha && !isZeroSha(remoteSha)) {
242
+ chunks.push(diffNames(root, remoteSha + ".." + localSha));
243
+ continue;
244
+ }
245
+ const upstream = runGit(["rev-parse", "--verify", "--quiet", "@{upstream}"], root);
246
+ if (upstream.status === 0 && upstream.stdout.trim().length > 0) {
247
+ chunks.push(diffNames(root, upstream.stdout.trim() + ".." + localSha));
248
+ continue;
249
+ }
250
+ chunks.push(changedFilesFromUnpushedCommits(root, localSha));
251
+ }
252
+ return uniqueLines(chunks);
253
+ }
254
+
193
255
  function resolveChangedFiles(root) {
194
256
  if (HOOK_NAME === "pre-commit") {
195
257
  const result = runGit(["diff", "--cached", "--name-only"], root);
196
258
  return result.status === 0 ? result.stdout : "";
197
259
  }
198
- const upstreamResult = runGit(["diff", "--name-only", "@{upstream}...HEAD"], root);
260
+ const stdinChanged = changedFilesFromPrePushStdin(root, readStdin());
261
+ if (stdinChanged.length > 0) {
262
+ return stdinChanged;
263
+ }
264
+ const upstreamResult = runGit(["diff", "--name-only", "@{upstream}..HEAD"], root);
199
265
  if (upstreamResult.status === 0) {
200
266
  return upstreamResult.stdout;
201
267
  }
268
+ const unpushed = changedFilesFromUnpushedCommits(root);
269
+ if (unpushed.length > 0) {
270
+ return unpushed;
271
+ }
202
272
  const fallback = runGit(["diff", "--name-only", "HEAD~1...HEAD"], root);
203
273
  return fallback.status === 0 ? fallback.stdout : "";
204
274
  }
@@ -443,6 +513,9 @@ async function writeEntryCommands(projectRoot) {
443
513
  await writeFileSafe(runtimePath(projectRoot, "commands", "next.md"), nextCommandContract());
444
514
  await writeFileSafe(runtimePath(projectRoot, "commands", "ideate.md"), ideateCommandContract());
445
515
  await writeFileSafe(runtimePath(projectRoot, "commands", "view.md"), viewCommandContract());
516
+ for (const stage of FLOW_STAGES) {
517
+ await writeFileSafe(runtimePath(projectRoot, "commands", `${stage}.md`), stageCommandShimMarkdown(stage));
518
+ }
446
519
  }
447
520
  function toObject(value) {
448
521
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -547,11 +620,20 @@ function mergeOpenCodePluginConfig(existingDoc, pluginRelPath) {
547
620
  if (!normalized.has(pluginRelPath)) {
548
621
  pluginsRaw.push(pluginRelPath);
549
622
  }
550
- const changed = !normalized.has(pluginRelPath) || !Array.isArray(root.plugin);
623
+ const permission = toObject(root.permission) ?? {};
624
+ const permissionChanged = permission.question !== "allow";
625
+ const changed = !normalized.has(pluginRelPath) ||
626
+ !Array.isArray(root.plugin) ||
627
+ permissionChanged ||
628
+ !toObject(root.permission);
551
629
  return {
552
630
  merged: {
553
631
  ...root,
554
- plugin: pluginsRaw
632
+ plugin: pluginsRaw,
633
+ permission: {
634
+ ...permission,
635
+ question: "allow"
636
+ }
555
637
  },
556
638
  changed
557
639
  };
@@ -933,7 +1015,6 @@ async function cleanLegacyArtifacts(projectRoot) {
933
1015
  await removeBestEffort(legacyPlugin);
934
1016
  }
935
1017
  for (const legacyRuntimeFile of [
936
- ...FLOW_STAGES.map((stage) => runtimePath(projectRoot, "commands", `${stage}.md`)),
937
1018
  ...DEPRECATED_COMMAND_FILES.map((file) => runtimePath(projectRoot, "commands", file)),
938
1019
  ...DEPRECATED_SKILL_FILES.map((segments) => runtimePath(projectRoot, "skills", ...segments)),
939
1020
  ...DEPRECATED_STATE_FILES.map((file) => runtimePath(projectRoot, "state", file)),
@@ -1248,7 +1329,7 @@ export async function uninstallCclaw(projectRoot) {
1248
1329
  try {
1249
1330
  const entries = await fs.readdir(codexSkillsRoot);
1250
1331
  for (const entry of entries) {
1251
- if (/^(?:cclaw-)?cc(?:-(?:next|view|ops|ideate))?$/u.test(entry)) {
1332
+ if (/^(?:cclaw-)?cc(?:-(?:next|view|ops|ideate|brainstorm|scope|design|spec|plan|tdd|review|ship))?$/u.test(entry)) {
1252
1333
  await fs.rm(path.join(codexSkillsRoot, entry), { recursive: true, force: true });
1253
1334
  }
1254
1335
  }
@@ -1258,6 +1339,19 @@ export async function uninstallCclaw(projectRoot) {
1258
1339
  }
1259
1340
  await removeIfEmpty(codexSkillsRoot);
1260
1341
  await removeIfEmpty(path.join(projectRoot, ".agents"));
1342
+ const managedAgentNames = [
1343
+ "planner",
1344
+ "product-manager",
1345
+ "critic",
1346
+ "reviewer",
1347
+ "security-reviewer",
1348
+ "test-author",
1349
+ "doc-updater"
1350
+ ];
1351
+ for (const agentName of managedAgentNames) {
1352
+ await removeBestEffort(path.join(projectRoot, ".opencode/agents", `${agentName}.md`));
1353
+ await removeBestEffort(path.join(projectRoot, ".codex/agents", `${agentName}.toml`));
1354
+ }
1261
1355
  for (const pluginPath of [
1262
1356
  path.join(projectRoot, ".opencode/plugins/viby-plugin.mjs"),
1263
1357
  path.join(projectRoot, ".opencode/plugins/opencode-plugin.mjs"),
@@ -1284,8 +1378,10 @@ export async function uninstallCclaw(projectRoot) {
1284
1378
  ".cursor/rules",
1285
1379
  ".cursor/commands",
1286
1380
  ".cursor",
1381
+ ".codex/agents",
1287
1382
  ".codex/commands",
1288
1383
  ".codex",
1384
+ ".opencode/agents",
1289
1385
  ".opencode/plugins",
1290
1386
  ".opencode/commands",
1291
1387
  ".opencode"
@@ -505,18 +505,40 @@ export async function appendKnowledge(projectRoot, seeds, defaults = {}) {
505
505
  appendedEntries
506
506
  };
507
507
  }
508
+ const SHORT_TECHNICAL_TOKEN_SET = new Set(["ci", "db", "ui", "qa", "ux"]);
508
509
  function tokenizeText(value) {
509
510
  if (!value)
510
511
  return [];
511
- return value
512
- .toLowerCase()
513
- .split(/[^a-z0-9]+/u)
514
- .map((token) => token.trim())
515
- .filter((token) => token.length >= 3);
512
+ const tokens = [];
513
+ const matches = value.matchAll(/[A-Za-z0-9]+/gu);
514
+ for (const match of matches) {
515
+ const raw = match[0] ?? "";
516
+ const normalized = raw.toLowerCase();
517
+ if (normalized.length >= 3) {
518
+ tokens.push(normalized);
519
+ continue;
520
+ }
521
+ if (/^[A-Z]{2}$/u.test(raw) || SHORT_TECHNICAL_TOKEN_SET.has(normalized)) {
522
+ tokens.push(normalized);
523
+ }
524
+ }
525
+ return tokens;
516
526
  }
517
527
  function uniqueTokens(values) {
518
528
  return [...new Set(values)];
519
529
  }
530
+ function supersededTriggerSet(entries) {
531
+ const superseded = new Set();
532
+ for (const entry of entries) {
533
+ for (const trigger of entry.supersedes ?? []) {
534
+ superseded.add(normalizeText(trigger));
535
+ }
536
+ }
537
+ return superseded;
538
+ }
539
+ function isSupersededLearning(entry, supersededTriggers) {
540
+ return entry.superseded_by !== undefined || supersededTriggers.has(normalizeText(entry.trigger));
541
+ }
520
542
  function pathTokens(paths) {
521
543
  if (!Array.isArray(paths) || paths.length === 0)
522
544
  return [];
@@ -538,7 +560,9 @@ export async function selectRelevantLearnings(projectRoot, options = {}) {
538
560
  const limit = typeof options.limit === "number" && Number.isFinite(options.limit) && options.limit > 0
539
561
  ? Math.floor(options.limit)
540
562
  : 8;
541
- const ranked = entries.map((entry, index) => {
563
+ const staleTriggers = supersededTriggerSet(entries);
564
+ const activeEntries = entries.filter((entry) => !isSupersededLearning(entry, staleTriggers));
565
+ const ranked = activeEntries.map((entry, index) => {
542
566
  let score = 0;
543
567
  let stageScore = 0;
544
568
  if (stage) {
@@ -35,6 +35,13 @@ function stateDirPath(projectRoot) {
35
35
  function archiveLockPath(projectRoot) {
36
36
  return path.join(projectRoot, RUNTIME_ROOT, "state", ".archive.lock");
37
37
  }
38
+ function compoundCloseoutComplete(state) {
39
+ return (state.closeout.compoundCompletedAt !== undefined ||
40
+ state.closeout.compoundPromoted > 0 ||
41
+ (state.closeout.compoundSkipped === true &&
42
+ typeof state.closeout.compoundSkipReason === "string" &&
43
+ state.closeout.compoundSkipReason.trim().length > 0));
44
+ }
38
45
  async function snapshotStateDirectory(projectRoot, destinationRoot) {
39
46
  const sourceDir = stateDirPath(projectRoot);
40
47
  if (!(await exists(sourceDir))) {
@@ -209,6 +216,10 @@ export async function archiveRun(projectRoot, runName, options = {}) {
209
216
  sourceState.closeout.retroSkipReason.trim().length > 0;
210
217
  const readyForArchive = sourceState.closeout.shipSubstate === "ready_to_archive";
211
218
  const inShipCloseout = sourceState.currentStage === "ship";
219
+ if (readyForArchive && !compoundCloseoutComplete(sourceState)) {
220
+ throw new Error("Archive blocked: compound closeout is incomplete. " +
221
+ "Promote compound guidance or skip compound review with an explicit reason before archiving.");
222
+ }
212
223
  if (inShipCloseout && skipRetro) {
213
224
  throw new Error("Archive blocked: --skip-retro is not allowed while current stage is ship. " +
214
225
  "Complete closeout to ready_to_archive via /cc-next.");
@@ -255,21 +255,27 @@ function sanitizeCloseoutState(value) {
255
255
  ? true
256
256
  : undefined;
257
257
  const compoundCompletedAt = typeof typed.compoundCompletedAt === "string" ? typed.compoundCompletedAt : undefined;
258
- const compoundSkipped = typeof typed.compoundSkipped === "boolean" ? typed.compoundSkipped : undefined;
258
+ const compoundSkipReason = typeof typed.compoundSkipReason === "string"
259
+ ? typed.compoundSkipReason.trim() || undefined
260
+ : undefined;
261
+ const compoundSkipped = typed.compoundSkipped === true && compoundSkipReason !== undefined
262
+ ? true
263
+ : undefined;
259
264
  const promotedRaw = typed.compoundPromoted;
260
265
  const compoundPromoted = typeof promotedRaw === "number" && Number.isFinite(promotedRaw) && promotedRaw >= 0
261
266
  ? Math.floor(promotedRaw)
262
267
  : 0;
263
- // Demote shipSubstate when its retro invariant is violated on disk. A
264
- // hand-edited flow-state could claim `ready_to_archive` or `compound_review`
265
- // without ever going through the retro step, which would let `archive`
266
- // proceed and skip the gate. Compound completion is not independently
267
- // tracked in all flows (some runs rely on knowledge.jsonl + the retro
268
- // window), so we only demote when the retro leg is missing outright.
268
+ // Demote shipSubstate when its closeout invariants are violated on disk. A
269
+ // hand-edited flow-state could claim `ready_to_archive` without completing
270
+ // the compound leg, which would let `archive` skip durable closeout proof.
269
271
  const retroDone = retroAcceptedAt !== undefined || retroSkipped === true;
272
+ const compoundDone = compoundCompletedAt !== undefined || compoundPromoted > 0 || compoundSkipped === true;
270
273
  if (!retroDone && (shipSubstate === "ready_to_archive" || shipSubstate === "compound_review")) {
271
274
  shipSubstate = "retro_review";
272
275
  }
276
+ else if (shipSubstate === "ready_to_archive" && !compoundDone) {
277
+ shipSubstate = "compound_review";
278
+ }
273
279
  return {
274
280
  shipSubstate,
275
281
  retroDraftedAt,
@@ -278,6 +284,7 @@ function sanitizeCloseoutState(value) {
278
284
  retroSkipReason,
279
285
  compoundCompletedAt,
280
286
  compoundSkipped,
287
+ compoundSkipReason,
281
288
  compoundPromoted
282
289
  };
283
290
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.51.22",
3
+ "version": "0.51.24",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {