@tekyzinc/gsd-t 3.29.11 → 4.0.12

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 (108) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/bin/gsd-t-parallel.cjs +64 -13
  3. package/bin/gsd-t.js +48 -376
  4. package/bin/journey-coverage.cjs +6 -3
  5. package/bin/parallel-cli.cjs +13 -1
  6. package/commands/gsd-t-debug.md +47 -533
  7. package/commands/gsd-t-design-decompose.md +24 -497
  8. package/commands/gsd-t-doc-ripple.md +23 -139
  9. package/commands/gsd-t-execute.md +41 -958
  10. package/commands/gsd-t-help.md +0 -2
  11. package/commands/gsd-t-impact.md +26 -295
  12. package/commands/gsd-t-integrate.md +44 -369
  13. package/commands/gsd-t-milestone.md +25 -124
  14. package/commands/gsd-t-partition.md +28 -539
  15. package/commands/gsd-t-plan.md +26 -472
  16. package/commands/gsd-t-prd.md +25 -311
  17. package/commands/gsd-t-quick.md +1 -1
  18. package/commands/gsd-t-resume.md +0 -33
  19. package/commands/gsd-t-verify.md +52 -569
  20. package/commands/gsd-t-wave.md +41 -430
  21. package/package.json +1 -1
  22. package/scripts/gsd-t-calibration-hook.js +3 -1
  23. package/scripts/hooks/pre-commit-journey-coverage +0 -2
  24. package/scripts/hooks/pre-commit-playwright-gate +0 -2
  25. package/templates/CLAUDE-global.md +43 -173
  26. package/templates/CLAUDE-project.md +118 -104
  27. package/templates/prompts/design-verify-subagent.md +3 -0
  28. package/templates/prompts/qa-subagent.md +3 -0
  29. package/templates/prompts/red-team-subagent.md +5 -2
  30. package/templates/workflows/_lib.js +176 -0
  31. package/templates/workflows/gsd-t-debug.workflow.js +86 -0
  32. package/templates/workflows/gsd-t-execute.workflow.js +206 -0
  33. package/templates/workflows/gsd-t-integrate.workflow.js +71 -0
  34. package/templates/workflows/gsd-t-phase.workflow.js +94 -0
  35. package/templates/workflows/gsd-t-quick.workflow.js +72 -0
  36. package/templates/workflows/gsd-t-verify.workflow.js +348 -0
  37. package/templates/workflows/gsd-t-wave.workflow.js +43 -0
  38. package/bin/check-headless-sessions.js +0 -140
  39. package/bin/context-budget-audit.cjs +0 -447
  40. package/bin/context-meter-config.cjs +0 -101
  41. package/bin/context-meter-config.test.cjs +0 -101
  42. package/bin/event-stream.cjs +0 -205
  43. package/bin/gsd-t-benchmark-orchestrator.js +0 -437
  44. package/bin/gsd-t-capture-lint.cjs +0 -440
  45. package/bin/gsd-t-economics.cjs +0 -315
  46. package/bin/gsd-t-in-session-usage.cjs +0 -213
  47. package/bin/gsd-t-orchestrator-config.cjs +0 -161
  48. package/bin/gsd-t-orchestrator-queue.cjs +0 -180
  49. package/bin/gsd-t-orchestrator-recover.cjs +0 -231
  50. package/bin/gsd-t-orchestrator-worker.cjs +0 -219
  51. package/bin/gsd-t-orchestrator.js +0 -535
  52. package/bin/gsd-t-parallel-probe.cjs +0 -132
  53. package/bin/gsd-t-ratelimit-probe-worker.cjs +0 -237
  54. package/bin/gsd-t-ratelimit-probe.cjs +0 -648
  55. package/bin/gsd-t-report-tokens.cjs +0 -549
  56. package/bin/gsd-t-stream-feed-client.cjs +0 -151
  57. package/bin/gsd-t-token-backfill.cjs +0 -366
  58. package/bin/gsd-t-token-capture.cjs +0 -321
  59. package/bin/gsd-t-token-dashboard.cjs +0 -353
  60. package/bin/gsd-t-token-regenerate-log.cjs +0 -129
  61. package/bin/gsd-t-tool-attribution.cjs +0 -377
  62. package/bin/gsd-t-tool-cost.cjs +0 -195
  63. package/bin/gsd-t-transcript-tee.cjs +0 -246
  64. package/bin/gsd-t-unattended-heartbeat.cjs +0 -188
  65. package/bin/gsd-t-unattended-platform.cjs +0 -551
  66. package/bin/gsd-t-unattended-platform.js +0 -381
  67. package/bin/gsd-t-unattended-safety.cjs +0 -773
  68. package/bin/gsd-t-unattended-safety.js +0 -788
  69. package/bin/gsd-t-unattended.cjs +0 -2109
  70. package/bin/gsd-t-unattended.js +0 -1367
  71. package/bin/gsd-t-worker-dispatch.cjs +0 -211
  72. package/bin/handoff-lock.cjs +0 -249
  73. package/bin/handoff-lock.js +0 -249
  74. package/bin/headless-auto-spawn.cjs +0 -711
  75. package/bin/headless-auto-spawn.js +0 -373
  76. package/bin/headless-exit-codes.cjs +0 -67
  77. package/bin/live-activity-report.cjs +0 -615
  78. package/bin/log-tail.cjs +0 -81
  79. package/bin/m44-proof-measure.cjs +0 -285
  80. package/bin/m46-iter-proof.cjs +0 -149
  81. package/bin/m46-worker-proof.cjs +0 -201
  82. package/bin/m55-substrate-proof.cjs +0 -134
  83. package/bin/metrics-rollup.js +0 -200
  84. package/bin/model-windows.cjs +0 -99
  85. package/bin/model-windows.test.cjs +0 -75
  86. package/bin/parallelism-report.cjs +0 -535
  87. package/bin/runway-estimator.cjs +0 -242
  88. package/bin/spawn-plan-derive.cjs +0 -163
  89. package/bin/spawn-plan-status-updater.cjs +0 -292
  90. package/bin/spawn-plan-writer.cjs +0 -204
  91. package/bin/supervisor-pid-fingerprint.cjs +0 -126
  92. package/bin/token-budget.cjs +0 -265
  93. package/bin/unattended-watch-format.cjs +0 -178
  94. package/bin/watch-progress.js +0 -155
  95. package/commands/gsd-t-unattended-stop.md +0 -85
  96. package/commands/gsd-t-unattended-watch.md +0 -465
  97. package/commands/gsd-t-unattended.md +0 -476
  98. package/commands/gsd-t-visualize.md +0 -116
  99. package/scripts/gsd-t-context-meter.e2e.test.js +0 -364
  100. package/scripts/gsd-t-context-meter.js +0 -341
  101. package/scripts/gsd-t-context-meter.test.js +0 -471
  102. package/scripts/gsd-t-dashboard-server.js +0 -1203
  103. package/scripts/gsd-t-post-commit-spawn-plan.sh +0 -86
  104. package/scripts/gsd-t-transcript.html +0 -1821
  105. package/scripts/hooks/gsd-t-conversation-capture.js +0 -439
  106. package/scripts/hooks/gsd-t-in-session-usage-hook.js +0 -84
  107. package/scripts/spawn-plan-fmt-tokens.cjs +0 -80
  108. package/templates/hooks/post-commit-spawn-plan.sh +0 -85
