cclaw-cli 0.10.1 → 0.12.0

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 (55) hide show
  1. package/README.md +4 -3
  2. package/dist/cli.d.ts +6 -0
  3. package/dist/cli.js +297 -9
  4. package/dist/config.js +83 -3
  5. package/dist/content/core-agents.d.ts +44 -0
  6. package/dist/content/core-agents.js +225 -0
  7. package/dist/content/doctor-references.d.ts +2 -0
  8. package/dist/content/doctor-references.js +144 -0
  9. package/dist/content/examples.js +1 -1
  10. package/dist/content/harnesses-doc.d.ts +1 -0
  11. package/dist/content/harnesses-doc.js +95 -0
  12. package/dist/content/hook-events.d.ts +4 -0
  13. package/dist/content/hook-events.js +42 -0
  14. package/dist/content/hooks.js +81 -11
  15. package/dist/content/meta-skill.d.ts +0 -8
  16. package/dist/content/meta-skill.js +51 -341
  17. package/dist/content/next-command.js +2 -1
  18. package/dist/content/protocols.d.ts +7 -0
  19. package/dist/content/protocols.js +123 -0
  20. package/dist/content/research-playbooks.d.ts +8 -0
  21. package/dist/content/research-playbooks.js +135 -0
  22. package/dist/content/skills.js +202 -312
  23. package/dist/content/stage-common-guidance.d.ts +2 -0
  24. package/dist/content/stage-common-guidance.js +71 -0
  25. package/dist/content/stage-schema.d.ts +11 -1
  26. package/dist/content/stage-schema.js +155 -52
  27. package/dist/content/start-command.js +19 -13
  28. package/dist/content/subagents.d.ts +1 -1
  29. package/dist/content/subagents.js +23 -38
  30. package/dist/content/templates.d.ts +1 -1
  31. package/dist/content/templates.js +49 -11
  32. package/dist/delegation.d.ts +1 -0
  33. package/dist/delegation.js +27 -1
  34. package/dist/doctor-registry.d.ts +8 -0
  35. package/dist/doctor-registry.js +127 -0
  36. package/dist/doctor.d.ts +5 -0
  37. package/dist/doctor.js +133 -27
  38. package/dist/flow-state.d.ts +4 -0
  39. package/dist/flow-state.js +4 -1
  40. package/dist/gate-evidence.d.ts +9 -1
  41. package/dist/gate-evidence.js +121 -17
  42. package/dist/harness-adapters.d.ts +7 -0
  43. package/dist/harness-adapters.js +53 -9
  44. package/dist/init-detect.d.ts +2 -0
  45. package/dist/init-detect.js +45 -0
  46. package/dist/install.js +73 -1
  47. package/dist/policy.js +21 -13
  48. package/dist/runs.js +21 -4
  49. package/dist/track-heuristics.d.ts +12 -0
  50. package/dist/track-heuristics.js +144 -0
  51. package/dist/types.d.ts +26 -3
  52. package/dist/types.js +6 -3
  53. package/package.json +2 -1
  54. package/dist/content/agents.d.ts +0 -48
  55. package/dist/content/agents.js +0 -411
package/dist/doctor.js CHANGED
@@ -4,7 +4,7 @@ import { execFile } from "node:child_process";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { promisify } from "node:util";
6
6
  import { COMMAND_FILE_ORDER, REQUIRED_DIRS, RUNTIME_ROOT } from "./constants.js";
7
- import { CCLAW_AGENTS } from "./content/agents.js";
7
+ import { CCLAW_AGENTS } from "./content/core-agents.js";
8
8
  import { readConfig } from "./config.js";
9
9
  import { exists } from "./fs-utils.js";
10
10
  import { gitignoreHasRequiredPatterns } from "./gitignore.js";
@@ -17,10 +17,13 @@ import { checkMandatoryDelegations } from "./delegation.js";
17
17
  import { buildTraceMatrix } from "./trace-matrix.js";
18
18
  import { reconcileAndWriteCurrentStageGateCatalog, verifyCompletedStagesGateClosure, verifyCurrentStageGateEvidence } from "./gate-evidence.js";
19
19
  import { stageSkillFolder } from "./content/skills.js";
20
+ import { doctorCheckMetadata } from "./doctor-registry.js";
20
21
  import { LANGUAGE_RULE_PACK_DIR, LANGUAGE_RULE_PACK_FILES, LEGACY_LANGUAGE_RULE_PACK_FOLDERS, UTILITY_SKILL_FOLDERS } from "./content/utility-skills.js";
21
22
  import { CONTEXT_MODES, DEFAULT_CONTEXT_MODE } from "./content/contexts.js";
23
+ import { DOCTOR_REFERENCE_MARKDOWN } from "./content/doctor-references.js";
22
24
  import { validateHookDocument } from "./hook-schema.js";
23
25
  const execFileAsync = promisify(execFile);
26
+ const PREAMBLE_COOLDOWN_MS = 15 * 60 * 1000;
24
27
  async function isGitRepo(projectRoot) {
25
28
  try {
26
29
  await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: projectRoot });
