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