cclaw-cli 0.5.17 → 0.7.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.
package/dist/cli.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import type { HarnessId } from "./types.js";
2
+ import type { FlowTrack, HarnessId, InitProfile } from "./types.js";
3
3
  type CommandName = "init" | "sync" | "doctor" | "upgrade" | "uninstall" | "archive";
4
4
  interface ParsedArgs {
5
5
  command?: CommandName;
6
6
  harnesses?: HarnessId[];
7
+ track?: FlowTrack;
8
+ profile?: InitProfile;
7
9
  reconcileGates?: boolean;
8
10
  archiveName?: string;
9
11
  showHelp?: boolean;
@@ -11,5 +13,7 @@ interface ParsedArgs {
11
13
  }
12
14
  export declare function usage(): string;
13
15
  declare function parseHarnesses(raw: string): HarnessId[];
16
+ declare function parseTrack(raw: string): FlowTrack;
17
+ declare function parseProfile(raw: string): InitProfile;
14
18
  declare function parseArgs(argv: string[]): ParsedArgs;
15
- export { parseArgs, parseHarnesses };
19
+ export { parseArgs, parseHarnesses, parseTrack, parseProfile };
package/dist/cli.js CHANGED
@@ -3,11 +3,12 @@ import { readFileSync, realpathSync } from "node:fs";
3
3
  import process from "node:process";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { HARNESS_IDS } from "./types.js";
6
+ import { FLOW_TRACKS, HARNESS_IDS, INIT_PROFILES } from "./types.js";
7
7
  import { doctorChecks, doctorSucceeded } from "./doctor.js";
8
8
  import { initCclaw, syncCclaw, uninstallCclaw, upgradeCclaw } from "./install.js";
9
9
  import { error, info } from "./logger.js";
10
10
  import { archiveRun } from "./runs.js";
11
+ import { RUNTIME_ROOT } from "./constants.js";
11
12
  const INSTALLER_COMMANDS = ["init", "sync", "doctor", "upgrade", "uninstall", "archive"];
12
13
  export function usage() {
13
14
  return `cclaw - installer-first flow toolkit
@@ -19,7 +20,9 @@ Usage:
19
20
 
20
21
  Commands:
21
22
  init Bootstrap .cclaw runtime, state, and harness shims in this project.
22
- Flags: --harnesses=<list> Comma list of harnesses (claude,cursor,opencode,codex).
23
+ Flags: --profile=<id> Pre-fill defaults. One of: minimal | standard | full. Default: standard.
24
+ --harnesses=<list> Comma list of harnesses (claude,cursor,opencode,codex). Overrides the profile default.
25
+ --track=<id> Flow track for new runs (standard | quick). Overrides the profile default.
23
26
  sync Regenerate harness shim files from the current .cclaw config (non-destructive).
24
27
  doctor Run health checks against the local .cclaw runtime. Exit code 2 on failure.
25
28
  Flags: --reconcile-gates Recompute current-stage gate evidence before checks.
@@ -77,6 +80,20 @@ function parseHarnesses(raw) {
77
80
  }
78
81
  return requested;
79
82
  }
83
+ function parseTrack(raw) {
84
+ const trimmed = raw.trim();
85
+ if (!FLOW_TRACKS.includes(trimmed)) {
86
+ throw new Error(`Unknown track: ${trimmed}. Supported: ${FLOW_TRACKS.join(", ")}`);
87
+ }
88
+ return trimmed;
89
+ }
90
+ function parseProfile(raw) {
91
+ const trimmed = raw.trim();
92
+ if (!INIT_PROFILES.includes(trimmed)) {
93
+ throw new Error(`Unknown profile: ${trimmed}. Supported: ${INIT_PROFILES.join(", ")}`);
94
+ }
95
+ return trimmed;
96
+ }
80
97
  function parseArgs(argv) {
81
98
  const parsed = {};
82
99
  const helpFlag = argv.find((arg) => arg === "--help" || arg === "-h");
@@ -96,6 +113,14 @@ function parseArgs(argv) {
96
113
  parsed.harnesses = parseHarnesses(flag.replace("--harnesses=", ""));
97
114
  continue;
98
115
  }
116
+ if (flag.startsWith("--track=")) {
117
+ parsed.track = parseTrack(flag.replace("--track=", ""));
118
+ continue;
119
+ }
120
+ if (flag.startsWith("--profile=")) {
121
+ parsed.profile = parseProfile(flag.replace("--profile=", ""));
122
+ continue;
123
+ }
99
124
  if (flag === "--reconcile-gates") {
100
125
  parsed.reconcileGates = true;
101
126
  continue;
@@ -123,9 +148,14 @@ async function runCommand(parsed, ctx) {
123
148
  if (command === "init") {
124
149
  await initCclaw({
125
150
  projectRoot: ctx.cwd,
126
- harnesses: parsed.harnesses
151
+ harnesses: parsed.harnesses,
152
+ track: parsed.track,
153
+ profile: parsed.profile
127
154
  });
128
- info(ctx, "Initialized .cclaw runtime and generated harness shims");
155
+ const profileNote = parsed.profile ? ` profile=${parsed.profile}` : "";
156
+ const trackNote = parsed.track ? ` track=${parsed.track}` : "";
157
+ const suffix = profileNote || trackNote ? ` (${(profileNote + trackNote).trim()})` : "";
158
+ info(ctx, `Initialized .cclaw runtime and generated harness shims${suffix}`);
129
159
  return 0;
130
160
  }
131
161
  if (command === "sync") {
@@ -153,6 +183,16 @@ async function runCommand(parsed, ctx) {
153
183
  ? ` Snapshotted ${archived.snapshottedStateFiles.length} state file(s) under ${archived.archivePath}/state and wrote archive-manifest.json.`
154
184
  : "";
155
185
  info(ctx, `Archived active artifacts to ${archived.archivePath}. Flow state reset to brainstorm.${snapshotSummary}`);
186
+ const k = archived.knowledge;
187
+ if (k.overThreshold) {
188
+ info(ctx, `Knowledge curation recommended: ${k.knowledgePath} now has ${k.activeEntryCount} active entries (soft threshold ${k.softThreshold}). Run \`/cc-learn curate\` to plan a soft-archive of stale/duplicate entries to ${RUNTIME_ROOT}/knowledge.archive.md.`);
189
+ }
190
+ else if (k.activeEntryCount > 0) {
191
+ info(ctx, `Knowledge: ${k.activeEntryCount}/${k.softThreshold} active entries. Run \`/cc-learn curate\` if you want a sweep before the next run.`);
192
+ }
193
+ else {
194
+ info(ctx, `Knowledge: 0 active entries in ${k.knowledgePath}. Capture lessons from this run with \`/cc-learn add\` before they fade.`);
195
+ }
156
196
  return 0;
