@tekyzinc/gsd-t 3.29.11 → 4.0.14

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 (114) hide show
  1. package/CHANGELOG.md +171 -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-complete-milestone.md +6 -12
  7. package/commands/gsd-t-debug.md +47 -533
  8. package/commands/gsd-t-design-decompose.md +24 -497
  9. package/commands/gsd-t-doc-ripple.md +23 -139
  10. package/commands/gsd-t-execute.md +41 -958
  11. package/commands/gsd-t-feature.md +2 -6
  12. package/commands/gsd-t-gap-analysis.md +3 -6
  13. package/commands/gsd-t-help.md +2 -4
  14. package/commands/gsd-t-impact.md +26 -295
  15. package/commands/gsd-t-init-scan-setup.md +7 -7
  16. package/commands/gsd-t-integrate.md +44 -369
  17. package/commands/gsd-t-milestone.md +25 -124
  18. package/commands/gsd-t-partition.md +28 -539
  19. package/commands/gsd-t-plan.md +26 -472
  20. package/commands/gsd-t-prd.md +25 -311
  21. package/commands/gsd-t-quick.md +1 -1
  22. package/commands/gsd-t-resume.md +0 -33
  23. package/commands/gsd-t-scan.md +45 -652
  24. package/commands/gsd-t-verify.md +52 -569
  25. package/commands/gsd-t-wave.md +41 -430
  26. package/package.json +1 -1
  27. package/scripts/gsd-t-calibration-hook.js +3 -1
  28. package/scripts/hooks/pre-commit-journey-coverage +0 -2
  29. package/scripts/hooks/pre-commit-playwright-gate +0 -2
  30. package/templates/CLAUDE-global.md +44 -173
  31. package/templates/CLAUDE-project.md +118 -104
  32. package/templates/prompts/design-verify-subagent.md +3 -0
  33. package/templates/prompts/qa-subagent.md +3 -0
  34. package/templates/prompts/red-team-subagent.md +5 -2
  35. package/templates/workflows/_lib.js +176 -0
  36. package/templates/workflows/gsd-t-debug.workflow.js +86 -0
  37. package/templates/workflows/gsd-t-execute.workflow.js +206 -0
  38. package/templates/workflows/gsd-t-integrate.workflow.js +71 -0
  39. package/templates/workflows/gsd-t-phase.workflow.js +94 -0
  40. package/templates/workflows/gsd-t-quick.workflow.js +72 -0
  41. package/templates/workflows/gsd-t-scan.workflow.js +711 -0
  42. package/templates/workflows/gsd-t-verify.workflow.js +348 -0
  43. package/templates/workflows/gsd-t-wave.workflow.js +43 -0
  44. package/bin/check-headless-sessions.js +0 -140
  45. package/bin/context-budget-audit.cjs +0 -447
  46. package/bin/context-meter-config.cjs +0 -101
  47. package/bin/context-meter-config.test.cjs +0 -101
  48. package/bin/event-stream.cjs +0 -205
  49. package/bin/gsd-t-benchmark-orchestrator.js +0 -437
  50. package/bin/gsd-t-capture-lint.cjs +0 -440
  51. package/bin/gsd-t-economics.cjs +0 -315
  52. package/bin/gsd-t-in-session-usage.cjs +0 -213
  53. package/bin/gsd-t-orchestrator-config.cjs +0 -161
  54. package/bin/gsd-t-orchestrator-queue.cjs +0 -180
  55. package/bin/gsd-t-orchestrator-recover.cjs +0 -231
  56. package/bin/gsd-t-orchestrator-worker.cjs +0 -219
  57. package/bin/gsd-t-orchestrator.js +0 -535
  58. package/bin/gsd-t-parallel-probe.cjs +0 -132
  59. package/bin/gsd-t-ratelimit-probe-worker.cjs +0 -237
  60. package/bin/gsd-t-ratelimit-probe.cjs +0 -648
  61. package/bin/gsd-t-report-tokens.cjs +0 -549
  62. package/bin/gsd-t-stream-feed-client.cjs +0 -151
  63. package/bin/gsd-t-token-backfill.cjs +0 -366
  64. package/bin/gsd-t-token-capture.cjs +0 -321
  65. package/bin/gsd-t-token-dashboard.cjs +0 -353
  66. package/bin/gsd-t-token-regenerate-log.cjs +0 -129
  67. package/bin/gsd-t-tool-attribution.cjs +0 -377
  68. package/bin/gsd-t-tool-cost.cjs +0 -195
  69. package/bin/gsd-t-transcript-tee.cjs +0 -246
  70. package/bin/gsd-t-unattended-heartbeat.cjs +0 -188
  71. package/bin/gsd-t-unattended-platform.cjs +0 -551
  72. package/bin/gsd-t-unattended-platform.js +0 -381
  73. package/bin/gsd-t-unattended-safety.cjs +0 -773
  74. package/bin/gsd-t-unattended-safety.js +0 -788
  75. package/bin/gsd-t-unattended.cjs +0 -2109
  76. package/bin/gsd-t-unattended.js +0 -1367
  77. package/bin/gsd-t-worker-dispatch.cjs +0 -211
  78. package/bin/handoff-lock.cjs +0 -249
  79. package/bin/handoff-lock.js +0 -249
  80. package/bin/headless-auto-spawn.cjs +0 -711
  81. package/bin/headless-auto-spawn.js +0 -373
  82. package/bin/headless-exit-codes.cjs +0 -67
  83. package/bin/live-activity-report.cjs +0 -615
  84. package/bin/log-tail.cjs +0 -81
  85. package/bin/m44-proof-measure.cjs +0 -285
  86. package/bin/m46-iter-proof.cjs +0 -149
  87. package/bin/m46-worker-proof.cjs +0 -201
  88. package/bin/m55-substrate-proof.cjs +0 -134
  89. package/bin/metrics-rollup.js +0 -200
  90. package/bin/model-windows.cjs +0 -99
  91. package/bin/model-windows.test.cjs +0 -75
  92. package/bin/parallelism-report.cjs +0 -535
  93. package/bin/runway-estimator.cjs +0 -242
  94. package/bin/spawn-plan-derive.cjs +0 -163
  95. package/bin/spawn-plan-status-updater.cjs +0 -292
  96. package/bin/spawn-plan-writer.cjs +0 -204
  97. package/bin/supervisor-pid-fingerprint.cjs +0 -126
  98. package/bin/token-budget.cjs +0 -265
  99. package/bin/unattended-watch-format.cjs +0 -178
  100. package/bin/watch-progress.js +0 -155
  101. package/commands/gsd-t-unattended-stop.md +0 -85
  102. package/commands/gsd-t-unattended-watch.md +0 -465
  103. package/commands/gsd-t-unattended.md +0 -476
  104. package/commands/gsd-t-visualize.md +0 -116
  105. package/scripts/gsd-t-context-meter.e2e.test.js +0 -364
  106. package/scripts/gsd-t-context-meter.js +0 -341
  107. package/scripts/gsd-t-context-meter.test.js +0 -471
  108. package/scripts/gsd-t-dashboard-server.js +0 -1203
  109. package/scripts/gsd-t-post-commit-spawn-plan.sh +0 -86
  110. package/scripts/gsd-t-transcript.html +0 -1821
  111. package/scripts/hooks/gsd-t-conversation-capture.js +0 -439
  112. package/scripts/hooks/gsd-t-in-session-usage-hook.js +0 -84
  113. package/scripts/spawn-plan-fmt-tokens.cjs +0 -80
  114. 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
- }