@@ -1,204 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * GSD-T Spawn Plan Writer (M44 D8 T1)
5
- *
6
- * Pure module — writes a spawn-plan JSON file under `.gsd-t/spawns/{spawnId}.json`
7
- * at every spawn chokepoint (captureSpawn, autoSpawnHeadless, unattended-worker
8
- * resume Step 0). The plan file answers exactly one question:
9
- *
10
- * "Of the tasks that were supposed to happen in this spawn, which are done,
11
- * which are in flight, which are pending?"
12
- *
13
- * Hard rules (see .gsd-t/domains/m44-d8-spawn-plan-visibility/constraints.md):
14
- * 1. Writer DERIVES, never decides — no LLM calls, no prompts, no heuristics
15
- * beyond reading partition.md + tasks.md.
16
- * 2. Spawn must launch even if writer fails — callers wrap in try/catch.
17
- * 3. Atomic writes only (temp file + rename).
18
- *
19
- * Contract: .gsd-t/contracts/spawn-plan-contract.md v1.0.0
20
- *
21
- * Zero external deps. `.cjs` so it loads in both ESM-default and CJS projects.
22
- */
23
-
24
- const fs = require('fs');
25
- const path = require('path');
26
-
27
- const SPAWNS_SUBDIR = path.join('.gsd-t', 'spawns');
28
- const SPAWN_PLAN_SCHEMA_VERSION = 1;
29
-
30
- /**
31
- * Write a spawn-plan file at `.gsd-t/spawns/{spawnId}.json` using an atomic
32
- * temp-file + rename. Creates the `.gsd-t/spawns/` directory and its
33
- * `.gitkeep` sentinel if missing.
34
- *
35
- * If `tasks` or `domains` are not explicitly supplied, the writer tries
36
- * to derive them from `.gsd-t/partition.md` + `.gsd-t/domains/*\/tasks.md`
37
- * via the companion `spawn-plan-derive.cjs` module. When derivation fails
38
- * (no partition file, malformed tasks.md, ENOENT), the writer falls back to
39
- * `{tasks: [], note: "no-partition"}` and STILL writes the file — the
40
- * observability panel's job is to render whatever is present; never to block.
41
- *
42
- * @param {object} opts
43
- * @param {string} opts.spawnId filesystem-safe id
44
- * @param {'unattended-worker'|'headless-detached'|'in-session-subagent'|'unattended-worker-sub'} opts.kind
45
- * @param {string} [opts.milestone] e.g. 'M44'
46
- * @param {string} [opts.wave] e.g. 'wave-3'
47
- * @param {string[]} [opts.domains] domain names involved
48
- * @param {Array<{id: string, title: string, status?: string}>} [opts.tasks]
49
- * explicit task list
50
- * (bypasses derivation)
51
- * @param {string} [opts.projectDir='.']
52
- * @param {Date} [opts.now] injection for tests
53
- * @returns {string} absolute path written
54
- */
55
- function writeSpawnPlan(opts) {
56
- if (!opts || typeof opts !== 'object') {
57
- throw new Error('writeSpawnPlan: opts is required');
58
- }
59
- const spawnId = _sanitizeSpawnId(opts.spawnId);
60
- if (!spawnId) {
61
- throw new Error('writeSpawnPlan: spawnId is required and must be filesystem-safe');
62
- }
63
- const kind = opts.kind || 'in-session-subagent';
64
- const projectDir = opts.projectDir || '.';
65
- const now = opts.now instanceof Date ? opts.now : new Date();
66
-
67
- const spawnsDir = path.join(projectDir, SPAWNS_SUBDIR);
68
- _ensureDir(spawnsDir);
69
- _ensureGitkeep(spawnsDir);
70
-
71
- // Derive or accept explicit plan slice. Explicit wins.
72
- let plan = _buildPlanSkeleton({ spawnId, kind, now });
73
- plan.milestone = opts.milestone || null;
74
- plan.wave = opts.wave || null;
75
- plan.domains = Array.isArray(opts.domains) ? opts.domains.slice() : [];
76
- plan.tasks = Array.isArray(opts.tasks)
77
- ? opts.tasks.map(_normalizeTask)
78
- : [];
79
-
80
- // Only auto-derive when caller did NOT explicitly supply tasks. A caller
81
- // can pass `tasks: []` to bypass derivation too.
82
- if (!Array.isArray(opts.tasks)) {
83
- try {
84
- const derive = require('./spawn-plan-derive.cjs');
85
- const derived = derive.derivePlanFromPartition({
86
- projectDir,
87
- milestone: opts.milestone,
88
- currentIter: opts.currentIter,
89
- });
90
- if (derived && typeof derived === 'object') {
91
- if (!plan.milestone && derived.milestone) plan.milestone = derived.milestone;
92
- if (!plan.wave && derived.wave) plan.wave = derived.wave;
93
- if (!plan.domains.length && Array.isArray(derived.domains)) plan.domains = derived.domains.slice();
94
- if (Array.isArray(derived.tasks)) plan.tasks = derived.tasks.map(_normalizeTask);
95
- }
96
- } catch (err) {
97
- // Derivation failures NEVER block the spawn. The plan file still
98
- // gets written with `{tasks: [], note: "no-partition"}` shape.
99
- plan.note = 'no-partition';
100
- try { process.stderr.write(`[spawn-plan-writer] derive failed (continuing): ${err && err.message || err}\n`); } catch (_) { /* silent */ }
101
- }
102
- }
103
-
104
- // Mark first incomplete task as in_progress if every predecessor is done.
105
- // Only one task per spawn may be in_progress at a time (see constraints §Format rules).
106
- _applyInProgressHint(plan.tasks);
107
-
108
- const targetPath = path.join(spawnsDir, spawnId + '.json');
109
- const absTarget = path.resolve(targetPath);
110
- _atomicWriteJson(absTarget, plan);
111
- return absTarget;
112
- }
113
-
114
- /**
115
- * Return the `.gsd-t/spawns/` directory for the given project.
116
- */
117
- function spawnsDirFor(projectDir) {
118
- return path.join(projectDir || '.', SPAWNS_SUBDIR);
119
- }
120
-
121
- // ── Internal helpers ────────────────────────────────────────────────────────
122
-
123
- function _sanitizeSpawnId(id) {
124
- if (id == null) return null;
125
- const s = String(id);
126
- // Filesystem-safe: alphanumerics, dash, underscore, dot. See constraints.md §Format rules.
127
- if (!/^[A-Za-z0-9._-]{1,200}$/.test(s)) return null;
128
- return s;
129
- }
130
-
131
- function _ensureDir(d) {
132
- if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
133
- }
134
-
135
- function _ensureGitkeep(dir) {
136
- const gk = path.join(dir, '.gitkeep');
137
- if (!fs.existsSync(gk)) {
138
- try { fs.writeFileSync(gk, ''); } catch (_) { /* best-effort */ }
139
- }
140
- }
141
-
142
- function _buildPlanSkeleton({ spawnId, kind, now }) {
143
- return {
144
- schemaVersion: SPAWN_PLAN_SCHEMA_VERSION,
145
- spawnId,
146
- kind,
147
- startedAt: now.toISOString(),
148
- endedAt: null,
149
- milestone: null,
150
- wave: null,
151
- domains: [],
152
- tasks: [],
153
- endedReason: null,
154
- };
155
- }
156
-
157
- function _normalizeTask(t) {
158
- if (!t || typeof t !== 'object') return null;
159
- const id = typeof t.id === 'string' ? t.id : null;
160
- const title = typeof t.title === 'string' ? t.title : '';
161
- const statusIn = typeof t.status === 'string' ? t.status : 'pending';
162
- const status = ['pending', 'in_progress', 'done'].includes(statusIn) ? statusIn : 'pending';
163
- const normalized = {
164
- id,
165
- title,
166
- status,
167
- };
168
- if (typeof t.commit === 'string' && t.commit) normalized.commit = t.commit;
169
- if (t.tokens && typeof t.tokens === 'object') {
170
- normalized.tokens = t.tokens;
171
- } else {
172
- normalized.tokens = null;
173
- }
174
- return normalized;
175
- }
176
-
177
- function _applyInProgressHint(tasks) {
178
- if (!Array.isArray(tasks) || tasks.length === 0) return;
179
- // Already an in_progress? Leave as-is.
180
- if (tasks.some((t) => t && t.status === 'in_progress')) return;
181
- for (const t of tasks) {
182
- if (!t) continue;
183
- if (t.status === 'pending') { t.status = 'in_progress'; return; }
184
- // Skip already-done; continue to next.
185
- }
186
- }
187
-
188
- function _atomicWriteJson(targetPath, obj) {
189
- const tmp = targetPath + '.tmp-' + process.pid + '-' + Date.now();
190
- const json = JSON.stringify(obj, null, 2) + '\n';
191
- fs.writeFileSync(tmp, json);
192
- fs.renameSync(tmp, targetPath);
193
- }
194
-
195
- module.exports = {
196
- writeSpawnPlan,
197
- spawnsDirFor,
198
- SPAWN_PLAN_SCHEMA_VERSION,
199
- // Exposed for unit-test reach-in only; not part of the public contract.
200
- _sanitizeSpawnId,
201
- _normalizeTask,
202
- _applyInProgressHint,
203
- _atomicWriteJson,
204
- };
@@ -1,126 +0,0 @@
1
- /**
2
- * Supervisor PID fingerprint.
3
- *
4
- * The supervisor.pid file is written as JSON `{pid, projectDir, startedAt}`
5
- * so that resume-time liveness checks can distinguish "our supervisor is
6
- * running" from "some other process recycled this PID."
7
- *
8
- * Backward-compat: if the file parses as a bare integer (legacy format),
9
- * treat it as `{pid, projectDir: null, startedAt: null, form: "legacy"}`
10
- * and let callers decide whether to trust it.
11
- */
12
- "use strict";
13
-
14
- const fs = require("node:fs");
15
- const path = require("node:path");
16
- const { execSync } = require("node:child_process");
17
-
18
- const PID_REL = path.join(".gsd-t", ".unattended", "supervisor.pid");
19
-
20
- function pidPathFor(projectDir) {
21
- return path.join(path.resolve(projectDir), PID_REL);
22
- }
23
-
24
- function writePidFile(projectDir, pid) {
25
- if (!projectDir || typeof projectDir !== "string") {
26
- throw new Error("writePidFile: projectDir required");
27
- }
28
- if (!Number.isInteger(pid) || pid <= 0) {
29
- throw new Error(`writePidFile: invalid pid ${pid}`);
30
- }
31
- const entry = {
32
- pid,
33
- projectDir: path.resolve(projectDir),
34
- startedAt: new Date().toISOString(),
35
- };
36
- const p = pidPathFor(projectDir);
37
- fs.mkdirSync(path.dirname(p), { recursive: true });
38
- fs.writeFileSync(p, JSON.stringify(entry) + "\n", "utf8");
39
- return entry;
40
- }
41
-
42
- function readPidFile(projectDir) {
43
- const p = pidPathFor(projectDir);
44
- if (!fs.existsSync(p)) return null;
45
- const raw = fs.readFileSync(p, "utf8").trim();
46
- if (!raw) return null;
47
-
48
- // Try JSON form first.
49
- if (raw.startsWith("{")) {
50
- try {
51
- const obj = JSON.parse(raw);
52
- if (obj && Number.isInteger(obj.pid)) {
53
- return {
54
- pid: obj.pid,
55
- projectDir: obj.projectDir || null,
56
- startedAt: obj.startedAt || null,
57
- form: "json",
58
- };
59
- }
60
- } catch {
61
- // fall through to legacy attempt
62
- }
63
- }
64
-
65
- // Legacy bare-integer form.
66
- const n = Number.parseInt(raw, 10);
67
- if (Number.isInteger(n) && n > 0) {
68
- return { pid: n, projectDir: null, startedAt: null, form: "legacy" };
69
- }
70
- return null;
71
- }
72
-
73
- /**
74
- * verifyFingerprint(entry, projectDir, opts?)
75
- *
76
- * Returns { ok, reason, command? }:
77
- * ok: true → entry matches this project AND ps confirms gsd-t command line
78
- * ok: false → mismatch (see reason)
79
- * ok: null → inconclusive (legacy entry, can't verify)
80
- *
81
- * reason values when ok=false:
82
- * "project_mismatch" — entry.projectDir !== resolved projectDir
83
- * "process_not_found" — ps -p returned nothing
84
- * "command_not_gsd_t" — ps succeeded but command line doesn't match /gsd-t|unattended/i
85
- * "ps_failed" — ps threw (couldn't introspect)
86
- *
87
- * opts._execSync — injection point for tests
88
- */
89
- function verifyFingerprint(entry, projectDir, opts = {}) {
90
- if (!entry || typeof entry !== "object") {
91
- return { ok: false, reason: "no_entry" };
92
- }
93
- if (entry.form === "legacy" || !entry.projectDir) {
94
- return { ok: null, reason: "legacy_fingerprint" };
95
- }
96
- const resolved = path.resolve(projectDir);
97
- if (entry.projectDir !== resolved) {
98
- return { ok: false, reason: "project_mismatch" };
99
- }
100
-
101
- const exec = opts._execSync || execSync;
102
- let cmd;
103
- try {
104
- const out = exec(`ps -p ${entry.pid} -o command=`, {
105
- encoding: "utf8",
106
- stdio: ["ignore", "pipe", "ignore"],
107
- });
108
- cmd = (out || "").trim();
109
- } catch {
110
- return { ok: false, reason: "ps_failed" };
111
- }
112
- if (!cmd) {
113
- return { ok: false, reason: "process_not_found" };
114
- }
115
- if (!/gsd-t|unattended/i.test(cmd)) {
116
- return { ok: false, reason: "command_not_gsd_t", command: cmd };
117
- }
118
- return { ok: true, reason: "verified", command: cmd };
119
- }
120
-
121
- module.exports = {
122
- pidPathFor,
123
- writePidFile,
124
- readPidFile,
125
- verifyFingerprint,
126
- };
@@ -1,265 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * GSD-T Token Budget — Session-level token tracking (single-band, v3.12)
5
- *
6
- * Reads .gsd-t/.context-meter-state.json (M34) for context-window readings
7
- * and returns a single-band status signal (normal / threshold) that the
8
- * orchestrator uses to decide whether the next subagent spawn must go
9
- * through autoSpawnHeadless().
10
- *
11
- * v3.12.0 (M38 — meter reduction):
12
- * - Collapsed three-band model (normal/warn/stop) to single-band
13
- * (normal/threshold). The orchestrator makes the routing decision;
14
- * the meter reports a band, not a degradation policy.
15
- * - `getDegradationActions` export removed.
16
- * - Dead-meter detection, `stale` band, and `deadReason` removed.
17
- * Stale state transparently falls through to the heuristic — the
18
- * fail-open hook never raised a user-visible alarm anyway, and the
19
- * orchestrator's headless-by-default posture handles overflow
20
- * structurally rather than by trying to instruct Claude mid-session.
21
- * - Threshold default is `thresholdPct` from context-meter-config.json
22
- * (default 75%). There is no intermediate warn band.
23
- *
24
- * Zero external dependencies (Node.js built-ins only).
25
- */
26
-
27
- const fs = require("fs");
28
- const path = require("path");
29
- const { SAFE_DEFAULT_WINDOW } = require("./model-windows.cjs");
30
-
31
- // ── Constants ────────────────────────────────────────────────────────────────
32
-
33
- const MODEL_RATIOS = { haiku: 1, sonnet: 5, opus: 25 };
34
-
35
- // Fallback context window when no per-model signal is available. Uses the
36
- // model-windows safe LARGE default (1M) rather than a bare 200K literal: a
37
- // too-small fallback re-introduces the premature-headless-handoff bug the
38
- // model-aware sizing was added to fix. See bin/model-windows.cjs.
39
- const FALLBACK_WINDOW = SAFE_DEFAULT_WINDOW;
40
-
41
- // Base token estimates per task type (in haiku-equivalent units)
42
- const BASE_ESTIMATES = {
43
- execute: 8000,
44
- qa: 5000,
45
- "red-team": 10000,
46
- "doc-ripple": 3000,
47
- plan: 4000,
48
- integrate: 6000,
49
- verify: 5000,
50
- default: 6000,
51
- };
52
-
53
- // v3.12 single-band default. Overridable via context-meter-config.json.
54
- const DEFAULT_THRESHOLD_PCT = 75;
55
-
56
- // ── Exports ──────────────────────────────────────────────────────────────────
57
-
58
- module.exports = {
59
- estimateCost,
60
- getSessionStatus,
61
- recordUsage,
62
- estimateMilestoneCost,
63
- getModelCostRatios,
64
- };
65
-
66
- // ── getModelCostRatios ────────────────────────────────────────────────────────
67
-
68
- /** @returns {{ haiku: 1, sonnet: 5, opus: 25 }} */
69
- function getModelCostRatios() {
70
- return { ...MODEL_RATIOS };
71
- }
72
-
73
- // ── estimateCost ──────────────────────────────────────────────────────────────
74
-
75
- /**
76
- * @param {string} model - 'haiku' | 'sonnet' | 'opus'
77
- * @param {string} taskType - 'execute' | 'qa' | 'red-team' | etc.
78
- * @param {{ complexity?: number, historicalAvg?: number, projectDir?: string }} [options]
79
- * @returns {number} estimated tokens
80
- */
81
- function estimateCost(model, taskType, options) {
82
- const o = options || {};
83
- const ratio = MODEL_RATIOS[model] || MODEL_RATIOS.sonnet;
84
- if (o.historicalAvg) return Math.round(o.historicalAvg * ratio);
85
- const hist = getHistoricalAvg(model, taskType, o.projectDir);
86
- if (hist) return Math.round(hist);
87
- const base = BASE_ESTIMATES[taskType] || BASE_ESTIMATES.default;
88
- const complexity = o.complexity || 1.0;
89
- return Math.round(base * ratio * complexity);
90
- }
91
-
92
- // ── getSessionStatus ─────────────────────────────────────────────────────────
93
-
94
- const STATE_FILE_REL = path.join(".gsd-t", ".context-meter-state.json");
95
- const CONFIG_FILE_REL = path.join(".gsd-t", "context-meter-config.json");
96
- const STATE_STALE_MS = 5 * 60 * 1000;
97
-
98
- /**
99
- * @param {string} [projectDir]
100
- * @returns {{ consumed: number, estimated_remaining: number, pct: number, threshold: 'normal'|'threshold' }}
101
- *
102
- * v3.12 (M38): reads `.gsd-t/.context-meter-state.json` produced by the
103
- * Context Meter PostToolUse hook. When the state file is fresh (timestamp
104
- * within 5 minutes), real `input_tokens` drive the response. Otherwise we
105
- * fall back to a historical heuristic from `.gsd-t/token-log.md`. Stale or
106
- * missing state is not a distinct band — the fail-open hook never raised
107
- * a user-visible alarm, and the orchestrator's headless-by-default spawn
108
- * path handles overflow structurally.
109
- */
110
- function getSessionStatus(projectDir) {
111
- const dir = projectDir || process.cwd();
112
- const thresholdPct = resolveThresholdPct(dir);
113
- const real = readContextMeterState(dir);
114
- if (real) {
115
- const consumed = real.inputTokens;
116
- // Primary: the Context Meter writes a model-aware modelWindowSize into
117
- // state (bin/model-windows.cjs). Fallback only when state predates that.
118
- const window = real.modelWindowSize > 0 ? real.modelWindowSize : FALLBACK_WINDOW;
119
- const estimated_remaining = Math.max(0, window - consumed);
120
- const pct = Math.round(real.pct * 10) / 10;
121
- const threshold = bandFor(pct, thresholdPct);
122
- return { consumed, estimated_remaining, pct, threshold };
123
- }
124
- return getSessionStatusHeuristic(dir, thresholdPct);
125
- }
126
-
127
- function readContextMeterState(dir) {
128
- try {
129
- const fp = path.join(dir, STATE_FILE_REL);
130
- const raw = fs.readFileSync(fp, "utf8");
131
- const s = JSON.parse(raw);
132
- if (!s || typeof s.inputTokens !== "number" || typeof s.pct !== "number") return null;
133
- if (!s.timestamp) return null;
134
- const age = Date.now() - Date.parse(s.timestamp);
135
- if (isNaN(age) || age > STATE_STALE_MS || age < 0) return null;
136
- if (s.lastError && typeof s.lastError === "object") return null;
137
- return s;
138
- } catch (_) {
139
- return null;
140
- }
141
- }
142
-
143
- function resolveThresholdPct(dir) {
144
- try {
145
- const fp = path.join(dir, CONFIG_FILE_REL);
146
- const raw = fs.readFileSync(fp, "utf8");
147
- const c = JSON.parse(raw);
148
- const pct = Number(c.thresholdPct);
149
- if (Number.isFinite(pct) && pct > 0 && pct < 100) return pct;
150
- } catch (_) {
151
- /* fall through */
152
- }
153
- return DEFAULT_THRESHOLD_PCT;
154
- }
155
-
156
- function getSessionStatusHeuristic(dir, thresholdPct) {
157
- const window = FALLBACK_WINDOW;
158
- const consumed = readSessionConsumed(dir);
159
- const estimated_remaining = Math.max(0, window - consumed);
160
- const pct = window > 0 ? Math.round((consumed / window) * 100 * 10) / 10 : 0;
161
- const threshold = bandFor(pct, thresholdPct);
162
- return { consumed, estimated_remaining, pct, threshold };
163
- }
164
-
165
- // ── recordUsage ──────────────────────────────────────────────────────────────
166
-
167
- /**
168
- * @param {{ model: string, taskType: string, tokens: number, duration_s: number, projectDir?: string }} usage
169
- */
170
- function recordUsage(usage) {
171
- const dir = usage.projectDir || process.cwd();
172
- const fp = tokenLogPath(dir);
173
- ensureDir(path.dirname(fp));
174
- const now = fmtDate(new Date());
175
- const line = `| ${now} | ${now} | token-budget | record | ${usage.model} | ${usage.duration_s}s | recorded | ${usage.tokens} | null | | ${usage.taskType} | N/A |\n`;
176
- if (!fs.existsSync(fp)) {
177
- const header = "| Datetime-start | Datetime-end | Command | Step | Model | Duration(s) | Notes | Tokens | Compacted | Domain | Task | Ctx% |\n|----------------|--------------|---------|------|-------|-------------|-------|--------|-----------|--------|------|---------|\n";
178
- fs.writeFileSync(fp, header);
179
- }
180
- fs.appendFileSync(fp, line);
181
- }
182
-
183
- // ── estimateMilestoneCost ─────────────────────────────────────────────────────
184
-
185
- /**
186
- * @param {object[]} remainingTasks - [{ model, taskType, complexity }]
187
- * @param {string} [projectDir]
188
- * @returns {{ estimatedTokens: number, estimatedPct: number, feasible: boolean }}
189
- */
190
- function estimateMilestoneCost(remainingTasks, projectDir) {
191
- const status = getSessionStatus(projectDir);
192
- const window = status.consumed + status.estimated_remaining || FALLBACK_WINDOW;
193
- const estimatedTokens = remainingTasks.reduce((sum, t) => {
194
- return sum + estimateCost(t.model, t.taskType, { complexity: t.complexity, projectDir });
195
- }, 0);
196
- const estimatedPct = window > 0 ? Math.min(100, Math.round((estimatedTokens / window) * 100 * 10) / 10) : 0;
197
- const feasible = estimatedTokens <= status.estimated_remaining;
198
- return { estimatedTokens, estimatedPct, feasible };
199
- }
200
-
201
- // ── Internal: single-band resolution ─────────────────────────────────────────
202
-
203
- function bandFor(pct, thresholdPct) {
204
- if (!Number.isFinite(pct)) return "normal";
205
- return pct >= thresholdPct ? "threshold" : "normal";
206
- }
207
-
208
- // ── Internal: token-log parsing ───────────────────────────────────────────────
209
-
210
- function readSessionConsumed(projectDir) {
211
- const fp = tokenLogPath(projectDir || process.cwd());
212
- if (!fs.existsSync(fp)) return 0;
213
- const today = fmtDate(new Date()).slice(0, 10); // YYYY-MM-DD
214
- const lines = fs.readFileSync(fp, "utf8").split("\n");
215
- let total = 0;
216
- for (const line of lines) {
217
- if (!line.startsWith("|")) continue;
218
- const cols = parseRow(line);
219
- if (cols.length < 8) continue;
220
- const dtStart = (cols[0] || "").trim();
221
- if (!dtStart.startsWith(today)) continue;
222
- const tokens = parseInt(cols[7], 10);
223
- if (!isNaN(tokens) && tokens > 0) total += tokens;
224
- }
225
- return total;
226
- }
227
-
228
- function getHistoricalAvg(model, taskType, projectDir) {
229
- const fp = tokenLogPath(projectDir || process.cwd());
230
- if (!fs.existsSync(fp)) return null;
231
- const lines = fs.readFileSync(fp, "utf8").split("\n");
232
- const matches = [];
233
- for (const line of lines) {
234
- if (!line.startsWith("|")) continue;
235
- const cols = parseRow(line);
236
- if (cols.length < 8) continue;
237
- const rowModel = (cols[4] || "").trim();
238
- const rowTask = (cols[10] || (cols[3] || "")).trim();
239
- const tokens = parseInt(cols[7], 10);
240
- if (rowModel === model && rowTask === taskType && !isNaN(tokens) && tokens > 0) {
241
- matches.push(tokens);
242
- }
243
- }
244
- if (matches.length === 0) return null;
245
- return matches.reduce((s, v) => s + v, 0) / matches.length;
246
- }
247
-
248
- function parseRow(line) {
249
- return line.split("|").slice(1, -1);
250
- }
251
-
252
- // ── Internal: helpers ────────────────────────────────────────────────────────
253
-
254
- function tokenLogPath(dir) {
255
- return path.join(dir, ".gsd-t", "token-log.md");
256
- }
257
-
258
- function ensureDir(d) {
259
- if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
260
- }
261
-
262
- function fmtDate(d) {
263
- const pad = (n) => String(n).padStart(2, "0");
264
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
265
- }