@colin4k1024/tsp 2.5.3 → 2.5.4

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 (49) hide show
  1. package/commands/goal.md +7 -1
  2. package/commands/heartbeat.md +34 -1
  3. package/commands/loop-start.md +34 -6
  4. package/commands/triage.md +8 -1
  5. package/hooks/README.md +2 -2
  6. package/hooks/harness-statusline.js +9 -30
  7. package/hooks/strategic-compact/README.md +11 -12
  8. package/manifests/install-components.json +40 -0
  9. package/manifests/install-modules.json +43 -0
  10. package/manifests/install-profiles.json +2 -0
  11. package/package.json +1 -1
  12. package/schemas/loop-spec.schema.json +124 -0
  13. package/scripts/hooks/pre-compact.js +39 -8
  14. package/scripts/hooks/session-start-goal-resume.js +3 -20
  15. package/scripts/hooks/suggest-compact.js +9 -115
  16. package/scripts/lib/completion-oracle.js +4 -27
  17. package/scripts/lib/context-window-state.js +129 -0
  18. package/scripts/lib/context-window.js +294 -0
  19. package/scripts/lib/heartbeat-scheduler.js +40 -25
  20. package/scripts/lib/install-targets/registry.js +1 -1
  21. package/scripts/lib/loop-oracle.js +5 -0
  22. package/scripts/lib/loop-spec.js +168 -0
  23. package/scripts/lib/loop-state-store.js +221 -0
  24. package/scripts/lib/transcript-usage.js +11 -1
  25. package/skills/goframe-v2/examples/practices/quick-demo/manifest/config/config.yaml +14 -14
  26. package/skills/repo-scan/SKILL.md +63 -63
  27. package/skills/strategic-compact/SKILL.md +11 -2
  28. package/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  29. package/scripts/__pycache__/build_platform_artifacts.cpython-311.pyc +0 -0
  30. package/scripts/__pycache__/install_platform.cpython-311.pyc +0 -0
  31. package/scripts/__pycache__/langfuse_trace.cpython-311.pyc +0 -0
  32. package/scripts/__pycache__/query_audit_logs.cpython-311.pyc +0 -0
  33. package/scripts/__pycache__/scan_leaked_keys.cpython-311.pyc +0 -0
  34. package/scripts/__pycache__/team_skills_platform.cpython-311.pyc +0 -0
  35. package/scripts/__pycache__/team_skills_platform.cpython-313.pyc +0 -0
  36. package/scripts/__pycache__/validate_library.cpython-311.pyc +0 -0
  37. package/scripts/__pycache__/validate_workflow_state.cpython-311.pyc +0 -0
  38. package/scripts/evolution/__pycache__/__init__.cpython-311.pyc +0 -0
  39. package/scripts/evolution/__pycache__/store.cpython-311.pyc +0 -0
  40. package/scripts/hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  41. package/scripts/hooks/__pycache__/mcp_health_check.cpython-311.pyc +0 -0
  42. package/scripts/hooks/__pycache__/observe.cpython-311.pyc +0 -0
  43. package/scripts/hooks/__pycache__/session_end.cpython-311.pyc +0 -0
  44. package/scripts/hooks/__pycache__/session_start.cpython-311.pyc +0 -0
  45. package/scripts/lib/__pycache__/audit_logger.cpython-311.pyc +0 -0
  46. package/scripts/lib/__pycache__/audit_query.cpython-311.pyc +0 -0
  47. package/scripts/lib/__pycache__/hook_contract.cpython-311.pyc +0 -0
  48. package/scripts/lib/__pycache__/memory_store.cpython-311.pyc +0 -0
  49. package/scripts/lib/__pycache__/utils.cpython-311.pyc +0 -0
package/commands/goal.md CHANGED
@@ -114,7 +114,13 @@ When ANY budget limit is hit, the goal escalates rather than continuing blindly.
114
114
 
115
115
  ## State Persistence
116
116
 
