cclaw-cli 0.48.5 → 0.48.6
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/artifact-linter.js +32 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +44 -5
- package/dist/content/hooks.js +111 -42
- package/dist/content/ideate-command.js +11 -0
- package/dist/content/iron-laws.d.ts +134 -0
- package/dist/content/iron-laws.js +182 -0
- package/dist/content/meta-skill.js +1 -0
- package/dist/content/next-command.js +12 -0
- package/dist/content/observe.js +188 -3
- package/dist/content/ops-command.js +11 -0
- package/dist/content/session-hooks.js +3 -1
- package/dist/content/stage-schema.d.ts +16 -0
- package/dist/content/stage-schema.js +82 -5
- package/dist/content/stages/review.js +2 -2
- package/dist/content/stages/tdd.js +7 -7
- package/dist/content/start-command.js +12 -0
- package/dist/content/subagents.js +26 -0
- package/dist/content/view-command.js +11 -0
- package/dist/harness-adapters.js +3 -0
- package/dist/install.js +8 -0
- package/dist/internal/advance-stage.js +10 -2
- package/dist/internal/envelope-validate.d.ts +7 -0
- package/dist/internal/envelope-validate.js +66 -0
- package/dist/internal/knowledge-digest.d.ts +7 -0
- package/dist/internal/knowledge-digest.js +93 -0
- package/dist/knowledge-store.d.ts +8 -0
- package/dist/knowledge-store.js +95 -0
- package/dist/tdd-cycle.d.ts +7 -0
- package/dist/tdd-cycle.js +29 -0
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
package/dist/artifact-linter.js
CHANGED
|
@@ -1092,6 +1092,14 @@ export async function validateReviewArmy(projectRoot) {
|
|
|
1092
1092
|
}
|
|
1093
1093
|
const severitySet = new Set(["Critical", "Important", "Suggestion"]);
|
|
1094
1094
|
const statusSet = new Set(["open", "accepted", "resolved"]);
|
|
1095
|
+
const sourceSet = new Set([
|
|
1096
|
+
"spec",
|
|
1097
|
+
"correctness",
|
|
1098
|
+
"security",
|
|
1099
|
+
"performance",
|
|
1100
|
+
"architecture",
|
|
1101
|
+
"external-safety"
|
|
1102
|
+
]);
|
|
1095
1103
|
const findingIds = new Set();
|
|
1096
1104
|
const openCriticalIds = new Set();
|
|
1097
1105
|
if (!Array.isArray(root.findings)) {
|
|
@@ -1128,6 +1136,17 @@ export async function validateReviewArmy(projectRoot) {
|
|
|
1128
1136
|
if (!isStringArray(o.reportedBy) || o.reportedBy.length === 0) {
|
|
1129
1137
|
errors.push(`findings[${i}].reportedBy must be a non-empty string array.`);
|
|
1130
1138
|
}
|
|
1139
|
+
if (o.sources !== undefined) {
|
|
1140
|
+
if (!isStringArray(o.sources) || o.sources.length === 0) {
|
|
1141
|
+
errors.push(`findings[${i}].sources must be a non-empty string array when present.`);
|
|
1142
|
+
}
|
|
1143
|
+
else {
|
|
1144
|
+
const invalidSources = o.sources.filter((source) => !sourceSet.has(source));
|
|
1145
|
+
if (invalidSources.length > 0) {
|
|
1146
|
+
errors.push(`findings[${i}].sources contains unknown values: ${invalidSources.join(", ")}.`);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1131
1150
|
if (o.location === undefined || o.location === null) {
|
|
1132
1151
|
errors.push(`findings[${i}].location is required and must be an object with file + line.`);
|
|
1133
1152
|
}
|
|
@@ -1231,6 +1250,19 @@ export async function validateReviewArmy(projectRoot) {
|
|
|
1231
1250
|
}
|
|
1232
1251
|
}
|
|
1233
1252
|
}
|
|
1253
|
+
if (rec.layerCoverage !== undefined) {
|
|
1254
|
+
if (rec.layerCoverage === null || typeof rec.layerCoverage !== "object" || Array.isArray(rec.layerCoverage)) {
|
|
1255
|
+
errors.push("reconciliation.layerCoverage must be an object when present.");
|
|
1256
|
+
}
|
|
1257
|
+
else {
|
|
1258
|
+
const coverage = rec.layerCoverage;
|
|
1259
|
+
for (const source of sourceSet) {
|
|
1260
|
+
if (coverage[source] !== undefined && typeof coverage[source] !== "boolean") {
|
|
1261
|
+
errors.push(`reconciliation.layerCoverage.${source} must be boolean when present.`);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1234
1266
|
}
|
|
1235
1267
|
return { valid: errors.length === 0, errors };
|
|
1236
1268
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ export declare function readConfig(projectRoot: string): Promise<CclawConfig>;
|
|
|
42
42
|
* the user set them explicitly. Keeps the default template small and honest:
|
|
43
43
|
* only knobs a new user would meaningfully flip show up.
|
|
44
44
|
*/
|
|
45
|
-
type AdvancedConfigKey = "promptGuardMode" | "tddEnforcement" | "tddTestGlobs" | "tdd" | "compound" | "defaultTrack" | "languageRulePacks" | "trackHeuristics" | "sliceReview";
|
|
45
|
+
type AdvancedConfigKey = "promptGuardMode" | "tddEnforcement" | "tddTestGlobs" | "tdd" | "compound" | "defaultTrack" | "languageRulePacks" | "trackHeuristics" | "sliceReview" | "ironLaws";
|
|
46
46
|
/**
|
|
47
47
|
* Options controlling the serialisation shape of `config.yaml`.
|
|
48
48
|
*
|
package/dist/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parse, stringify } from "yaml";
|
|
4
4
|
import { CCLAW_VERSION, DEFAULT_HARNESSES, FLOW_VERSION, RUNTIME_ROOT } from "./constants.js";
|
|
5
|
+
import { isIronLawId, normalizeStrictLawIds } from "./content/iron-laws.js";
|
|
5
6
|
import { exists, writeFileSafe } from "./fs-utils.js";
|
|
6
7
|
import { FLOW_TRACKS, HARNESS_IDS, LANGUAGE_RULE_PACKS } from "./types.js";
|
|
7
8
|
const CONFIG_PATH = `${RUNTIME_ROOT}/config.yaml`;
|
|
@@ -25,7 +26,8 @@ const ALLOWED_CONFIG_KEYS = new Set([
|
|
|
25
26
|
"defaultTrack",
|
|
26
27
|
"languageRulePacks",
|
|
27
28
|
"trackHeuristics",
|
|
28
|
-
"sliceReview"
|
|
29
|
+
"sliceReview",
|
|
30
|
+
"ironLaws"
|
|
29
31
|
]);
|
|
30
32
|
/**
|
|
31
33
|
* Config keys always present in the minimal init template. Everything else
|
|
@@ -141,7 +143,11 @@ export function createDefaultConfig(harnesses = DEFAULT_HARNESSES, defaultTrack
|
|
|
141
143
|
},
|
|
142
144
|
gitHookGuards: false,
|
|
143
145
|
defaultTrack,
|
|
144
|
-
languageRulePacks: []
|
|
146
|
+
languageRulePacks: [],
|
|
147
|
+
ironLaws: {
|
|
148
|
+
mode: "advisory",
|
|
149
|
+
strictLaws: []
|
|
150
|
+
}
|
|
145
151
|
};
|
|
146
152
|
}
|
|
147
153
|
/**
|
|
@@ -409,6 +415,36 @@ export async function readConfig(projectRoot) {
|
|
|
409
415
|
enforceOnTracks: enforceOnTracks ?? DEFAULT_SLICE_REVIEW_TRACKS
|
|
410
416
|
};
|
|
411
417
|
}
|
|
418
|
+
const ironLawsRaw = parsed.ironLaws;
|
|
419
|
+
let ironLaws = undefined;
|
|
420
|
+
if (Object.prototype.hasOwnProperty.call(parsed, "ironLaws")) {
|
|
421
|
+
if (!isRecord(ironLawsRaw)) {
|
|
422
|
+
throw configValidationError(fullPath, `"ironLaws" must be an object`);
|
|
423
|
+
}
|
|
424
|
+
const unknownIronLawKeys = Object.keys(ironLawsRaw).filter((key) => key !== "mode" && key !== "strictLaws");
|
|
425
|
+
if (unknownIronLawKeys.length > 0) {
|
|
426
|
+
throw configValidationError(fullPath, `"ironLaws" has unknown key(s): ${unknownIronLawKeys.join(", ")}`);
|
|
427
|
+
}
|
|
428
|
+
const modeRaw = ironLawsRaw.mode;
|
|
429
|
+
if (modeRaw !== undefined && modeRaw !== "advisory" && modeRaw !== "strict") {
|
|
430
|
+
throw configValidationError(fullPath, `"ironLaws.mode" must be "advisory" or "strict"`);
|
|
431
|
+
}
|
|
432
|
+
const strictLawIdsRaw = validateStringArray(ironLawsRaw.strictLaws, "ironLaws.strictLaws", fullPath) ?? [];
|
|
433
|
+
const unknownStrictLawIds = strictLawIdsRaw.filter((id) => !isIronLawId(id));
|
|
434
|
+
if (unknownStrictLawIds.length > 0) {
|
|
435
|
+
throw configValidationError(fullPath, `"ironLaws.strictLaws" contains unknown law id(s): ${unknownStrictLawIds.join(", ")}`);
|
|
436
|
+
}
|
|
437
|
+
ironLaws = {
|
|
438
|
+
mode: modeRaw === "strict" ? "strict" : "advisory",
|
|
439
|
+
strictLaws: normalizeStrictLawIds(strictLawIdsRaw)
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
ironLaws = {
|
|
444
|
+
mode: strictness,
|
|
445
|
+
strictLaws: []
|
|
446
|
+
};
|
|
447
|
+
}
|
|
412
448
|
return {
|
|
413
449
|
version: parsed.version ?? CCLAW_VERSION,
|
|
414
450
|
flowVersion: parsed.flowVersion ?? FLOW_VERSION,
|
|
@@ -428,7 +464,8 @@ export async function readConfig(projectRoot) {
|
|
|
428
464
|
defaultTrack,
|
|
429
465
|
languageRulePacks,
|
|
430
466
|
trackHeuristics,
|
|
431
|
-
sliceReview
|
|
467
|
+
sliceReview,
|
|
468
|
+
ironLaws
|
|
432
469
|
};
|
|
433
470
|
}
|
|
434
471
|
function isMinimalKey(key) {
|
|
@@ -452,7 +489,8 @@ function buildSerializableConfig(config, options = {}) {
|
|
|
452
489
|
"defaultTrack",
|
|
453
490
|
"languageRulePacks",
|
|
454
491
|
"trackHeuristics",
|
|
455
|
-
"sliceReview"
|
|
492
|
+
"sliceReview",
|
|
493
|
+
"ironLaws"
|
|
456
494
|
];
|
|
457
495
|
for (const key of ordered) {
|
|
458
496
|
const value = config[key];
|
|
@@ -506,7 +544,8 @@ export async function detectAdvancedKeys(projectRoot) {
|
|
|
506
544
|
"defaultTrack",
|
|
507
545
|
"languageRulePacks",
|
|
508
546
|
"trackHeuristics",
|
|
509
|
-
"sliceReview"
|
|
547
|
+
"sliceReview",
|
|
548
|
+
"ironLaws"
|
|
510
549
|
];
|
|
511
550
|
const present = new Set();
|
|
512
551
|
for (const key of advancedCandidates) {
|
package/dist/content/hooks.js
CHANGED
|
@@ -48,6 +48,7 @@ STATE_FILE="$ROOT/${RUNTIME_ROOT}/state/flow-state.json"
|
|
|
48
48
|
ACTIVE_FEATURE_FILE="$ROOT/${RUNTIME_ROOT}/state/active-feature.json"
|
|
49
49
|
CHECKPOINT_FILE="$ROOT/${RUNTIME_ROOT}/state/checkpoint.json"
|
|
50
50
|
ACTIVITY_FILE="$ROOT/${RUNTIME_ROOT}/state/stage-activity.jsonl"
|
|
51
|
+
IRON_LAWS_FILE="$ROOT/${RUNTIME_ROOT}/state/iron-laws.json"
|
|
51
52
|
SUGGESTION_MEMORY_FILE="$ROOT/${RUNTIME_ROOT}/state/suggestion-memory.json"
|
|
52
53
|
CONTEXT_WARNINGS_FILE="$ROOT/${RUNTIME_ROOT}/state/context-warnings.jsonl"
|
|
53
54
|
CONTEXT_MODE_FILE="$ROOT/${RUNTIME_ROOT}/state/context-mode.json"
|
|
@@ -352,74 +353,99 @@ if [ -f "$META_SKILL" ]; then
|
|
|
352
353
|
META_CONTENT=$(cat "$META_SKILL" 2>/dev/null || echo "")
|
|
353
354
|
fi
|
|
354
355
|
|
|
355
|
-
# --- Build compact knowledge digest (stage
|
|
356
|
+
# --- Build compact knowledge digest (stage + branch + diff aware) ---
|
|
356
357
|
KNOWLEDGE_DIGEST=""
|
|
357
358
|
LEARNINGS_COUNT=0
|
|
358
359
|
if [ -f "$KNOWLEDGE_FILE" ] && [ -s "$KNOWLEDGE_FILE" ]; then
|
|
359
360
|
LEARNINGS_COUNT=$(grep -c '^{' "$KNOWLEDGE_FILE" 2>/dev/null || echo "0")
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
if command -v cclaw >/dev/null 2>&1 && [ -f "$KNOWLEDGE_FILE" ] && [ -s "$KNOWLEDGE_FILE" ]; then
|
|
364
|
+
BRANCH_NAME=""
|
|
365
|
+
if command -v git >/dev/null 2>&1 && git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
366
|
+
BRANCH_NAME=$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
367
|
+
fi
|
|
368
|
+
DIFF_FILES_CSV=""
|
|
369
|
+
if command -v git >/dev/null 2>&1 && git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
370
|
+
DIFF_FILES_CSV=$(git -C "$ROOT" diff --name-only HEAD~5..HEAD 2>/dev/null | head -n 20 | tr '\n' ',' | sed 's/,$//' || echo "")
|
|
371
|
+
fi
|
|
372
|
+
OPEN_GATES_CSV=""
|
|
373
|
+
if [ -f "$STATE_FILE" ] && command -v jq >/dev/null 2>&1; then
|
|
374
|
+
OPEN_GATES_CSV=$(jq -r --arg stage "$STAGE" '
|
|
375
|
+
(.stageGateCatalog[$stage].required // [])
|
|
376
|
+
- (.stageGateCatalog[$stage].passed // [])
|
|
377
|
+
| join(",")
|
|
378
|
+
' "$STATE_FILE" 2>/dev/null || echo "")
|
|
379
|
+
fi
|
|
380
|
+
DIGEST_CMD=(cclaw internal knowledge-digest --stage="$STAGE" --limit=8)
|
|
381
|
+
if [ -n "$BRANCH_NAME" ]; then
|
|
382
|
+
DIGEST_CMD+=("--branch=$BRANCH_NAME")
|
|
383
|
+
fi
|
|
384
|
+
if [ -n "$DIFF_FILES_CSV" ]; then
|
|
385
|
+
DIGEST_CMD+=("--diff-files=$DIFF_FILES_CSV")
|
|
386
|
+
fi
|
|
387
|
+
if [ -n "$OPEN_GATES_CSV" ]; then
|
|
388
|
+
DIGEST_CMD+=("--open-gates=$OPEN_GATES_CSV")
|
|
389
|
+
fi
|
|
390
|
+
KNOWLEDGE_DIGEST=$("\${DIGEST_CMD[@]}" 2>/dev/null || echo "")
|
|
391
|
+
fi
|
|
392
|
+
|
|
393
|
+
if [ -z "$KNOWLEDGE_DIGEST" ] && [ -f "$KNOWLEDGE_FILE" ] && [ -s "$KNOWLEDGE_FILE" ]; then
|
|
360
394
|
if command -v jq >/dev/null 2>&1; then
|
|
361
|
-
KNOWLEDGE_DIGEST=$(tail -n
|
|
395
|
+
KNOWLEDGE_DIGEST=$(tail -n 120 "$KNOWLEDGE_FILE" 2>/dev/null | jq -Rsc --arg stage "$STAGE" '
|
|
362
396
|
split("\\n")
|
|
363
397
|
| map(select(length > 0))
|
|
364
398
|
| map(try fromjson catch null)
|
|
365
399
|
| map(select(type == "object"))
|
|
366
400
|
| map(select((.stage // null) == $stage or (.stage // null) == null))
|
|
367
401
|
| reverse
|
|
368
|
-
| .[0:
|
|
402
|
+
| .[0:6]
|
|
369
403
|
| map("- [" + ((.confidence // "unknown")|tostring) + " • " + ((.stage // "global")|tostring) + " • " + ((.domain // "general")|tostring) + "] " + ((.trigger // "trigger")|tostring) + " -> " + ((.action // "action")|tostring))
|
|
370
404
|
| join("\\n")
|
|
371
405
|
' 2>/dev/null || echo "")
|
|
406
|
+
else
|
|
407
|
+
KNOWLEDGE_DIGEST=$(tail -n 6 "$KNOWLEDGE_FILE" 2>/dev/null || echo "")
|
|
408
|
+
fi
|
|
409
|
+
fi
|
|
410
|
+
|
|
411
|
+
if [ -n "$KNOWLEDGE_DIGEST" ]; then
|
|
412
|
+
printf '# Knowledge digest (auto-generated)\\n\\n%s\\n' "$KNOWLEDGE_DIGEST" > "$KNOWLEDGE_DIGEST_FILE" 2>/dev/null || true
|
|
413
|
+
elif [ -f "$KNOWLEDGE_DIGEST_FILE" ]; then
|
|
414
|
+
printf '# Knowledge digest (auto-generated)\\n\\n(no matching entries for current stage)\\n' > "$KNOWLEDGE_DIGEST_FILE" 2>/dev/null || true
|
|
415
|
+
fi
|
|
416
|
+
|
|
417
|
+
IRON_LAWS_SUMMARY=""
|
|
418
|
+
if [ -f "$IRON_LAWS_FILE" ]; then
|
|
419
|
+
if command -v jq >/dev/null 2>&1; then
|
|
420
|
+
IRON_LAWS_SUMMARY=$(jq -r '
|
|
421
|
+
(.laws // [])
|
|
422
|
+
| map("- [" + (if (.strict // false) then "strict" else "advisory" end) + "] " + ((.id // "law")|tostring) + " -> " + ((.rule // "")|tostring))
|
|
423
|
+
| .[0:6]
|
|
424
|
+
| join("\\n")
|
|
425
|
+
' "$IRON_LAWS_FILE" 2>/dev/null || echo "")
|
|
372
426
|
elif command -v python3 >/dev/null 2>&1; then
|
|
373
|
-
|
|
427
|
+
IRON_LAWS_SUMMARY=$(python3 - "$IRON_LAWS_FILE" <<'PY'
|
|
374
428
|
import json
|
|
375
429
|
import sys
|
|
376
|
-
|
|
377
|
-
path = sys.argv[1]
|
|
378
|
-
stage = sys.argv[2]
|
|
379
|
-
entries = []
|
|
430
|
+
out = []
|
|
380
431
|
try:
|
|
381
|
-
with open(
|
|
382
|
-
|
|
383
|
-
for
|
|
384
|
-
|
|
385
|
-
if not raw:
|
|
386
|
-
continue
|
|
387
|
-
try:
|
|
388
|
-
obj = json.loads(raw)
|
|
389
|
-
except Exception:
|
|
390
|
-
continue
|
|
391
|
-
if not isinstance(obj, dict):
|
|
392
|
-
continue
|
|
393
|
-
row_stage = obj.get("stage")
|
|
394
|
-
if row_stage not in (stage, None):
|
|
432
|
+
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
433
|
+
parsed = json.load(fh)
|
|
434
|
+
for row in (parsed.get("laws") or [])[:6]:
|
|
435
|
+
if not isinstance(row, dict):
|
|
395
436
|
continue
|
|
396
|
-
|
|
437
|
+
strict = "strict" if row.get("strict") else "advisory"
|
|
438
|
+
law_id = str(row.get("id") or "law")
|
|
439
|
+
rule = str(row.get("rule") or "")
|
|
440
|
+
out.append(f"- [{strict}] {law_id} -> {rule}")
|
|
397
441
|
except Exception:
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
entries = list(reversed(entries))[:8]
|
|
401
|
-
out = []
|
|
402
|
-
for obj in entries:
|
|
403
|
-
conf = str(obj.get("confidence", "unknown"))
|
|
404
|
-
row_stage = str(obj.get("stage", "global"))
|
|
405
|
-
domain = str(obj.get("domain", "general"))
|
|
406
|
-
trigger = str(obj.get("trigger", "trigger"))
|
|
407
|
-
action = str(obj.get("action", "action"))
|
|
408
|
-
out.append(f"- [{conf} • {row_stage} • {domain}] {trigger} -> {action}")
|
|
442
|
+
out = []
|
|
409
443
|
print("\\n".join(out))
|
|
410
444
|
PY
|
|
411
445
|
)
|
|
412
|
-
else
|
|
413
|
-
KNOWLEDGE_DIGEST=$(tail -n 8 "$KNOWLEDGE_FILE" 2>/dev/null || echo "")
|
|
414
446
|
fi
|
|
415
447
|
fi
|
|
416
448
|
|
|
417
|
-
if [ -n "$KNOWLEDGE_DIGEST" ]; then
|
|
418
|
-
printf '# Knowledge digest (auto-generated)\\n\\n%s\\n' "$KNOWLEDGE_DIGEST" > "$KNOWLEDGE_DIGEST_FILE" 2>/dev/null || true
|
|
419
|
-
elif [ -f "$KNOWLEDGE_DIGEST_FILE" ]; then
|
|
420
|
-
printf '# Knowledge digest (auto-generated)\\n\\n(no matching entries for current stage)\\n' > "$KNOWLEDGE_DIGEST_FILE" 2>/dev/null || true
|
|
421
|
-
fi
|
|
422
|
-
|
|
423
449
|
# --- Installed cclaw-cli version vs. project's recorded version (one-block
|
|
424
450
|
# upgrade-check, gstack-style). Purely informational — we never block. ---
|
|
425
451
|
VERSION_NOTE=""
|
|
@@ -503,6 +529,11 @@ if [ -n "$KNOWLEDGE_DIGEST" ]; then
|
|
|
503
529
|
Knowledge digest (top relevant entries):
|
|
504
530
|
$KNOWLEDGE_DIGEST"
|
|
505
531
|
fi
|
|
532
|
+
if [ -n "$IRON_LAWS_SUMMARY" ]; then
|
|
533
|
+
CTX="$CTX
|
|
534
|
+
Iron laws (enforced policy highlights):
|
|
535
|
+
$IRON_LAWS_SUMMARY"
|
|
536
|
+
fi
|
|
506
537
|
if [ -n "$META_CONTENT" ]; then
|
|
507
538
|
CTX="$CTX
|
|
508
539
|
|
|
@@ -545,6 +576,7 @@ STATE_FILE="$STATE_DIR/flow-state.json"
|
|
|
545
576
|
CHECKPOINT_FILE="$STATE_DIR/checkpoint.json"
|
|
546
577
|
CHECKPOINT_TMP="$STATE_DIR/checkpoint.json.tmp.$$"
|
|
547
578
|
CHECKPOINT_LOCK_DIR="$STATE_DIR/.checkpoint.lock"
|
|
579
|
+
IRON_LAWS_FILE="$STATE_DIR/iron-laws.json"
|
|
548
580
|
STAGE="none"
|
|
549
581
|
ACTIVE_RUN="none"
|
|
550
582
|
LOOP_COUNT=""
|
|
@@ -617,6 +649,38 @@ if command -v git >/dev/null 2>&1; then
|
|
|
617
649
|
fi
|
|
618
650
|
fi
|
|
619
651
|
|
|
652
|
+
STRICT_STOP_DIRTY="false"
|
|
653
|
+
if [ -f "$IRON_LAWS_FILE" ]; then
|
|
654
|
+
if command -v jq >/dev/null 2>&1; then
|
|
655
|
+
STRICT_STOP_DIRTY=$(jq -r '
|
|
656
|
+
if (.mode // "advisory") == "strict" then "true"
|
|
657
|
+
elif ((.laws // []) | any(.id == "stop-clean-or-checkpointed" and .strict == true)) then "true"
|
|
658
|
+
else "false"
|
|
659
|
+
end
|
|
660
|
+
' "$IRON_LAWS_FILE" 2>/dev/null || echo "false")
|
|
661
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
662
|
+
STRICT_STOP_DIRTY=$(python3 - "$IRON_LAWS_FILE" <<'PY'
|
|
663
|
+
import json
|
|
664
|
+
import sys
|
|
665
|
+
value = "false"
|
|
666
|
+
try:
|
|
667
|
+
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
668
|
+
parsed = json.load(fh)
|
|
669
|
+
if str(parsed.get("mode", "advisory")) == "strict":
|
|
670
|
+
value = "true"
|
|
671
|
+
else:
|
|
672
|
+
for row in parsed.get("laws", []):
|
|
673
|
+
if isinstance(row, dict) and row.get("id") == "stop-clean-or-checkpointed" and row.get("strict") is True:
|
|
674
|
+
value = "true"
|
|
675
|
+
break
|
|
676
|
+
except Exception:
|
|
677
|
+
value = "false"
|
|
678
|
+
print(value)
|
|
679
|
+
PY
|
|
680
|
+
)
|
|
681
|
+
fi
|
|
682
|
+
fi
|
|
683
|
+
|
|
620
684
|
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
|
|
621
685
|
mkdir -p "$STATE_DIR" 2>/dev/null || true
|
|
622
686
|
CHECKPOINT_WRITTEN=0
|
|
@@ -742,6 +806,11 @@ if [ "$CHECKPOINT_WRITTEN" -eq 0 ]; then
|
|
|
742
806
|
CHECKPOINT_NOTE="Checkpoint update failed. Review ${RUNTIME_ROOT}/state/checkpoint.json manually."
|
|
743
807
|
fi
|
|
744
808
|
|
|
809
|
+
if [ "$DIRTY_STATE" = "dirty" ] && [ "$STRICT_STOP_DIRTY" = "true" ]; then
|
|
810
|
+
printf '[cclaw] Stop blocked by iron law "stop-clean-or-checkpointed": working tree is dirty. Commit/revert changes or update checkpoint blockers before ending the session.\\n' >&2
|
|
811
|
+
exit 1
|
|
812
|
+
fi
|
|
813
|
+
|
|
745
814
|
RUN_SYNC_NOTE="Run metadata sync removed; active artifacts stay in ${RUNTIME_ROOT}/artifacts until /cc-ops archive (or cclaw archive runtime)."
|
|
746
815
|
|
|
747
816
|
# --- Escape for JSON ---
|
|
@@ -58,6 +58,17 @@ same session, or save/discard the backlog.
|
|
|
58
58
|
6. **Present the handoff prompt** with four concrete options — not A/B/C
|
|
59
59
|
letters. Default = "Start /cc on the top recommendation".
|
|
60
60
|
|
|
61
|
+
## Headless mode
|
|
62
|
+
|
|
63
|
+
For skill-to-skill invocation, emit exactly one JSON envelope:
|
|
64
|
+
|
|
65
|
+
\`\`\`json
|
|
66
|
+
{"version":"1","kind":"stage-output","stage":"brainstorm","payload":{"command":"/cc-ideate","artifact":".cclaw/artifacts/ideate-<date>-<slug>.md","recommendation":"I-1"},"emittedAt":"<ISO-8601>"}
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
Validate envelopes with:
|
|
70
|
+
\`cclaw internal envelope-validate --stdin\`
|
|
71
|
+
|
|
61
72
|
## Primary skill
|
|
62
73
|
|
|
63
74
|
**${RUNTIME_ROOT}/skills/${IDEATE_SKILL_FOLDER}/SKILL.md**
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { FlowStage } from "../types.js";
|
|
2
|
+
export type IronLawEnforcementPoint = "PreToolUse" | "PostToolUse" | "SessionStart" | "Stop" | "advisory";
|
|
3
|
+
export type IronLawSeverity = "hard-gate" | "soft-gate";
|
|
4
|
+
export interface IronLawDefinition {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
rule: string;
|
|
8
|
+
rationale: string;
|
|
9
|
+
enforcement: IronLawEnforcementPoint;
|
|
10
|
+
severity: IronLawSeverity;
|
|
11
|
+
appliesTo: "all" | FlowStage[];
|
|
12
|
+
hookMatcher?: {
|
|
13
|
+
toolPattern?: string;
|
|
14
|
+
payloadPattern?: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export interface IronLawRuntimeRecord {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
rule: string;
|
|
21
|
+
enforcement: IronLawEnforcementPoint;
|
|
22
|
+
severity: IronLawSeverity;
|
|
23
|
+
appliesTo: "all" | FlowStage[];
|
|
24
|
+
strict: boolean;
|
|
25
|
+
hookMatcher?: {
|
|
26
|
+
toolPattern?: string;
|
|
27
|
+
payloadPattern?: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface IronLawRuntimeDocument {
|
|
31
|
+
version: 1;
|
|
32
|
+
generatedAt: string;
|
|
33
|
+
mode: "advisory" | "strict";
|
|
34
|
+
strictLaws: string[];
|
|
35
|
+
laws: IronLawRuntimeRecord[];
|
|
36
|
+
}
|
|
37
|
+
export declare const IRON_LAWS: readonly [{
|
|
38
|
+
readonly id: "tdd-red-before-write";
|
|
39
|
+
readonly title: "RED before production write";
|
|
40
|
+
readonly rule: "Do not edit production code in tdd stage before a failing RED test exists for the slice.";
|
|
41
|
+
readonly rationale: "Prevents implementation-first behavior and keeps RED as executable specification.";
|
|
42
|
+
readonly enforcement: "PreToolUse";
|
|
43
|
+
readonly severity: "hard-gate";
|
|
44
|
+
readonly appliesTo: ["tdd"];
|
|
45
|
+
readonly hookMatcher: {
|
|
46
|
+
readonly toolPattern: "write|edit|multiedit|applypatch|shell|bash";
|
|
47
|
+
readonly payloadPattern: "\\.(ts|tsx|js|jsx|py|go|java|rs|rb|php|c|cc|cpp|h|hpp)";
|
|
48
|
+
};
|
|
49
|
+
}, {
|
|
50
|
+
readonly id: "plan-requires-approval";
|
|
51
|
+
readonly title: "No implementation before plan approval";
|
|
52
|
+
readonly rule: "Do not perform write-like actions while plan stage is pending WAIT_FOR_CONFIRM approval.";
|
|
53
|
+
readonly rationale: "Locks intent before execution and reduces expensive rework from unapproved paths.";
|
|
54
|
+
readonly enforcement: "PreToolUse";
|
|
55
|
+
readonly severity: "hard-gate";
|
|
56
|
+
readonly appliesTo: ["plan"];
|
|
57
|
+
}, {
|
|
58
|
+
readonly id: "runtime-writes-managed-only";
|
|
59
|
+
readonly title: "Runtime writes are managed";
|
|
60
|
+
readonly rule: "Do not mutate .cclaw/state, .cclaw/hooks, or .cclaw/skills by ad-hoc edits unless using cclaw-managed commands.";
|
|
61
|
+
readonly rationale: "Protects generated runtime integrity and avoids drift that silently breaks hooks or skills.";
|
|
62
|
+
readonly enforcement: "PreToolUse";
|
|
63
|
+
readonly severity: "hard-gate";
|
|
64
|
+
readonly appliesTo: "all";
|
|
65
|
+
readonly hookMatcher: {
|
|
66
|
+
readonly toolPattern: "write|edit|multiedit|delete|applypatch|shell|bash";
|
|
67
|
+
readonly payloadPattern: "\\.cclaw/(state|hooks|skills)";
|
|
68
|
+
};
|
|
69
|
+
}, {
|
|
70
|
+
readonly id: "flow-state-read-fresh";
|
|
71
|
+
readonly title: "Fresh flow-state read required";
|
|
72
|
+
readonly rule: "Before mutating actions, a fresh read of .cclaw/state/flow-state.json must exist within guard freshness window.";
|
|
73
|
+
readonly rationale: "Prevents stale-stage mutations after context shifts or multi-agent divergence.";
|
|
74
|
+
readonly enforcement: "PreToolUse";
|
|
75
|
+
readonly severity: "hard-gate";
|
|
76
|
+
readonly appliesTo: "all";
|
|
77
|
+
}, {
|
|
78
|
+
readonly id: "review-layer-order";
|
|
79
|
+
readonly title: "Review layers are sequential";
|
|
80
|
+
readonly rule: "Review stage must complete Layer 1 spec compliance before Layer 2 quality/security passes.";
|
|
81
|
+
readonly rationale: "Stops premature quality discussion when acceptance criteria are not yet satisfied.";
|
|
82
|
+
readonly enforcement: "advisory";
|
|
83
|
+
readonly severity: "soft-gate";
|
|
84
|
+
readonly appliesTo: ["review"];
|
|
85
|
+
}, {
|
|
86
|
+
readonly id: "review-criticals-close-before-ship";
|
|
87
|
+
readonly title: "No ship with open criticals";
|
|
88
|
+
readonly rule: "Ship decisions are blocked when review-army contains open Critical findings or ship blockers.";
|
|
89
|
+
readonly rationale: "Enforces explicit risk closure before release finalization.";
|
|
90
|
+
readonly enforcement: "PreToolUse";
|
|
91
|
+
readonly severity: "hard-gate";
|
|
92
|
+
readonly appliesTo: ["ship"];
|
|
93
|
+
}, {
|
|
94
|
+
readonly id: "ship-preflight-required";
|
|
95
|
+
readonly title: "Preflight required before finalization";
|
|
96
|
+
readonly rule: "Do not execute release finalization actions until ship preflight gate is passed.";
|
|
97
|
+
readonly rationale: "Catches regressions before irreversible release steps.";
|
|
98
|
+
readonly enforcement: "PreToolUse";
|
|
99
|
+
readonly severity: "hard-gate";
|
|
100
|
+
readonly appliesTo: ["ship"];
|
|
101
|
+
}, {
|
|
102
|
+
readonly id: "subagent-task-self-contained";
|
|
103
|
+
readonly title: "Subagent tasks are self-contained";
|
|
104
|
+
readonly rule: "Delegated tasks must include explicit objective, constraints, and expected output, not just references.";
|
|
105
|
+
readonly rationale: "Avoids context loss and low-quality delegation in isolated worker contexts.";
|
|
106
|
+
readonly enforcement: "advisory";
|
|
107
|
+
readonly severity: "soft-gate";
|
|
108
|
+
readonly appliesTo: "all";
|
|
109
|
+
}, {
|
|
110
|
+
readonly id: "no-secrets-in-artifacts";
|
|
111
|
+
readonly title: "Never log secrets in artifacts";
|
|
112
|
+
readonly rule: "Secrets/tokens/passwords must not be written to review, ship, or runtime state artifacts.";
|
|
113
|
+
readonly rationale: "Prevents accidental credential leakage through generated workflow artifacts.";
|
|
114
|
+
readonly enforcement: "PostToolUse";
|
|
115
|
+
readonly severity: "hard-gate";
|
|
116
|
+
readonly appliesTo: "all";
|
|
117
|
+
}, {
|
|
118
|
+
readonly id: "stop-clean-or-checkpointed";
|
|
119
|
+
readonly title: "Stop only from clean checkpoint";
|
|
120
|
+
readonly rule: "Do not end a session with dirty state unless checkpoint explicitly records unresolved work and blockers.";
|
|
121
|
+
readonly rationale: "Protects continuity and prevents silent half-finished sessions.";
|
|
122
|
+
readonly enforcement: "Stop";
|
|
123
|
+
readonly severity: "hard-gate";
|
|
124
|
+
readonly appliesTo: "all";
|
|
125
|
+
}];
|
|
126
|
+
export declare function isIronLawId(value: string): boolean;
|
|
127
|
+
export declare function normalizeStrictLawIds(ids: string[] | undefined): string[];
|
|
128
|
+
export declare function ironLawRuntimeDocument(options?: {
|
|
129
|
+
mode?: "advisory" | "strict";
|
|
130
|
+
strictLaws?: string[];
|
|
131
|
+
nowIso?: string;
|
|
132
|
+
}): IronLawRuntimeDocument;
|
|
133
|
+
export declare function ironLawsAgentsMdBlock(): string;
|
|
134
|
+
export declare function ironLawsSkillMarkdown(): string;
|