@@ -278,8 +281,8 @@ export async function doctorChecks(projectRoot, options = {}) {
278
281
  { id: "iron_law", pattern: /^\*\*IRON LAW — [A-Z]+:\*\* .+$/m, label: "Iron Law punchcard (<EXTREMELY-IMPORTANT> wrapper)" },
279
282
  { id: "hard_gate", pattern: /^## HARD-GATE$/m, label: "## HARD-GATE" },
280
283
  { id: "checklist", pattern: /^## Checklist$/m, label: "## Checklist" },
281
- { id: "completion_protocol", pattern: /^## Stage Completion Protocol$/m, label: "## Stage Completion Protocol" },
282
- { id: "handoff_menu", pattern: /^### Handoff Menu$/m, label: "### Handoff Menu" },
284
+ { id: "completion_parameters", pattern: /^## Completion Parameters$/m, label: "## Completion Parameters" },
285
+ { id: "shared_guidance", pattern: /^## Shared Stage Guidance$/m, label: "## Shared Stage Guidance" },
283
286
  { id: "good_vs_bad", pattern: /Good vs Bad/i, label: "Good vs Bad examples" },
284
287
  { id: "anti_patterns", pattern: /^## Anti-Patterns & Red Flags$/m, label: "## Anti-Patterns & Red Flags" }
285
288
  ];
@@ -303,14 +306,12 @@ export async function doctorChecks(projectRoot, options = {}) {
303
306
  const metaContent = await fs.readFile(metaSkillPath, "utf8");
304
307
  const requiredSignals = [
305
308
  { id: "instruction_priority", pattern: /Instruction Priority/i, label: "Instruction Priority" },
306
- { id: "spawned_detection", pattern: /Spawned Subagent Detection/i, label: "Spawned Subagent Detection" },
307
- { id: "shared_decision", pattern: /Shared Decision \+ Tool-Use Protocol/i, label: "Shared Decision + Tool-Use Protocol" },
308
- { id: "shared_completion", pattern: /Shared Stage Completion Protocol/i, label: "Shared Stage Completion Protocol" },
309
- { id: "escalation_rule", pattern: /Escalation Rule \(3 attempts\)/i, label: "Escalation Rule (3 attempts)" },
310
- { id: "invocation_preamble", pattern: /Invocation Preamble/i, label: "Invocation Preamble" },
311
- { id: "operational_self_improvement", pattern: /Operational Self-Improvement/i, label: "Operational Self-Improvement" },
312
- { id: "engineering_ethos", pattern: /Engineering Ethos/i, label: "Engineering Ethos" },
313
- { id: "task_classification", pattern: /Task Classification/i, label: "Task Classification" }
309
+ { id: "routing_flow", pattern: /Routing flow/i, label: "Routing flow" },
310
+ { id: "task_classification", pattern: /Task classification/i, label: "Task classification" },
311
+ { id: "stage_map", pattern: /Stage quick map/i, label: "Stage quick map" },
312
+ { id: "protocol_refs", pattern: /Protocol references/i, label: "Protocol references" },
313
+ { id: "knowledge_guidance", pattern: /Knowledge guidance/i, label: "Knowledge guidance" },
314
+ { id: "failure_guardrails", pattern: /Failure guardrails/i, label: "Failure guardrails" }
314
315
  ];
315
316
  const missingMeta = requiredSignals
316
317
  .filter((signal) => !signal.pattern.test(metaContent))
@@ -347,6 +348,20 @@ export async function doctorChecks(projectRoot, options = {}) {
347
348
  details: refPath
348
349
  });
349
350
  }
351
+ checks.push({
352
+ name: "harness_ref:matrix",
353
+ ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "references", "harnesses.md")),
354
+ details: `${RUNTIME_ROOT}/references/harnesses.md`
355
+ });
356
+ const doctorRefDir = path.join(projectRoot, RUNTIME_ROOT, "references", "doctor");
357
+ for (const fileName of Object.keys(DOCTOR_REFERENCE_MARKDOWN)) {
358
+ const refPath = path.join(doctorRefDir, fileName);
359
+ checks.push({
360
+ name: `doctor_ref:${fileName.replace(/\.md$/, "")}`,
361
+ ok: await exists(refPath),
362
+ details: refPath
363
+ });
364
+ }
350
365
  checks.push({
351
366
  name: "gitignore:required_patterns",
352
367
  ok: await gitignoreHasRequiredPatterns(projectRoot),
@@ -782,6 +797,11 @@ export async function doctorChecks(projectRoot, options = {}) {
782
797
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl")),
783
798
  details: `${RUNTIME_ROOT}/knowledge.jsonl must exist`
784
799
  });
800
+ checks.push({
801
+ name: "knowledge:digest_exists",
802
+ ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "knowledge-digest.md")),
803
+ details: `${RUNTIME_ROOT}/state/knowledge-digest.md must exist`
804
+ });
785
805
  // There must be NO legacy markdown knowledge store — JSONL is the only store.
786
806
  const legacyKnowledgeMdPath = path.join(projectRoot, RUNTIME_ROOT, "knowledge.md");
787
807
  const legacyExists = await exists(legacyKnowledgeMdPath);
@@ -807,6 +827,11 @@ export async function doctorChecks(projectRoot, options = {}) {
807
827
  ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "suggestion-memory.json")),
808
828
  details: `${RUNTIME_ROOT}/state/suggestion-memory.json must exist for proactive suggestion memory`
809
829
  });