117
- Goals persist to `~/.claude/goals/{goalId}.json` (follows goal.schema.json).
117
+ Goals persist through the shared loop state store:
118
+
119
+ - `TSP_LOOP_STATE_DIR/goals/{goalId}.json` when `TSP_LOOP_STATE_DIR` is set
120
+ - `.tsp/loops/goals/{goalId}.json` for project-local loop state
121
+ - target defaults such as `~/.claude/loops/goals/{goalId}.json` or `~/.codex/loops/goals/{goalId}.json`
122
+
123
+ Legacy `~/.claude/goals/{goalId}.json` remains readable during migration.
118
124
 
119
125
  - Active goals survive session restarts
120
126
  - SessionStart hook displays active goal reminder
@@ -51,7 +51,39 @@ Execute all configured scans immediately (one-shot, no scheduling).
51
51
 
52
52
  ## Configuration
53
53
 
54
- Heartbeat reads from `.claude/heartbeat.yaml` in the project root:
54
+ Heartbeat reads configuration in this order:
55
+
56
+ 1. `.tsp/loop.yaml` — preferred Loop Engineering spec. Gates become discovery scans.
57
+ 2. `.tsp/heartbeat.yaml` — heartbeat-only config.
58
+ 3. `.claude/heartbeat.yaml` — legacy compatibility path.
59
+
60
+ Preferred `.tsp/loop.yaml`:
61
+
62
+ ```yaml
63
+ loop:
64
+ id: ci-triage
65
+ description: Keep CI failures triaged and fix machine-checkable failures.
66
+ cadence: 30m
67
+ skill: loop-ci-triage
68
+ stateFile: .tsp/loops/state/ci-triage.md
69
+ gates:
70
+ - name: library-validation
71
+ command: node scripts/validate-library.js
72
+ - name: tests
73
+ command: npm test
74
+ maker:
75
+ role: backend-engineer
76
+ writeAccess: true
77
+ checker:
78
+ role: qa-engineer
79
+ writeAccess: false
80
+ budget:
81
+ maxIterations: 10
82
+ maxDuration: 2h
83
+ maxDollars: 5
84
+ ```
85
+
86
+ Heartbeat-only config remains supported:
55
87
 