157
197
  }
158
198
  await uninstallCclaw(ctx.cwd);
@@ -190,4 +230,4 @@ function isDirectExecution() {
190
230
  if (isDirectExecution()) {
191
231
  void main();
192
232
  }
193
- export { parseArgs, parseHarnesses };
233
+ export { parseArgs, parseHarnesses, parseTrack, parseProfile };
package/dist/config.d.ts CHANGED
@@ -1,5 +1,15 @@
1
- import type { HarnessId, VibyConfig } from "./types.js";
1
+ import type { FlowTrack, HarnessId, InitProfile, LanguageRulePack, VibyConfig } from "./types.js";
2
2
  export declare function configPath(projectRoot: string): string;
3
- export declare function createDefaultConfig(harnesses?: HarnessId[]): VibyConfig;
3
+ export declare function createDefaultConfig(harnesses?: HarnessId[], defaultTrack?: FlowTrack): VibyConfig;
4
+ /**
5
+ * Build a VibyConfig for a named init profile. Profile defaults are applied
6
+ * first, then any explicit overrides (CLI flags) win. This keeps the profile
7
+ * contract deterministic and testable.
8
+ */
9
+ export declare function createProfileConfig(profile: InitProfile, overrides?: {
10
+ harnesses?: HarnessId[];
11
+ defaultTrack?: FlowTrack;
12
+ languageRulePacks?: LanguageRulePack[];
13
+ }): VibyConfig;
4
14
  export declare function readConfig(projectRoot: string): Promise<VibyConfig>;
5
15
  export declare function writeConfig(projectRoot: string, config: VibyConfig): Promise<void>;
package/dist/config.js CHANGED
@@ -3,17 +3,23 @@ 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
5
  import { exists, writeFileSafe } from "./fs-utils.js";
6
- import { HARNESS_IDS } from "./types.js";
6
+ import { FLOW_TRACKS, HARNESS_IDS, LANGUAGE_RULE_PACKS } from "./types.js";
7
7
  const CONFIG_PATH = `${RUNTIME_ROOT}/config.yaml`;
8
8
  const HARNESS_ID_SET = new Set(HARNESS_IDS);
9
+ const FLOW_TRACK_SET = new Set(FLOW_TRACKS);
10
+ const LANGUAGE_RULE_PACK_SET = new Set(LANGUAGE_RULE_PACKS);
9
11
  const SUPPORTED_HARNESSES_TEXT = HARNESS_IDS.join(", ");
12
+ const SUPPORTED_TRACKS_TEXT = FLOW_TRACKS.join(", ");
13
+ const SUPPORTED_LANGUAGE_RULE_PACKS_TEXT = LANGUAGE_RULE_PACKS.join(", ");
10
14
  const ALLOWED_CONFIG_KEYS = new Set([
11
15
  "version",
12
16
  "flowVersion",
13
17
  "harnesses",
14
18
  "autoAdvance",
15
19
  "promptGuardMode",
16
- "gitHookGuards"
20
+ "gitHookGuards",
21
+ "defaultTrack",
22
+ "languageRulePacks"
17
23
  ]);
18
24
  function configFixExample() {
19
25
  return `harnesses:
@@ -23,22 +29,66 @@ function configFixExample() {
23
29
  function configValidationError(configFilePath, reason) {
24
30
  return new Error(`Invalid cclaw config at ${configFilePath}: ${reason}\n` +
25
31
  `Supported harnesses: ${SUPPORTED_HARNESSES_TEXT}\n` +
32
+ `Supported tracks: ${SUPPORTED_TRACKS_TEXT}\n` +
33
+ `Supported languageRulePacks: ${SUPPORTED_LANGUAGE_RULE_PACKS_TEXT}\n` +
26
34
  `Example config:\n${configFixExample()}\n` +
27
35
  `After fixing, run: cclaw sync`);
28
36
  }
29
37
  export function configPath(projectRoot) {
30
38
  return path.join(projectRoot, CONFIG_PATH);
31
39
  }
32
- export function createDefaultConfig(harnesses = DEFAULT_HARNESSES) {
40
+ export function createDefaultConfig(harnesses = DEFAULT_HARNESSES, defaultTrack = "standard") {
33
41
  return {
34
42
  version: CCLAW_VERSION,
35
43
  flowVersion: FLOW_VERSION,
36
44
  harnesses,
37
45
  autoAdvance: false,
38
46
  promptGuardMode: "advisory",
39
- gitHookGuards: false
47
+ gitHookGuards: false,
48
+ defaultTrack,
49
+ languageRulePacks: []
40
50
  };
41
51
  }
52
+ /**
53
+ * Build a VibyConfig for a named init profile. Profile defaults are applied
54
+ * first, then any explicit overrides (CLI flags) win. This keeps the profile
55
+ * contract deterministic and testable.
56
+ */
57
+ export function createProfileConfig(profile, overrides = {}) {
58
+ const base = createDefaultConfig();
59
+ switch (profile) {
60
+ case "minimal":
61
+ return {
62
+ ...base,
63
+ harnesses: overrides.harnesses ?? ["claude"],
64
+ autoAdvance: false,
65
+ promptGuardMode: "advisory",
66
+ gitHookGuards: false,
67
+ defaultTrack: overrides.defaultTrack ?? "quick",
68
+ languageRulePacks: overrides.languageRulePacks ?? []
69
+ };
70
+ case "standard":
71
+ return {
72
+ ...base,
73
+ harnesses: overrides.harnesses ?? DEFAULT_HARNESSES,
74
+ autoAdvance: false,
75
+ promptGuardMode: "advisory",
76
+ gitHookGuards: false,
77
+ defaultTrack: overrides.defaultTrack ?? "standard",
78
+ languageRulePacks: overrides.languageRulePacks ?? []
79
+ };
80
+ case "full":
81
+ return {
82
+ ...base,
83
+ harnesses: overrides.harnesses ?? DEFAULT_HARNESSES,
84
+ autoAdvance: false,
85
+ promptGuardMode: "strict",
86
+ gitHookGuards: true,
87
+ defaultTrack: overrides.defaultTrack ?? "standard",
88
+ languageRulePacks: overrides.languageRulePacks ?? [...LANGUAGE_RULE_PACKS]
89
+ };
90
+ }
91
+ }
42
92
  export async function readConfig(projectRoot) {
43
93
  const fullPath = configPath(projectRoot);
44
94
  if (!(await exists(fullPath))) {
@@ -89,13 +139,37 @@ export async function readConfig(projectRoot) {
89
139
  throw configValidationError(fullPath, `"gitHookGuards" must be a boolean`);
90
140
  }
91
141
  const gitHookGuards = typeof gitHookGuardsRaw === "boolean" ? gitHookGuardsRaw : false;
142
+ const defaultTrackRaw = parsed.defaultTrack;
143
+ if (Object.prototype.hasOwnProperty.call(parsed, "defaultTrack") &&
144
+ (typeof defaultTrackRaw !== "string" || !FLOW_TRACK_SET.has(defaultTrackRaw))) {
145
+ throw configValidationError(fullPath, `"defaultTrack" must be one of: ${SUPPORTED_TRACKS_TEXT}`);
146
+ }
147
+ const defaultTrack = typeof defaultTrackRaw === "string" && FLOW_TRACK_SET.has(defaultTrackRaw)
148
+ ? defaultTrackRaw
149
+ : "standard";
150
+ const languageRulePacksRaw = parsed.languageRulePacks;
151
+ const hasLanguageRulePacksField = Object.prototype.hasOwnProperty.call(parsed, "languageRulePacks");
152
+ if (hasLanguageRulePacksField && !Array.isArray(languageRulePacksRaw)) {
153
+ throw configValidationError(fullPath, `"languageRulePacks" must be an array`);
154
+ }
155
+ const rawPacks = (languageRulePacksRaw ?? []);
156
+ const invalidPacks = rawPacks.filter((pack) => typeof pack !== "string" || !LANGUAGE_RULE_PACK_SET.has(pack));
157
+ if (invalidPacks.length > 0) {
158
+ const formatted = invalidPacks
159
+ .map((item) => (typeof item === "string" ? item : JSON.stringify(item)))
160
+ .join(", ");
161
+ throw configValidationError(fullPath, `unknown languageRulePacks id(s): ${formatted}`);
162
+ }
163
+ const languageRulePacks = [...new Set(rawPacks)];
92
164
  return {
93
165
  version: parsed.version ?? CCLAW_VERSION,
94
166
  flowVersion: parsed.flowVersion ?? FLOW_VERSION,
95
167
  harnesses,
96
168
  autoAdvance,
97
169
  promptGuardMode,
98
- gitHookGuards
170
+ gitHookGuards,
171
+ defaultTrack,
172
+ languageRulePacks
99
173
  };
100
174
  }
101
175
  export async function writeConfig(projectRoot, config) {
@@ -4,9 +4,9 @@ export declare const RUNTIME_ROOT = ".cclaw";
4
4
  export declare const CCLAW_VERSION = "0.1.1";
5
5
  export declare const FLOW_VERSION = "1.0.0";
6
6
  export declare const DEFAULT_HARNESSES: HarnessId[];
7
- export declare const REQUIRED_DIRS: readonly [".cclaw", ".cclaw/commands", ".cclaw/skills", ".cclaw/contexts", ".cclaw/templates", ".cclaw/artifacts", ".cclaw/state", ".cclaw/runs", ".cclaw/rules", ".cclaw/adapters", ".cclaw/agents", ".cclaw/hooks"];
7
+ export declare const REQUIRED_DIRS: readonly [".cclaw", ".cclaw/commands", ".cclaw/skills", ".cclaw/contexts", ".cclaw/templates", ".cclaw/artifacts", ".cclaw/state", ".cclaw/runs", ".cclaw/rules", ".cclaw/adapters", ".cclaw/agents", ".cclaw/hooks", ".cclaw/custom-skills"];
8
8
  export declare const REQUIRED_GITIGNORE_PATTERNS: readonly ["# cclaw generated artifacts", ".cclaw/", ".claude/commands/cc-*.md", ".claude/commands/cc.md", ".cursor/commands/cc-*.md", ".cursor/commands/cc.md", ".opencode/commands/cc-*.md", ".opencode/commands/cc.md", ".codex/commands/cc-*.md", ".codex/commands/cc.md", ".claude/hooks/hooks.json", ".cursor/hooks.json", ".codex/hooks.json", ".opencode/plugins/cclaw-plugin.mjs", ".cursor/rules/cclaw-workflow.mdc"];
9
9
  export declare const COMMAND_FILE_ORDER: FlowStage[];
10
- export declare const UTILITY_COMMANDS: readonly ["learn", "next"];
10
+ export declare const UTILITY_COMMANDS: readonly ["learn", "next", "status"];
11
11
  export declare const SUBAGENT_SKILL_FOLDERS: readonly ["subagent-dev", "parallel-dispatch"];
12
12
  export type UtilityCommand = (typeof UTILITY_COMMANDS)[number];
package/dist/constants.js CHANGED
@@ -20,7 +20,8 @@ export const REQUIRED_DIRS = [
20
20
  `${RUNTIME_ROOT}/rules`,
21
21
  `${RUNTIME_ROOT}/adapters`,
22
22
  `${RUNTIME_ROOT}/agents`,
23
- `${RUNTIME_ROOT}/hooks`
23
+ `${RUNTIME_ROOT}/hooks`,
24
+ `${RUNTIME_ROOT}/custom-skills`
24
25
  ];
25
26
  export const REQUIRED_GITIGNORE_PATTERNS = [
26
27
  "# cclaw generated artifacts",
@@ -49,7 +50,7 @@ export const COMMAND_FILE_ORDER = [
49
50
  "review",
50
51
  "ship"
51
52
  ];
52
- export const UTILITY_COMMANDS = ["learn", "next"];
53
+ export const UTILITY_COMMANDS = ["learn", "next", "status"];
53
54
  export const SUBAGENT_SKILL_FOLDERS = [
54
55
  "subagent-dev",
55
56
  "parallel-dispatch"
@@ -11,6 +11,7 @@ export interface HookRuntimeOptions {
11
11
  export declare const RUNTIME_SHELL_DETECT_ROOT = "HARNESS=\"codex\"\nif [ -n \"${CLAUDE_PROJECT_DIR:-}\" ]; then\n HARNESS=\"claude\"\nelif [ -n \"${CURSOR_PROJECT_DIR:-}\" ] || [ -n \"${CURSOR_PROJECT_ROOT:-}\" ]; then\n HARNESS=\"cursor\"\nelif [ -n \"${OPENCODE_PROJECT_DIR:-}\" ] || [ -n \"${OPENCODE_PROJECT_ROOT:-}\" ]; then\n HARNESS=\"opencode\"\nfi\n\nROOT=\"\"\nfor candidate in \"${CCLAW_PROJECT_ROOT:-}\" \"${CLAUDE_PROJECT_DIR:-}\" \"${CURSOR_PROJECT_DIR:-}\" \"${CURSOR_PROJECT_ROOT:-}\" \"${OPENCODE_PROJECT_DIR:-}\" \"${OPENCODE_PROJECT_ROOT:-}\" \"${PWD:-}\"; do\n if [ -n \"$candidate\" ] && [ -d \"$candidate/.cclaw\" ]; then\n ROOT=\"$candidate\"\n break\n fi\ndone\nif [ -z \"$ROOT\" ]; then\n ROOT=\"${CCLAW_PROJECT_ROOT:-${CLAUDE_PROJECT_DIR:-${CURSOR_PROJECT_DIR:-${CURSOR_PROJECT_ROOT:-${OPENCODE_PROJECT_DIR:-${OPENCODE_PROJECT_ROOT:-${PWD}}}}}}}\"\nfi";
12
12
  export declare function sessionStartScript(_options?: HookRuntimeOptions): string;
13
13
  export declare function stopCheckpointScript(): string;
14
+ export declare function preCompactScript(): string;
14
15
  export { claudeHooksJsonWithObservation as claudeHooksJson } from "./observe.js";
15
16
  export { cursorHooksJsonWithObservation as cursorHooksJson } from "./observe.js";
16
17
  export { codexHooksJsonWithObservation as codexHooksJson } from "./observe.js";
@@ -616,6 +616,151 @@ case "$HARNESS" in
616
616
  esac
617
617
  `;
618
618
  }
619
+ export function preCompactScript() {
620
+ return `#!/usr/bin/env bash
621
+ # cclaw pre-compact hook — generated by cclaw sync
622
+ # Persists a session digest before the harness compacts/clears context, so the
623
+ # next session-start hook can restore the most important state without the agent
624
+ # having to re-derive it from scratch.
625
+ set -uo pipefail
626
+
627
+ ${DETECT_ROOT}
628
+
629
+ INPUT=$(cat 2>/dev/null || echo '{}')
630
+
631
+ STATE_DIR="$ROOT/${RUNTIME_ROOT}/state"
632
+ STATE_FILE="$STATE_DIR/flow-state.json"
633
+ DELEGATION_FILE="$STATE_DIR/delegation-log.json"
634
+ KNOWLEDGE_FILE="$ROOT/${RUNTIME_ROOT}/knowledge.md"
635
+ DIGEST_FILE="$STATE_DIR/session-digest.md"
636
+ DIGEST_TMP="$STATE_DIR/session-digest.md.tmp.$$"
637
+
638
+ mkdir -p "$STATE_DIR" 2>/dev/null || true
639
+
640
+ cleanup_digest_tmp() {
641
+ rm -f "$DIGEST_TMP" 2>/dev/null || true
642
+ }
643
+ trap cleanup_digest_tmp EXIT INT TERM
644
+
645
+ STAGE="none"
646
+ TRACK="standard"
647
+ COMPLETED="0"
648
+ SKIPPED=""
649
+ ACTIVE_RUN="none"
650
+ PASSED_GATES=""
651
+ BLOCKED_GATES=""
652
+
653
+ if [ -f "$STATE_FILE" ]; then
654
+ if command -v jq >/dev/null 2>&1; then
655
+ STAGE=$(jq -r '.currentStage // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
656
+ TRACK=$(jq -r '.track // "standard"' "$STATE_FILE" 2>/dev/null || echo "standard")
657
+ COMPLETED=$(jq -r '(.completedStages // []) | length' "$STATE_FILE" 2>/dev/null || echo "0")
658
+ SKIPPED=$(jq -r '(.skippedStages // []) | join(",")' "$STATE_FILE" 2>/dev/null || echo "")
659
+ ACTIVE_RUN=$(jq -r '.activeRunId // "none"' "$STATE_FILE" 2>/dev/null || echo "none")
660
+ PASSED_GATES=$(jq -r --arg stage "$STAGE" '(.stageGates[$stage].passed // []) | join(",")' "$STATE_FILE" 2>/dev/null || echo "")
661
+ BLOCKED_GATES=$(jq -r --arg stage "$STAGE" '(.stageGates[$stage].blocked // []) | join(",")' "$STATE_FILE" 2>/dev/null || echo "")
662
+ elif command -v python3 >/dev/null 2>&1; then
663
+ OUTPUT=$(python3 - "$STATE_FILE" <<'PY'
664
+ import json, sys
665
+ try:
666
+ with open(sys.argv[1], "r", encoding="utf-8") as fh:
667
+ data = json.load(fh)
668
+ except Exception:
669
+ data = {}
670
+ stage = data.get("currentStage") or "none"
671
+ track = data.get("track") or "standard"
672
+ completed = data.get("completedStages") or []
673
+ skipped = data.get("skippedStages") or []
674
+ run = data.get("activeRunId") or "none"
675
+ gates = (data.get("stageGates") or {}).get(stage) or {}
676
+ passed = gates.get("passed") or []
677
+ blocked = gates.get("blocked") or []
678
+ print(stage)
679
+ print(track)
680
+ print(len(completed) if isinstance(completed, list) else 0)
681
+ print(",".join(skipped) if isinstance(skipped, list) else "")
682
+ print(run)
683
+ print(",".join(passed) if isinstance(passed, list) else "")
684
+ print(",".join(blocked) if isinstance(blocked, list) else "")
685
+ PY
686
+ )
687
+ {
688
+ IFS= read -r STAGE
689
+ IFS= read -r TRACK
690
+ IFS= read -r COMPLETED
691
+ IFS= read -r SKIPPED
692
+ IFS= read -r ACTIVE_RUN
693
+ IFS= read -r PASSED_GATES
694
+ IFS= read -r BLOCKED_GATES
695
+ } <<EOF
696
+ $OUTPUT
697
+ EOF
698
+ fi
699
+ fi
700
+
701
+ DELEGATION_PENDING=""
702
+ if [ -f "$DELEGATION_FILE" ] && command -v jq >/dev/null 2>&1; then
703
+ DELEGATION_PENDING=$(jq -r --arg stage "$STAGE" '
704
+ (.entries // [])
705
+ | map(select((.stage // "") == $stage and (.status // "") != "completed" and (.status // "") != "waived"))
706
+ | map(.agent // "unknown")
707
+ | unique
708
+ | join(",")
709
+ ' "$DELEGATION_FILE" 2>/dev/null || echo "")
710
+ fi
711
+
712
+ KNOWLEDGE_TAIL=""
713
+ if [ -f "$KNOWLEDGE_FILE" ] && [ -s "$KNOWLEDGE_FILE" ]; then
714
+ KNOWLEDGE_TAIL=$(tail -n 12 "$KNOWLEDGE_FILE" 2>/dev/null || echo "")
715
+ fi
716
+
717
+ GIT_HEAD=""
718
+ GIT_BRANCH=""
719
+ GIT_DIRTY="unknown"
720
+ if command -v git >/dev/null 2>&1 && git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
721
+ GIT_HEAD=$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo "")
722
+ GIT_BRANCH=$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
723
+ if [ -n "$(git -C "$ROOT" status --porcelain 2>/dev/null)" ]; then
724
+ GIT_DIRTY="dirty"
725
+ else
726
+ GIT_DIRTY="clean"
727
+ fi
728
+ fi
729
+
730
+ TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
731
+
732
+ {
733
+ printf '# Session Digest\n'
734
+ printf '_Generated by pre-compact hook at %s_\n\n' "$TS"
735
+ printf '## Flow snapshot\n'
736
+ printf '- track: %s\n' "$TRACK"
737
+ printf '- current stage: %s\n' "$STAGE"
738
+ printf '- completed: %s stages\n' "$COMPLETED"
739
+ printf '- skipped: %s\n' "\${SKIPPED:-(none)}"
740
+ printf '- run: %s\n\n' "$ACTIVE_RUN"
741
+ printf '## Gates (current stage)\n'
742
+ printf '- passed: %s\n' "\${PASSED_GATES:-(none)}"
743
+ printf '- blocked: %s\n\n' "\${BLOCKED_GATES:-(none)}"
744
+ printf '## Outstanding delegations\n'
745
+ printf '- pending: %s\n\n' "\${DELEGATION_PENDING:-(none)}"
746
+ printf '## Git\n'
747
+ printf '- branch: %s\n' "\${GIT_BRANCH:-(unknown)}"
748
+ printf '- head: %s\n' "\${GIT_HEAD:-(unknown)}"
749
+ printf '- worktree: %s\n\n' "$GIT_DIRTY"
750
+ if [ -n "$KNOWLEDGE_TAIL" ]; then
751
+ printf '## Knowledge tail\n'
752
+ printf '%s\n' "$KNOWLEDGE_TAIL"
753
+ fi
754
+ } > "$DIGEST_TMP" 2>/dev/null || true
755
+
756
+ if [ -s "$DIGEST_TMP" ]; then
757
+ mv "$DIGEST_TMP" "$DIGEST_FILE" 2>/dev/null || rm -f "$DIGEST_TMP" 2>/dev/null || true
758
+ fi
759
+
760
+ trap - EXIT INT TERM
761
+ exit 0
762
+ `;
763
+ }
619
764
  // ---------------------------------------------------------------------------
620
765
  // hooks.json generators are defined in observe.ts (shared across harnesses).
621
766
  // ---------------------------------------------------------------------------