830
+ checks.push({
831
+ name: "state:harness_gaps_exists",
832
+ ok: await exists(path.join(projectRoot, RUNTIME_ROOT, "state", "harness-gaps.json")),
833
+ details: `${RUNTIME_ROOT}/state/harness-gaps.json must exist for tiered harness capability tracking`
834
+ });
810
835
  const contextModeStatePath = path.join(projectRoot, RUNTIME_ROOT, "state", "context-mode.json");
811
836
  checks.push({
812
837
  name: "state:context_mode_exists",
@@ -837,6 +862,81 @@ export async function doctorChecks(projectRoot, options = {}) {
837
862
  details: modePath
838
863
  });
839
864
  }
865
+ const preambleLogPath = path.join(projectRoot, RUNTIME_ROOT, "state", "preamble-log.jsonl");
866
+ const preambleLogExists = await exists(preambleLogPath);
867
+ checks.push({
868
+ name: "state:preamble_log_exists",
869
+ ok: preambleLogExists,
870
+ details: `${RUNTIME_ROOT}/state/preamble-log.jsonl must exist for preamble budget tracking`
871
+ });
872
+ if (preambleLogExists) {
873
+ let duplicateHits = 0;
874
+ let parsedEntries = 0;
875
+ let malformedEntries = 0;
876
+ try {
877
+ const now = Date.now();
878
+ const byKey = new Map();
879
+ const raw = await fs.readFile(preambleLogPath, "utf8");
880
+ const lines = raw
881
+ .split("\n")
882
+ .map((line) => line.trim())
883
+ .filter((line) => line.length > 0);
884
+ for (const line of lines) {
885
+ try {
886
+ const parsed = JSON.parse(line);
887
+ const tsRaw = parsed.ts;
888
+ const stageRaw = parsed.stage;
889
+ const triggerRaw = parsed.trigger;
890
+ const hashRaw = parsed.hash;
891
+ if (typeof tsRaw !== "string" ||
892
+ typeof stageRaw !== "string" ||
893
+ typeof triggerRaw !== "string" ||
894
+ typeof hashRaw !== "string") {
895
+ malformedEntries += 1;
896
+ continue;
897
+ }
898
+ const stamp = Date.parse(tsRaw);
899
+ if (!Number.isFinite(stamp)) {
900
+ malformedEntries += 1;
901
+ continue;
902
+ }
903
+ if (now - stamp > 24 * 60 * 60 * 1000) {
904
+ continue;
905
+ }
906
+ parsedEntries += 1;
907
+ const key = `${stageRaw}|${triggerRaw}|${hashRaw}`;
908
+ const bucket = byKey.get(key) ?? [];
909
+ bucket.push(stamp);
910
+ byKey.set(key, bucket);
911
+ }
912
+ catch {
913
+ malformedEntries += 1;
914
+ }
915
+ }
916
+ for (const stamps of byKey.values()) {
917
+ stamps.sort((a, b) => a - b);
918
+ for (let i = 1; i < stamps.length; i += 1) {
919
+ if (stamps[i] - stamps[i - 1] < PREAMBLE_COOLDOWN_MS) {
920
+ duplicateHits += 1;
921
+ }
922
+ }
923
+ }
924
+ }
925
+ catch {
926
+ malformedEntries += 1;
927
+ }
928
+ checks.push({
929
+ name: "warning:preamble:dedup",
930
+ ok: true,
931
+ details: duplicateHits > 0
932
+ ? `warning: detected ${duplicateHits} repeated preamble emission(s) inside ${Math.floor(PREAMBLE_COOLDOWN_MS / 60000)}m cooldown window`
933
+ : parsedEntries > 0
934
+ ? `preamble budget healthy (${parsedEntries} recent preamble entry/entries checked)`
935
+ : malformedEntries > 0
936
+ ? `warning: preamble log exists but entries are malformed (${malformedEntries} line(s) ignored)`
937
+ : "preamble log is empty; no recent preamble emissions recorded"
938
+ });
939
+ }
840
940
  let flowState = await readFlowState(projectRoot);
841
941
  if (options.reconcileCurrentStageGates === true) {
842
942
  const reconciliation = await reconcileAndWriteCurrentStageGateCatalog(projectRoot);
@@ -908,7 +1008,9 @@ export async function doctorChecks(projectRoot, options = {}) {
908
1008
  name: "warning:delegation:waived",
909
1009
  ok: true,
910
1010
  details: delegation.waived.length > 0
911
- ? `warning: waived mandatory delegations for stage "${flowState.currentStage}": ${delegation.waived.join(", ")}`
1011
+ ? `warning: waived mandatory delegations for stage "${flowState.currentStage}": ${delegation.waived.join(", ")}${delegation.autoWaived.length > 0
1012
+ ? ` (auto-waived due to harness limitation: ${delegation.autoWaived.join(", ")})`
1013
+ : ""}`
912
1014
  : "no waived mandatory delegations for current stage"
913
1015
  });