56
88
  ```yaml
57
89
  heartbeat:
@@ -119,6 +151,7 @@ Each scan result is classified into an action:
119
151
  - **`/triage`**: Heartbeat populates triage inbox from `triage` scan failures
120
152
  - **Budget**: Integrates with cost tracking; pauses if hourly budget exceeded
121
153
  - **Hooks**: Uses CronCreate/CronDelete for scheduling (7-day auto-expiry applies)
154
+ - **State**: Writes via the shared loop state store under `TSP_LOOP_STATE_DIR`, `.tsp/loops/`, or a target default
122
155
 
123
156
  ## Arguments
124
157
 
@@ -4,29 +4,57 @@ Start a managed autonomous loop pattern with safety defaults.
4
4
 
5
5
  ## Usage
6
6
 
7
- `/loop-start [pattern] [--mode safe|fast]`
7
+ `/loop-start [pattern] [--mode safe|fast] [--spec .tsp/loop.yaml]`
8
8
 
9
9
  - `pattern`: `sequential`, `continuous-pr`, `rfc-dag`, `infinite`
10
10
  - `--mode`:
11
11
  - `safe` (default): strict quality gates and checkpoints
12
12
  - `fast`: reduced gates for speed
13
+ - `--spec`: loop spec path, default `.tsp/loop.yaml`
14
+
15
+ ## Loop Engineering Intake
16
+
17
+ Do not start a scheduled loop until all four conditions hold:
18
+
19
+ 1. The task repeats often enough to amortize loop setup.
20
+ 2. Verification is automated with at least one hard gate.
21
+ 3. Budget is explicit: iterations, wall-clock time, and cost.
22
+ 4. Tool access is sufficient and bounded for the intended work.
23
+
24
+ If any condition fails, use `/quick`, `/verify`, or a one-shot script instead of
25
+ creating a loop.
13
26
 
14
27
  ## Flow
15
28
 
16
29
  1. Confirm repository state and branch strategy.
17
- 2. Select loop pattern and model tier strategy.
18
- 3. Enable required hooks/profile for the chosen mode.
19
- 4. Create loop plan and write runbook under `.claude/plans/`.
20
- 5. Print commands to start and monitor the loop.
30
+ 2. Validate `.tsp/loop.yaml` against `schemas/loop-spec.schema.json`.
31
+ 3. Initialize loop state under `TSP_LOOP_STATE_DIR`, `.tsp/loops/`, or the target adapter default.
32
+ 4. Select loop pattern and model tier strategy.
33
+ 5. Enable required hooks/profile for the chosen mode.
34
+ 6. Print commands to start and monitor the loop.
21
35
 
22
36
  ## Required Safety Checks
23
37
 
24
38
  - Verify tests pass before first loop iteration.
25
39
  - Ensure `ECC_HOOK_PROFILE` is not disabled globally.
26
- - Ensure loop has explicit stop condition.
40
+ - Ensure loop has at least one objective gate.
41
+ - Ensure loop has explicit stop conditions and budget limits.
42
+ - Ensure maker and checker are separate roles or agents.
43
+ - Ensure loop writes go through a branch or worktree, not protected main.
44
+
45
+ ## State
46
+
47
+ Loop runtime state is target-neutral:
48
+
49
+ - `TSP_LOOP_STATE_DIR` when set
50
+ - project-local `.tsp/loops/`
51
+ - target default, such as `~/.claude/loops/` or `~/.codex/loops/`
52
+
53
+ Legacy Claude goal and triage paths remain readable for migration.
27
54
 
28
55
  ## Arguments
29
56
 
30
57
  $ARGUMENTS:
31
58
  - `<pattern>` optional (`sequential|continuous-pr|rfc-dag|infinite`)
32
59
  - `--mode safe|fast` optional
60
+ - `--spec <path>` optional
@@ -86,7 +86,14 @@ Items enter triage from:
86
86
 
87
87
  ## Storage
88
88
 
89
- Inbox is stored as newline-delimited JSON at `~/.claude/triage/inbox.jsonl`.
89
+ Inbox is stored as newline-delimited JSON through the shared loop state store:
90
+
91
+ 1. `TSP_LOOP_STATE_DIR/triage/inbox.jsonl` when `TSP_LOOP_STATE_DIR` is set
92
+ 2. `.tsp/loops/triage/inbox.jsonl` for project-local state
93
+ 3. target defaults such as `~/.claude/loops/triage/inbox.jsonl` or `~/.codex/loops/triage/inbox.jsonl`
94
+
95
+ Legacy `~/.claude/triage/inbox.jsonl` remains a migration source for older Claude installs.
96
+
90
97
  This format is:
91
98
  - Append-only (safe for concurrent writes)
92
99
  - Human-readable (one JSON object per line)
package/hooks/README.md CHANGED
@@ -26,7 +26,7 @@ User request → Claude picks a tool → PreToolUse hook runs → Tool executes
26
26
  | **Git push reminder** | `Bash` | Reminds to review changes before `git push` | 0 (warns) |
27
27
  | **Pre-commit quality check** | `Bash` | Runs quality checks before `git commit`: lints staged files, validates commit message format when provided via `-m/--message`, detects console.log/debugger/secrets | 2 (blocks critical) / 0 (warns) |
28
28
  | **Doc file warning** | `Write` | Warns about non-standard `.md`/`.txt` files (allows README, CLAUDE, CONTRIBUTING, CHANGELOG, LICENSE, SKILL, docs/, skills/); cross-platform path handling | 0 (warns) |
29
- | **Strategic compact** | `*` | Suggests `/compact` when context usage crosses 70/85/95% thresholds, using Claude context-window metrics when available | 0 (warns) |
29
+ | **Strategic compact** | `*` | Suggests `/compact` when context usage crosses 65/70/85/95% thresholds, using CCometixLine-compatible remaining context first | 0 (warns) |
30
30
  | **InsAIts security monitor (opt-in)** | `Bash\|Write\|Edit\|MultiEdit` | Optional security scan for high-signal tool inputs. Disabled unless `ECC_ENABLE_INSAITS=1`. Blocks on critical findings, warns on non-critical, and writes audit log to `.insaits_audit_session.jsonl`. JS wrapper is the canonical hook entry; the Python monitor is an explicit third-party SDK exception. Requires `pip install insa-its`. [Details](../scripts/hooks/insaits-security-monitor.py) | 2 (blocks critical) / 0 (warns) |
31
31
 
32
32
  ### PostToolUse Hooks
@@ -48,7 +48,7 @@ User request → Claude picks a tool → PreToolUse hook runs → Tool executes
48
48
  |------|-------|-------------|
49
49
  | **Session start** | `SessionStart` | Loads previous context and detects package manager |
50
50
  | **PUA always-on restore** | `SessionStart` | Restores flavor, level, and failure count when `~/.claude/pua/config.json` enables always-on |
51
- | **Pre-compact** | `PreCompact` | Saves state before context compaction |
51
+ | **Pre-compact** | `PreCompact` | Saves state and increments project/session compact count before context compaction |
52
52
  | **PUA state snapshot** | `PreCompact` | Writes flavor, level, and failure count to the PUA builder journal |
53
53
  | **Console.log audit** | `Stop` | Checks all modified files for `console.log` after each response |
54
54
  | **Session summary** | `Stop` | Persists session state when transcript path is available |
@@ -15,9 +15,7 @@
15
15
  const fs = require('fs');
16
16
  const path = require('path');
17
17
  const os = require('os');
18
-
19
- // Autocompact buffer ~16.5% — GSD pattern, normalize to usable context
20
- const AUTO_COMPACT_BUFFER_PCT = 16.5;
18
+ const { resolveContextMetrics } = require('../scripts/lib/context-window');
21
19
 
22
20
  let input = '';
23
21
  // Guard: if stdin stalls (Windows/pipe issues), exit silently
@@ -31,8 +29,6 @@ process.stdin.on('end', () => {
31
29
  const session = data.session_id || '';
32
30
  const model = data.model?.display_name || 'Claude';
33
31
  const dir = data.cwd || data.workspace?.current_dir || process.cwd();
34
- const remaining = data.context_window?.remaining_percentage;
35
-
36
32
  // --- Resolve active role from harness state file ---
37
33
  const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
38
34
  const projectDir = process.env.CLAUDE_PROJECT_DIR || dir;
@@ -49,30 +45,9 @@ process.stdin.on('end', () => {
49
45
  // --- Context window metrics ---
50
46
  let ctxBar = '';
51
47
  let usedPct = null;
52
- if (remaining != null) {
53
- const usableRemaining = Math.max(0, ((remaining - AUTO_COMPACT_BUFFER_PCT) / (100 - AUTO_COMPACT_BUFFER_PCT)) * 100);
54
- usedPct = Math.max(0, Math.min(100, Math.round(100 - usableRemaining)));
55
- } else if (data.transcript_path) {
56
- // Parse actual token usage from transcript JSONL (CCometixLine approach)
57
- try {
58
- const { resolveTranscriptMetrics } = require('../scripts/lib/transcript-usage');
59
- const modelId = (data.model && data.model.id) || process.env.CLAUDE_MODEL || null;
60
- const metrics = resolveTranscriptMetrics(data.transcript_path, modelId);
61
- if (metrics) {
62
- usedPct = Math.max(0, Math.min(100, Math.round(metrics.usagePct)));
63
- }
64
- } catch (_) { /* ignore */ }
65
-
66
- // Final fallback: file size estimate
67
- if (usedPct == null) {
68
- try {
69
- const stat = fs.statSync(data.transcript_path);
70
- const estimatedTokens = Math.round(stat.size * 0.25);
71
- if (estimatedTokens > 0) {
72
- usedPct = Math.max(0, Math.min(100, Math.round((estimatedTokens / 200000) * 100)));
73
- }
74
- } catch (_) { /* ignore */ }
75
- }
48
+ const metrics = resolveContextMetrics(data);
49
+ if (metrics) {
50
+ usedPct = metrics.usagePct;
76
51
  }
77
52
 
78
53
  // Write bridge file for downstream hooks (suggest-compact, context-monitor)
@@ -81,8 +56,12 @@ process.stdin.on('end', () => {
81
56
  const bridgePath = path.join(os.tmpdir(), `harness-ctx-${session}.json`);
82
57
  fs.writeFileSync(bridgePath, JSON.stringify({
83
58
  session_id: session,
84
- remaining_percentage: remaining != null ? remaining : null,
59
+ remaining_percentage: metrics?.remainingPct ?? null,
60
+ remaining_tokens: metrics?.remainingTokens ?? null,
85
61
  used_pct: usedPct,
62
+ context_limit: metrics?.contextLimit ?? null,
63
+ compact_count: metrics?.compactCount ?? null,
64
+ context_source: metrics?.source ?? null,
86
65
  active_role: activeRole,
87
66
  timestamp: Math.floor(Date.now() / 1000),
88
67
  }));
@@ -13,12 +13,9 @@
13
13
  **触发时机**: 上下文压缩前自动调用
14
14
 
15
15
  **功能**:
16
- - 分析所有上下文元素
17
- - 按重要性分类:
18
- - `keep`: 高价值内容(决策、结论、pending items、验证结果)
19
- - `summarize`: 中等价值内容(长输出、对话历史)
20
- - `discard`: 低价值内容(工具结果、搜索结果、重复信息)
21
- - 生成摘要指令
16
+ - 保存 compaction 事件
17
+ - 递增 `.tsp/context/compact-state.json` 中的 session compact count 与 total compact count
18
+ - compact 轮次写入 session tmp 文件,供后续恢复和动态上下文策略使用
22
19
 
23
20
  **优先级规则**:
24
21
  - 总是保留的类型:`decision`, `conclusion`, `pending_item`, `task_output`, `verification_result`, `error_fix`
@@ -27,13 +24,14 @@
27
24
 
28
25
  ### suggest-compact.js
29
26
 
30
- **触发时机**: `hooks/hooks.json` 的 `pre:all:strategic-compact` 在工具调用前运行;当真实上下文使用超过 70% 时注入 `/compact` 建议。
27
+ **触发时机**: `hooks/hooks.json` 的 `pre:all:strategic-compact` 在工具调用前运行;当真实上下文使用超过 65% 时注入 `/compact` 建议。
31
28
 
32
29
  **功能**:
33
- - 优先读取 Claude hook 输入里的 `context_window.used_percentage` / `context_window.remaining_percentage`
34
- - 其次读取 `CLAUDE_CONTEXT_SIZE` / `CLAUDE_CONTEXT_LIMIT`
35
- - 最后读取 `harness-statusline.js` 写入的 `/tmp/harness-ctx-{session_id}.json`
36
- - 评估紧迫度:`low` (< 70%) | `medium` (70-85%) | `high` (85-95%) | `critical` (> 95%)
30
+ - 优先读取 CCometixLine-compatible remaining context:`TSP_CONTEXT_WINDOW_JSON` / `CCOMETIXLINE_CONTEXT_JSON`、`TSP_CONTEXT_WINDOW_FILE` / `CCOMETIXLINE_CONTEXT_FILE`、hook 输入的 `ccometixline.context_window` / `ccometixline_context_window`
31
+ - 其次读取 Claude hook 输入里的 `context_window.used_percentage` / `context_window.remaining_percentage`
32
+ - 再退回 transcript JSONL usage、`CLAUDE_CONTEXT_SIZE` / `CLAUDE_CONTEXT_LIMIT`、`harness-statusline.js` 写入的 `/tmp/harness-ctx-{session_id}.json`
33
+ - 评估紧迫度:`low` (< 65%) | `advisory` (65-70%) | `medium` (70-85%) | `high` (85-95%) | `critical` (> 95%)
34
+ - 输出 `compact_count`,让动态上下文策略知道当前已经经历过几轮 compact
37
35
  - 估算可节省的 token
38
36
  - 提供具体压缩建议:
39
37
  - 压缩早期对话历史
@@ -59,7 +57,8 @@
59
57
 
60
58
  | 使用率 | 紧迫度 | 建议操作 |
61
59
  |--------|--------|---------|
62
- | < 70% | low | 无需操作 |
60
+ | < 65% | low | 无需操作 |
61
+ | 65-70% | advisory | 控制上下文增长 |
63
62
  | 70-85% | medium | 建议压缩,可选择性执行 |
64
63
  | 85-95% | high | 强烈建议压缩 |
65
64
  | > 95% | critical | 必须立即压缩 |
@@ -193,6 +193,14 @@
193
193
  "orchestration"
194
194
  ]
195
195
  },
196
+ {
197
+ "id": "capability:loop-engineering",
198
+ "family": "capability",
199
+ "description": "Loop Engineering runtime: automation, skills, target-neutral state, hard gates, and maker/checker loop primitives.",
200
+ "modules": [
201
+ "loop-engineering"
202
+ ]
203
+ },
196
204
  {
197
205
  "id": "capability:workflow-engine",
198
206
  "family": "capability",
@@ -396,6 +404,38 @@
396
404
  "workflow-quality"
397
405
  ]
398
406
  },
407
+ {
408
+ "id": "skill:loop-heartbeat",
409
+ "family": "skill",
410
+ "description": "Scheduled discovery automation that routes findings to goals or triage.",
411
+ "modules": [
412
+ "loop-engineering"
413
+ ]
414
+ },
415
+ {
416
+ "id": "skill:goal-convergence",
417
+ "family": "skill",
418
+ "description": "Goal convergence patterns for bounded autonomous loops.",
419
+ "modules": [
420
+ "loop-engineering"
421
+ ]
422
+ },
423
+ {
424
+ "id": "skill:rework-loop",
425
+ "family": "skill",
426
+ "description": "Rework loop patterns for checker-driven iteration.",
427
+ "modules": [
428
+ "loop-engineering"
429
+ ]
430
+ },
431
+ {
432
+ "id": "skill:autonomous-loops",
433
+ "family": "skill",
434
+ "description": "Autonomous loop design and safety guidance.",
435
+ "modules": [
436
+ "loop-engineering"
437
+ ]
438
+ },
399
439
  {
400
440
  "id": "skill:strategic-compact",
401
441
  "family": "skill",
@@ -528,6 +528,49 @@
528
528
  "cost": "medium",
529
529
  "stability": "beta"
530
530
  },
531
+ {
532
+ "id": "loop-engineering",
533
+ "kind": "orchestration",
534
+ "description": "Loop Engineering command surface, target-neutral state runtime, loop spec schema, heartbeat discovery, and maker/checker loop skills.",
535
+ "paths": [
536
+ ".tsp/loop.example.yaml",
537
+ "commands/goal.md",
538
+ "commands/heartbeat.md",
539
+ "commands/triage.md",
540
+ "commands/loop-start.md",
541
+ "commands/loop-status.md",
542
+ "docs/runbooks/loop-engineering-usage.md",
543
+ "schemas/loop-spec.schema.json",
544
+ "scripts/lib/loop-state-store.js",
545
+ "scripts/lib/loop-spec.js",
546
+ "scripts/lib/loop-oracle.js",
547
+ "scripts/lib/completion-oracle.js",
548
+ "scripts/lib/heartbeat-scheduler.js",
549
+ "scripts/hooks/session-start-goal-resume.js",
550
+ "skills/loop-heartbeat",
551
+ "skills/goal-convergence",
552
+ "skills/rework-loop",
553
+ "skills/context-engineering",
554
+ "skills/context-lifecycle",
555
+ "templates/context-docs"
556
+ ],
557
+ "targets": [
558
+ "claude",
559
+ "cursor",
560
+ "codex",
561
+ "opencode",
562
+ "cangming",
563
+ "codebuddy",
564
+ "codewhale"
565
+ ],
566
+ "dependencies": [
567
+ "commands-core",
568
+ "platform-configs"
569
+ ],
570
+ "defaultInstall": false,
571
+ "cost": "medium",
572
+ "stability": "beta"
573
+ },
531
574
  {
532
575
  "id": "workflow-engine",
533
576
  "kind": "workflow",
@@ -78,6 +78,7 @@
78
78
  "workflow-engine",
79
79
  "workflow-defaults",
80
80
  "orchestration",
81
+ "loop-engineering",
81
82
  "swift-apple",
82
83
  "agentic-patterns",
83
84
  "devops-infra",
@@ -105,6 +106,7 @@
105
106
  "knowledge-graph",
106
107
  "workflow-engine",
107
108
  "workflow-defaults",
109
+ "loop-engineering",
108
110
  "design-prototyping",
109
111
  "enhanced-workflows",
110
112
  "rtk-optimization"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colin4k1024/tsp",
3
- "version": "2.5.3",
3
+ "version": "2.5.4",
4
4
  "description": "Open-source Team Skills Platform for role-based AI delivery workflows, shared skills, hooks, commands, and multi-platform installs.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,124 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://colin4k1024.github.io/tsp/schemas/loop-spec.schema.json",
4
+ "title": "TSP Loop Engineering Spec",
5
+ "type": "object",
6
+ "required": ["loop"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "loop": {
10
+ "type": "object",
11
+ "required": [
12
+ "id",
13
+ "description",
14
+ "cadence",
15
+ "skill",
16
+ "stateFile",
17
+ "gates",
18
+ "maker",
19
+ "checker",
20
+ "budget"
21
+ ],
22
+ "additionalProperties": false,
23
+ "properties": {
24
+ "id": {
25
+ "type": "string",
26
+ "minLength": 1
27
+ },
28
+ "description": {
29
+ "type": "string",
30
+ "minLength": 1
31
+ },
32
+ "cadence": {
33
+ "type": "string",
34
+ "pattern": "^[0-9]+[mhd]$"
35
+ },
36
+ "skill": {
37
+ "type": "string",
38
+ "minLength": 1
39
+ },
40
+ "stateFile": {
41
+ "type": "string",
42
+ "minLength": 1
43
+ },
44
+ "gates": {
45
+ "type": "array",
46
+ "minItems": 1,
47
+ "items": {
48
+ "type": "object",
49
+ "required": ["name", "command"],
50
+ "additionalProperties": false,
51
+ "properties": {
52
+ "name": {
53
+ "type": "string",
54
+ "minLength": 1
55
+ },
56
+ "command": {
57
+ "type": "string",
58
+ "minLength": 1
59
+ },
60
+ "description": {
61
+ "type": "string"
62
+ }
63
+ }
64
+ }
65
+ },
66
+ "maker": {
67
+ "$ref": "#/$defs/actor"
68
+ },
69
+ "checker": {
70
+ "$ref": "#/$defs/actor"
71
+ },
72
+ "budget": {
73
+ "type": "object",
74
+ "required": ["maxIterations", "maxDuration", "maxDollars"],
75
+ "additionalProperties": false,
76
+ "properties": {
77
+ "maxIterations": {
78
+ "type": "number",
79
+ "exclusiveMinimum": 0
80
+ },
81
+ "maxDuration": {
82
+ "type": "string",
83
+ "pattern": "^[0-9]+[mh]$"
84
+ },
85
+ "maxDollars": {
86
+ "type": "number",
87
+ "exclusiveMinimum": 0
88
+ }
89
+ }
90
+ },
91
+ "escalation": {
92
+ "type": "object",
93
+ "additionalProperties": false,
94
+ "properties": {
95
+ "onBudgetExhausted": {
96
+ "type": "string",
97
+ "enum": ["triage", "human", "stop"]
98
+ },
99
+ "onSecurityFinding": {
100
+ "type": "string",
101
+ "enum": ["human", "triage", "stop"]
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+ },
108
+ "$defs": {
109
+ "actor": {
110
+ "type": "object",
111
+ "required": ["role", "writeAccess"],
112
+ "additionalProperties": false,
113
+ "properties": {
114
+ "role": {
115
+ "type": "string",
116
+ "minLength": 1
117
+ },
118
+ "writeAccess": {
119
+ "type": "boolean"
120
+ }
121
+ }
122
+ }
123
+ }
124
+ }
@@ -18,8 +18,16 @@ const {
18
18
  appendFile,
19
19
  log
20
20
  } = require('../lib/utils');
21
+ const { recordCompactEvent } = require('../lib/context-window-state');
22
+
23
+ async function main(rawInput = '{}') {
24
+ let input = {};
25
+ try {
26
+ input = JSON.parse(rawInput || '{}');
27
+ } catch (_) {
28
+ input = {};
29
+ }
21
30
 
22
- async function main() {
23
31
  const sessionsDir = getSessionsDir();
24
32
  const compactionLog = path.join(sessionsDir, 'compaction-log.txt');
25
33
 
@@ -27,7 +35,18 @@ async function main() {
27
35
 
28
36
  // Log compaction event with timestamp
29
37
  const timestamp = getDateTimeString();
30
- appendFile(compactionLog, `[${timestamp}] Context compaction triggered\n`);
38
+ let compactEvent = null;
39
+ try {
40
+ compactEvent = recordCompactEvent(input, {
41
+ cwd: input.cwd || input.workspace?.current_dir || process.cwd(),
42
+ });
43
+ } catch (_) {
44
+ compactEvent = null;
45
+ }
46
+ const compactCountSuffix = compactEvent
47
+ ? ` (session compact #${compactEvent.sessionCompactCount}, total #${compactEvent.totalCompactCount})`
48
+ : '';
49
+ appendFile(compactionLog, `[${timestamp}] Context compaction triggered${compactCountSuffix}\n`);
31
50
 
32
51
  // If there's an active session file, note the compaction
33
52
  const sessions = findFiles(sessionsDir, '*-session.tmp');
@@ -35,14 +54,26 @@ async function main() {
35
54
  if (sessions.length > 0) {
36
55
  const activeSession = sessions[0].path;
37
56
  const timeStr = getTimeString();
38
- appendFile(activeSession, `\n---\n**[Compaction occurred at ${timeStr}]** - Context was summarized\n`);
57
+ const countNote = compactEvent ? ` (compact #${compactEvent.sessionCompactCount})` : '';
58
+ appendFile(activeSession, `\n---\n**[Compaction occurred at ${timeStr}${countNote}]** - Context was summarized\n`);
39
59
  }
40
60
 
41
- log('[PreCompact] State saved before compaction');
61
+ log(`[PreCompact] State saved before compaction${compactCountSuffix}`);
42
62
  process.exit(0);
43
63
  }
44
64
 
45
- main().catch(err => {
46
- console.error('[PreCompact] Error:', err.message);
47
- process.exit(0);
48
- });
65
+ if (require.main === module) {
66
+ let input = '';
67
+ const stdinTimeout = setTimeout(() => main(input), 5000);
68
+ process.stdin.setEncoding('utf8');
69
+ process.stdin.on('data', chunk => { input += chunk; });
70
+ process.stdin.on('end', () => {
71
+ clearTimeout(stdinTimeout);
72
+ main(input).catch(err => {
73
+ console.error('[PreCompact] Error:', err.message);
74
+ process.exit(0);
75
+ });
76
+ });
77
+ }
78
+
79
+ module.exports = { main };
@@ -11,34 +11,17 @@
11
11
  * session start output so the user knows they can resume with `/goal resume`.
12
12
  *
13
13
  * Behavior:
14
- * 1. Scans ~/.claude/goals/ for goal files with state "active" or "paused"
14
+ * 1. Scans the TSP loop state goals store for active or paused goal files
15
15
  * 2. If found, appends a summary to the hook output
16
16
  * 3. Non-blocking: failures are silently ignored
17
17
  */
18
18
 
19
19
  const fs = require('fs');
20
- const path = require('path');
21
-
22
- function getGoalsDir() {
23
- const home = process.env.HOME || process.env.USERPROFILE || '';
24
- return path.join(home, '.claude', 'goals');
25
- }
20
+ const { listGoals } = require('../lib/loop-state-store');
26
21
 
27
22
  function scanActiveGoals() {
28
- const dir = getGoalsDir();
29
- if (!fs.existsSync(dir)) return [];
30
-
31
23
  try {
32
- return fs.readdirSync(dir)
33
- .filter(f => f.endsWith('.json'))
34
- .map(f => {
35
- try {
36
- return JSON.parse(fs.readFileSync(path.join(dir, f), 'utf-8'));
37
- } catch {
38
- return null;
39
- }
40
- })
41
- .filter(g => g && (g.state === 'active' || g.state === 'paused'))
24
+ return [...listGoals('active'), ...listGoals('paused')]
42
25
  .sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
43
26
  } catch {
44
27
  return [];