914
1016
  checks.push({
@@ -968,9 +1070,16 @@ export async function doctorChecks(projectRoot, options = {}) {
968
1070
  name: "gates:evidence:current_stage",
969
1071
  ok: gateEvidence.ok,
970
1072
  details: gateEvidence.ok
971
- ? `stage "${gateEvidence.stage}" gate evidence is consistent (required=${gateEvidence.requiredCount}, passed=${gateEvidence.passedCount}, blocked=${gateEvidence.blockedCount})`
1073
+ ? `stage "${gateEvidence.stage}" gate evidence is consistent (required=${gateEvidence.requiredCount}, recommended=${gateEvidence.recommendedCount}, conditional=${gateEvidence.conditionalCount}, triggered=${gateEvidence.triggeredConditionalCount}, passed=${gateEvidence.passedCount}, blocked=${gateEvidence.blockedCount})`
972
1074
  : gateEvidence.issues.join(" ")
973
1075
  });
1076
+ checks.push({
1077
+ name: "warning:gates:recommended:current_stage",
1078
+ ok: true,
1079
+ details: gateEvidence.missingRecommended.length > 0
1080
+ ? `warning: stage "${gateEvidence.stage}" has unmet recommended gates: ${gateEvidence.missingRecommended.join(", ")}`
1081
+ : `no unmet recommended gates for stage "${gateEvidence.stage}"`
1082
+ });
974
1083
  const completedClosure = verifyCompletedStagesGateClosure(flowState);
975
1084
  checks.push({
976
1085
  name: "gates:closure:completed_stages",
@@ -981,18 +1090,6 @@ export async function doctorChecks(projectRoot, options = {}) {
981
1090
  : `all ${flowState.completedStages.length} completed stages have every required gate passed`
982
1091
  : completedClosure.issues.join(" ")
983
1092
  });
984
- // Self-improvement block in stage skills
985
- for (const stage of COMMAND_FILE_ORDER) {
986
- const skillPath = path.join(projectRoot, RUNTIME_ROOT, "skills", stageSkillFolder(stage), "SKILL.md");
987
- if (await exists(skillPath)) {
988
- const content = await fs.readFile(skillPath, "utf8");
989
- checks.push({
990
- name: `skill:${stage}:self_improvement`,
991
- ok: content.includes("## Operational Self-Improvement"),
992
- details: `${skillPath} must contain self-improvement block`
993
- });
994
- }
995
- }
996
1093
  const isRepo = await isGitRepo(projectRoot);
997
1094
  checks.push({
998
1095
  name: "git:cclaw_ignored_runtime",
@@ -1027,8 +1124,17 @@ export async function doctorChecks(projectRoot, options = {}) {
1027
1124
  });
1028
1125
  const policy = await policyChecks(projectRoot, { harnesses: configuredHarnesses });
1029
1126
  checks.push(...policy);
1030
- return checks;
1127
+ return checks.map((check) => {
1128
+ const metadata = doctorCheckMetadata(check.name);
1129
+ return {
1130
+ ...check,
1131
+ severity: check.severity ?? metadata.severity,
1132
+ summary: check.summary ?? metadata.summary,
1133
+ fix: check.fix ?? metadata.fix,
1134
+ docRef: check.docRef ?? metadata.docRef
1135
+ };
1136
+ });
1031
1137
  }
1032
1138
  export function doctorSucceeded(checks) {
1033
- return checks.every((check) => check.ok);
1139
+ return checks.every((check) => check.ok || check.severity !== "error");
1034
1140
  }
@@ -2,6 +2,10 @@ import type { FlowStage, FlowTrack, TransitionRule } from "./types.js";
2
2
  export declare const TRANSITION_RULES: TransitionRule[];
3
3
  export interface StageGateState {
4
4
  required: string[];
5
+ recommended: string[];
6
+ conditional: string[];
7
+ /** Conditional gates currently considered active for blocking checks. */
8
+ triggered: string[];
5
9
  passed: string[];
6
10
  blocked: string[];
7
11
  }
@@ -1,5 +1,5 @@
1
1
  import { COMMAND_FILE_ORDER } from "./constants.js";
2
- import { buildTransitionRules, orderedStageSchemas, stageGateIds } from "./content/stage-schema.js";
2
+ import { buildTransitionRules, orderedStageSchemas, stageConditionalGateIds, stageGateIds, stageRecommendedGateIds } from "./content/stage-schema.js";
3
3
  import { FLOW_STAGES, FLOW_TRACKS, TRACK_STAGES } from "./types.js";
4
4
  export const TRANSITION_RULES = buildTransitionRules();
5
5
  export function isFlowTrack(value) {
@@ -27,6 +27,9 @@ export function createInitialFlowState(activeRunIdOrOptions = "active", maybeTra
27
27
  for (const schema of orderedStageSchemas()) {
28
28
  stageGateCatalog[schema.stage] = {
29
29
  required: stageGateIds(schema.stage),
30
+ recommended: stageRecommendedGateIds(schema.stage),
31
+ conditional: stageConditionalGateIds(schema.stage),
32
+ triggered: [],
30
33
  passed: [],
31
34
  blocked: []
32
35
  };
@@ -5,12 +5,19 @@ export interface GateEvidenceCheckResult {
5
5
  stage: FlowStage;
6
6
  issues: string[];
7
7
  requiredCount: number;
8
+ recommendedCount: number;
9
+ conditionalCount: number;
10
+ triggeredConditionalCount: number;
8
11
  passedCount: number;
9
12
  blockedCount: number;
10
- /** True only when every required gate for the stage is in `passed` and none are `blocked`. */
13
+ /** True only when required + triggered conditional gates are passed and unblocked. */
11
14
  complete: boolean;
12
15
  /** Required gate ids that are neither passed nor blocked. */
13
16
  missingRequired: string[];
17
+ /** Recommended gates not yet passed (does not block). */
18
+ missingRecommended: string[];
19
+ /** Triggered conditional gates that are not yet passed. */
20
+ missingTriggeredConditional: string[];
14
21
  }
15
22
  export interface CompletedStagesClosureResult {
16
23
  ok: boolean;
@@ -18,6 +25,7 @@ export interface CompletedStagesClosureResult {
18
25
  openStages: Array<{
19
26
  stage: FlowStage;
20
27
  missingRequired: string[];
28
+ missingTriggeredConditional: string[];
21
29
  blocked: string[];
22
30
  }>;
23
31
  }
@@ -36,21 +36,56 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
36
36
  const stage = flowState.currentStage;
37
37
  const schema = stageSchema(stage);
38
38
  const catalog = flowState.stageGateCatalog[stage];
39
- const required = schema.requiredGates.map((gate) => gate.id);
39
+ const required = schema.requiredGates
40
+ .filter((gate) => gate.tier === "required")
41
+ .map((gate) => gate.id);
42
+ const recommended = schema.requiredGates
43
+ .filter((gate) => gate.tier === "recommended")
44
+ .map((gate) => gate.id);
45
+ const conditional = schema.requiredGates
46
+ .filter((gate) => gate.tier === "conditional")
47
+ .map((gate) => gate.id);
40
48
  const requiredSet = new Set(required);
49
+ const recommendedSet = new Set(recommended);
50
+ const conditionalSet = new Set(conditional);
51
+ const allowedSet = new Set([...required, ...recommended, ...conditional]);
41
52
  const issues = [];
42
53
  const catalogRequired = unique(catalog.required);
54
+ const catalogRecommended = unique(catalog.recommended ?? []);
55
+ const catalogConditional = unique(catalog.conditional ?? []);
56
+ const catalogTriggered = unique(catalog.triggered ?? []);
43
57
  const missingInCatalog = required.filter((gateId) => !catalogRequired.includes(gateId));
44
58
  const unexpectedInCatalog = catalogRequired.filter((gateId) => !requiredSet.has(gateId));
59
+ const missingRecommendedInCatalog = recommended.filter((gateId) => !catalogRecommended.includes(gateId));
60
+ const unexpectedRecommendedInCatalog = catalogRecommended.filter((gateId) => !recommendedSet.has(gateId));
61
+ const missingConditionalInCatalog = conditional.filter((gateId) => !catalogConditional.includes(gateId));
62
+ const unexpectedConditionalInCatalog = catalogConditional.filter((gateId) => !conditionalSet.has(gateId));
45
63
  for (const gateId of missingInCatalog) {
46
64
  issues.push(`gate "${gateId}" missing from stageGateCatalog.required for stage "${stage}".`);
47
65
  }
48
66
  for (const gateId of unexpectedInCatalog) {
49
67
  issues.push(`unexpected gate "${gateId}" found in stageGateCatalog.required for stage "${stage}".`);
50
68
  }
69
+ for (const gateId of missingRecommendedInCatalog) {
70
+ issues.push(`gate "${gateId}" missing from stageGateCatalog.recommended for stage "${stage}".`);
71
+ }
72
+ for (const gateId of unexpectedRecommendedInCatalog) {
73
+ issues.push(`unexpected gate "${gateId}" found in stageGateCatalog.recommended for stage "${stage}".`);
74
+ }
75
+ for (const gateId of missingConditionalInCatalog) {
76
+ issues.push(`gate "${gateId}" missing from stageGateCatalog.conditional for stage "${stage}".`);
77
+ }
78
+ for (const gateId of unexpectedConditionalInCatalog) {
79
+ issues.push(`unexpected gate "${gateId}" found in stageGateCatalog.conditional for stage "${stage}".`);
80
+ }
81
+ for (const gateId of catalogTriggered) {
82
+ if (!conditionalSet.has(gateId)) {
83
+ issues.push(`triggered gate "${gateId}" is not defined as conditional for stage "${stage}".`);
84
+ }
85
+ }
51
86
  const blockedSet = new Set(catalog.blocked);
52
87
  for (const gateId of catalog.passed) {
53
- if (!requiredSet.has(gateId)) {
88
+ if (!allowedSet.has(gateId)) {
54
89
  issues.push(`passed gate "${gateId}" is not defined for stage "${stage}".`);
55
90
  continue;
56
91
  }
@@ -63,7 +98,7 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
63
98
  }
64
99
  }
65
100
  for (const gateId of catalog.blocked) {
66
- if (!requiredSet.has(gateId)) {
101
+ if (!allowedSet.has(gateId)) {
67
102
  issues.push(`blocked gate "${gateId}" is not defined for stage "${stage}".`);
68
103
  }
69
104
  }
@@ -91,14 +126,25 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
91
126
  }
92
127
  }
93
128
  const passedSet = new Set(catalog.passed);
129
+ const triggeredConditionalSet = new Set([
130
+ ...catalogTriggered.filter((gateId) => conditionalSet.has(gateId)),
131
+ ...catalog.passed.filter((gateId) => conditionalSet.has(gateId)),
132
+ ...catalog.blocked.filter((gateId) => conditionalSet.has(gateId))
133
+ ]);
94
134
  const missingRequired = required.filter((gateId) => !passedSet.has(gateId));
95
- const complete = missingRequired.length === 0 && catalog.blocked.length === 0;
135
+ const missingRecommended = recommended.filter((gateId) => !passedSet.has(gateId));
136
+ const missingTriggeredConditional = [...triggeredConditionalSet].filter((gateId) => !passedSet.has(gateId));
137
+ const blockingBlocked = catalog.blocked.filter((gateId) => requiredSet.has(gateId) || triggeredConditionalSet.has(gateId));
138
+ const complete = missingRequired.length === 0 && missingTriggeredConditional.length === 0 && blockingBlocked.length === 0;
96
139
  if (flowState.completedStages.includes(stage) && !complete) {
97
140
  if (missingRequired.length > 0) {
98
141
  issues.push(`stage "${stage}" is marked completed but required gates are not passed: ${missingRequired.join(", ")}.`);
99
142
  }
100
- if (catalog.blocked.length > 0) {
101
- issues.push(`stage "${stage}" is marked completed but has blocked gates: ${catalog.blocked.join(", ")}.`);
143
+ if (missingTriggeredConditional.length > 0) {
144
+ issues.push(`stage "${stage}" is marked completed but triggered conditional gates are not passed: ${missingTriggeredConditional.join(", ")}.`);
145
+ }
146
+ if (blockingBlocked.length > 0) {
147
+ issues.push(`stage "${stage}" is marked completed but has blocking blocked gates: ${blockingBlocked.join(", ")}.`);
102
148
  }
103
149
  }
104
150
  return {
@@ -106,10 +152,15 @@ export async function verifyCurrentStageGateEvidence(projectRoot, flowState) {
106
152
  stage,
107
153
  issues,
108
154
  requiredCount: required.length,
155
+ recommendedCount: recommended.length,
156
+ conditionalCount: conditional.length,
157
+ triggeredConditionalCount: triggeredConditionalSet.size,
109
158
  passedCount: catalog.passed.length,
110
159
  blockedCount: catalog.blocked.length,
111
160
  complete,
112
- missingRequired
161
+ missingRequired,
162
+ missingRecommended,
163
+ missingTriggeredConditional
113
164
  };
114
165
  }
115
166
  export function verifyCompletedStagesGateClosure(flowState) {
@@ -118,16 +169,37 @@ export function verifyCompletedStagesGateClosure(flowState) {
118
169
  for (const stage of flowState.completedStages) {
119
170
  const schema = stageSchema(stage);
120
171
  const catalog = flowState.stageGateCatalog[stage];
121
- const required = schema.requiredGates.map((gate) => gate.id);
172
+ const required = schema.requiredGates
173
+ .filter((gate) => gate.tier === "required")
174
+ .map((gate) => gate.id);
175
+ const conditional = schema.requiredGates
176
+ .filter((gate) => gate.tier === "conditional")
177
+ .map((gate) => gate.id);
178
+ const conditionalSet = new Set(conditional);
122
179
  const passedSet = new Set(catalog.passed);
180
+ const triggeredSet = new Set([
181
+ ...(catalog.triggered ?? []).filter((gateId) => conditionalSet.has(gateId)),
182
+ ...catalog.passed.filter((gateId) => conditionalSet.has(gateId)),
183
+ ...catalog.blocked.filter((gateId) => conditionalSet.has(gateId))
184
+ ]);
123
185
  const missingRequired = required.filter((gateId) => !passedSet.has(gateId));
124
- if (missingRequired.length > 0 || catalog.blocked.length > 0) {
125
- openStages.push({ stage, missingRequired, blocked: [...catalog.blocked] });
186
+ const missingTriggeredConditional = [...triggeredSet].filter((gateId) => !passedSet.has(gateId));
187
+ const blockingBlocked = catalog.blocked.filter((gateId) => required.includes(gateId) || triggeredSet.has(gateId));
188
+ if (missingRequired.length > 0 || missingTriggeredConditional.length > 0 || blockingBlocked.length > 0) {
189
+ openStages.push({
190
+ stage,
191
+ missingRequired,
192
+ missingTriggeredConditional,
193
+ blocked: [...blockingBlocked]
194
+ });
126
195
  if (missingRequired.length > 0) {
127
196
  issues.push(`completed stage "${stage}" has unpassed required gates: ${missingRequired.join(", ")}.`);
128
197
  }
129
- if (catalog.blocked.length > 0) {
130
- issues.push(`completed stage "${stage}" still has blocked gates: ${catalog.blocked.join(", ")}.`);
198
+ if (missingTriggeredConditional.length > 0) {
199
+ issues.push(`completed stage "${stage}" has unpassed triggered conditional gates: ${missingTriggeredConditional.join(", ")}.`);
200
+ }
201
+ if (blockingBlocked.length > 0) {
202
+ issues.push(`completed stage "${stage}" still has blocking blocked gates: ${blockingBlocked.join(", ")}.`);
131
203
  }
132
204
  }
133
205
  }
@@ -135,29 +207,55 @@ export function verifyCompletedStagesGateClosure(flowState) {
135
207
  }
136
208
  export function reconcileCurrentStageGateCatalog(flowState) {
137
209
  const stage = flowState.currentStage;
138
- const required = stageSchema(stage).requiredGates.map((gate) => gate.id);
210
+ const required = stageSchema(stage).requiredGates
211
+ .filter((gate) => gate.tier === "required")
212
+ .map((gate) => gate.id);
213
+ const recommended = stageSchema(stage).requiredGates
214
+ .filter((gate) => gate.tier === "recommended")
215
+ .map((gate) => gate.id);
216
+ const conditional = stageSchema(stage).requiredGates
217
+ .filter((gate) => gate.tier === "conditional")
218
+ .map((gate) => gate.id);
139
219
  const requiredSet = new Set(required);
220
+ const recommendedSet = new Set(recommended);
221
+ const conditionalSet = new Set(conditional);
222
+ const allowedSet = new Set([...required, ...recommended, ...conditional]);
140
223
  const catalog = flowState.stageGateCatalog[stage];
141
224
  const notes = [];
142
225
  const before = {
143
226
  required: [...catalog.required],
227
+ recommended: [...catalog.recommended],
228
+ conditional: [...catalog.conditional],
229
+ triggered: [...catalog.triggered],
144
230
  passed: [...catalog.passed],
145
231
  blocked: [...catalog.blocked]
146
232
  };
147
233
  const passedSet = new Set(unique(catalog.passed).filter((gateId) => {
148
- const keep = requiredSet.has(gateId);
234
+ const keep = allowedSet.has(gateId);
149
235
  if (!keep) {
150
236
  notes.push(`removed unknown passed gate "${gateId}"`);
151
237
  }
152
238
  return keep;
153
239
  }));
154
240
  const blockedSet = new Set(unique(catalog.blocked).filter((gateId) => {
155
- const keep = requiredSet.has(gateId);
241
+ const keep = allowedSet.has(gateId);
156
242
  if (!keep) {
157
243
  notes.push(`removed unknown blocked gate "${gateId}"`);
158
244
  }
159
245
  return keep;
160
246
  }));
247
+ const triggeredSet = new Set(unique(catalog.triggered).filter((gateId) => {
248
+ const keep = conditionalSet.has(gateId);
249
+ if (!keep) {
250
+ notes.push(`removed unknown triggered gate "${gateId}"`);
251
+ }
252
+ return keep;
253
+ }));
254
+ for (const gateId of [...passedSet, ...blockedSet]) {
255
+ if (conditionalSet.has(gateId)) {
256
+ triggeredSet.add(gateId);
257
+ }
258
+ }
161
259
  for (const gateId of [...passedSet]) {
162
260
  if (!blockedSet.has(gateId))
163
261
  continue;
@@ -180,10 +278,16 @@ export function reconcileCurrentStageGateCatalog(flowState) {
180
278
  }
181
279
  const after = {
182
280
  required: [...required],
183
- passed: required.filter((gateId) => passedSet.has(gateId)),
184
- blocked: required.filter((gateId) => blockedSet.has(gateId) && !passedSet.has(gateId))
281
+ recommended: [...recommended],
282
+ conditional: [...conditional],
283
+ triggered: conditional.filter((gateId) => triggeredSet.has(gateId)),
284
+ passed: [...required, ...recommended, ...conditional].filter((gateId) => passedSet.has(gateId)),
285
+ blocked: [...required, ...recommended, ...conditional].filter((gateId) => blockedSet.has(gateId) && !passedSet.has(gateId))
185
286
  };
186
287
  const changed = !sameStringArray(before.required, after.required) ||
288
+ !sameStringArray(before.recommended, after.recommended) ||
289
+ !sameStringArray(before.conditional, after.conditional) ||
290
+ !sameStringArray(before.triggered, after.triggered) ||
187
291
  !sameStringArray(before.passed, after.passed) ||
188
292
  !sameStringArray(before.blocked, after.blocked);
189
293
  const nextState = changed
@@ -4,8 +4,15 @@ export declare const CCLAW_MARKER_END = "<!-- cclaw-end -->";
4
4
  export interface HarnessAdapter {
5
5
  id: HarnessId;
6
6
  commandDir: string;
7
+ capabilities: {
8
+ nativeSubagentDispatch: "full" | "partial" | "none";
9
+ hookSurface: "full" | "plugin" | "limited" | "none";
10
+ structuredAsk: "AskUserQuestion" | "AskQuestion" | "plain-text";
11
+ };
7
12
  }
8
13
  export declare const HARNESS_ADAPTERS: Record<HarnessId, HarnessAdapter>;
14
+ export type HarnessTier = "tier1" | "tier2" | "tier3";
15
+ export declare function harnessTier(harnessId: HarnessId): HarnessTier;
9
16
  /** Removes the cclaw AGENTS.md block. */
10
17
  export declare function stripCclawBlock(content: string): string;
11
18
  export declare function removeCclawFromAgentsMd(projectRoot: string): Promise<void>;
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { RUNTIME_ROOT } from "./constants.js";
4
- import { CCLAW_AGENTS, agentMarkdown } from "./content/agents.js";
4
+ import { CCLAW_AGENTS, agentMarkdown } from "./content/core-agents.js";
5
5
  import { ensureDir, exists, writeFileSafe } from "./fs-utils.js";
6
6
  export const CCLAW_MARKER_START = "<!-- cclaw-start -->";
7
7
  export const CCLAW_MARKER_END = "<!-- cclaw-end -->";
@@ -12,11 +12,57 @@ const RUNTIME_AGENTS_BLOCK_SOURCE = `${escapeRegExp(CCLAW_MARKER_START)}[\\s\\S]
12
12
  const RUNTIME_AGENTS_BLOCK_PATTERN = new RegExp(RUNTIME_AGENTS_BLOCK_SOURCE, "u");
13
13
  const RUNTIME_AGENTS_BLOCK_GLOBAL_PATTERN = new RegExp(RUNTIME_AGENTS_BLOCK_SOURCE, "gu");
14
14
  export const HARNESS_ADAPTERS = {
15
- claude: { id: "claude", commandDir: ".claude/commands" },
16
- cursor: { id: "cursor", commandDir: ".cursor/commands" },
17
- opencode: { id: "opencode", commandDir: ".opencode/commands" },
18
- codex: { id: "codex", commandDir: ".codex/commands" }
15
+ claude: {
16
+ id: "claude",
17
+ commandDir: ".claude/commands",
18
+ capabilities: {
19
+ nativeSubagentDispatch: "full",
20
+ hookSurface: "full",
21
+ structuredAsk: "AskUserQuestion"
22
+ }
23
+ },
24
+ cursor: {
25
+ id: "cursor",
26
+ commandDir: ".cursor/commands",
27
+ capabilities: {
28
+ nativeSubagentDispatch: "partial",
29
+ hookSurface: "full",
30
+ structuredAsk: "AskQuestion"
31
+ }
32
+ },
33
+ opencode: {
34
+ id: "opencode",
35
+ commandDir: ".opencode/commands",
36
+ capabilities: {
37
+ nativeSubagentDispatch: "partial",
38
+ hookSurface: "plugin",
39
+ structuredAsk: "plain-text"
40
+ }
41
+ },
42
+ codex: {
43
+ id: "codex",
44
+ commandDir: ".codex/commands",
45
+ capabilities: {
46
+ nativeSubagentDispatch: "none",
47
+ hookSurface: "full",
48
+ structuredAsk: "plain-text"
49
+ }
50
+ }
19
51
  };
52
+ export function harnessTier(harnessId) {
53
+ const capabilities = HARNESS_ADAPTERS[harnessId].capabilities;
54
+ if (capabilities.nativeSubagentDispatch === "full" &&
55
+ capabilities.structuredAsk !== "plain-text" &&
56
+ capabilities.hookSurface === "full") {
57
+ return "tier1";
58
+ }
59
+ if (capabilities.hookSurface === "full" ||
60
+ capabilities.hookSurface === "plugin" ||
61
+ capabilities.nativeSubagentDispatch === "partial") {
62
+ return "tier2";
63
+ }
64
+ return "tier3";
65
+ }
20
66
  function agentsMdBlock() {
21
67
  return `${CCLAW_MARKER_START}
22
68
  ## Cclaw — Workflow Adapter
@@ -63,10 +109,6 @@ When in doubt, prefer **non-trivial** — the quick track is opt-in and only saf
63
109
  **Stage order:** brainstorm > scope > design > spec > plan > tdd > review > ship.
64
110
  \`/cc-next\` loads the right stage skill automatically. Gates must pass before handoff.
65
111
 
66
- ### Invocation Preamble (non-trivial turns)
67
-
68
- Before starting substantive work, emit a one-paragraph preamble: **Stage**, **Goal**, **Plan** (next 1–3 actions), **Guardrails**. Skip for pure questions, trivial edits, and dispatched subagent invocations.
69
-
70
112
  ### Verification Discipline
71
113
 
72
114
  No completion claims without fresh evidence. No "Done" / "All good" / "Tests pass" without running the command in this message. Failed tool calls are diagnostic data, not instructions.
@@ -78,7 +120,9 @@ If the same approach fails three times in a row (same command, same finding, sam
78
120
  ### Detail Level
79
121
 
80
122
  - This managed AGENTS block is intentionally minimal for cross-project use.
123
+ - Harness coverage is tiered: Tier1 (claude), Tier2 (cursor/opencode/codex), Tier3 (fallback/manual-only).
81
124
  - Detailed operating procedures live in \`.cclaw/skills/using-cclaw/SKILL.md\`.
125
+ - Preamble budget and cooldown rules live in \`.cclaw/references/protocols/ethos.md\`.
82
126
  - Subagent orchestration patterns: \`.cclaw/skills/subagent-dev/SKILL.md\` and \`.cclaw/skills/parallel-dispatch/SKILL.md\`.
83
127
  ${CCLAW_MARKER_END}`;
84
128
  }
@@ -0,0 +1,2 @@
1
+ import type { HarnessId } from "./types.js";
2
+ export declare function detectHarnesses(projectRoot: string): Promise<HarnessId[]>;