@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.
- package/CHANGELOG.md +171 -0
- package/bin/gsd-t-parallel.cjs +64 -13
- package/bin/gsd-t.js +48 -376
- package/bin/journey-coverage.cjs +6 -3
- package/bin/parallel-cli.cjs +13 -1
- package/commands/gsd-t-complete-milestone.md +6 -12
- package/commands/gsd-t-debug.md +47 -533
- package/commands/gsd-t-design-decompose.md +24 -497
- package/commands/gsd-t-doc-ripple.md +23 -139
- package/commands/gsd-t-execute.md +41 -958
- package/commands/gsd-t-feature.md +2 -6
- package/commands/gsd-t-gap-analysis.md +3 -6
- package/commands/gsd-t-help.md +2 -4
- package/commands/gsd-t-impact.md +26 -295
- package/commands/gsd-t-init-scan-setup.md +7 -7
- package/commands/gsd-t-integrate.md +44 -369
- package/commands/gsd-t-milestone.md +25 -124
- package/commands/gsd-t-partition.md +28 -539
- package/commands/gsd-t-plan.md +26 -472
- package/commands/gsd-t-prd.md +25 -311
- package/commands/gsd-t-quick.md +1 -1
- package/commands/gsd-t-resume.md +0 -33
- package/commands/gsd-t-scan.md +45 -652
- package/commands/gsd-t-verify.md +52 -569
- package/commands/gsd-t-wave.md +41 -430
- package/package.json +1 -1
- package/scripts/gsd-t-calibration-hook.js +3 -1
- package/scripts/hooks/pre-commit-journey-coverage +0 -2
- package/scripts/hooks/pre-commit-playwright-gate +0 -2
- package/templates/CLAUDE-global.md +44 -173
- package/templates/CLAUDE-project.md +118 -104
- package/templates/prompts/design-verify-subagent.md +3 -0
- package/templates/prompts/qa-subagent.md +3 -0
- package/templates/prompts/red-team-subagent.md +5 -2
- package/templates/workflows/_lib.js +176 -0
- package/templates/workflows/gsd-t-debug.workflow.js +86 -0
- package/templates/workflows/gsd-t-execute.workflow.js +206 -0
- package/templates/workflows/gsd-t-integrate.workflow.js +71 -0
- package/templates/workflows/gsd-t-phase.workflow.js +94 -0
- package/templates/workflows/gsd-t-quick.workflow.js +72 -0
- package/templates/workflows/gsd-t-scan.workflow.js +711 -0
- package/templates/workflows/gsd-t-verify.workflow.js +348 -0
- package/templates/workflows/gsd-t-wave.workflow.js +43 -0
- package/bin/check-headless-sessions.js +0 -140
- package/bin/context-budget-audit.cjs +0 -447
- package/bin/context-meter-config.cjs +0 -101
- package/bin/context-meter-config.test.cjs +0 -101
- package/bin/event-stream.cjs +0 -205
- package/bin/gsd-t-benchmark-orchestrator.js +0 -437
- package/bin/gsd-t-capture-lint.cjs +0 -440
- package/bin/gsd-t-economics.cjs +0 -315
- package/bin/gsd-t-in-session-usage.cjs +0 -213
- package/bin/gsd-t-orchestrator-config.cjs +0 -161
- package/bin/gsd-t-orchestrator-queue.cjs +0 -180
- package/bin/gsd-t-orchestrator-recover.cjs +0 -231
- package/bin/gsd-t-orchestrator-worker.cjs +0 -219
- package/bin/gsd-t-orchestrator.js +0 -535
- package/bin/gsd-t-parallel-probe.cjs +0 -132
- package/bin/gsd-t-ratelimit-probe-worker.cjs +0 -237
- package/bin/gsd-t-ratelimit-probe.cjs +0 -648
- package/bin/gsd-t-report-tokens.cjs +0 -549
- package/bin/gsd-t-stream-feed-client.cjs +0 -151
- package/bin/gsd-t-token-backfill.cjs +0 -366
- package/bin/gsd-t-token-capture.cjs +0 -321
- package/bin/gsd-t-token-dashboard.cjs +0 -353
- package/bin/gsd-t-token-regenerate-log.cjs +0 -129
- package/bin/gsd-t-tool-attribution.cjs +0 -377
- package/bin/gsd-t-tool-cost.cjs +0 -195
- package/bin/gsd-t-transcript-tee.cjs +0 -246
- package/bin/gsd-t-unattended-heartbeat.cjs +0 -188
- package/bin/gsd-t-unattended-platform.cjs +0 -551
- package/bin/gsd-t-unattended-platform.js +0 -381
- package/bin/gsd-t-unattended-safety.cjs +0 -773
- package/bin/gsd-t-unattended-safety.js +0 -788
- package/bin/gsd-t-unattended.cjs +0 -2109
- package/bin/gsd-t-unattended.js +0 -1367
- package/bin/gsd-t-worker-dispatch.cjs +0 -211
- package/bin/handoff-lock.cjs +0 -249
- package/bin/handoff-lock.js +0 -249
- package/bin/headless-auto-spawn.cjs +0 -711
- package/bin/headless-auto-spawn.js +0 -373
- package/bin/headless-exit-codes.cjs +0 -67
- package/bin/live-activity-report.cjs +0 -615
- package/bin/log-tail.cjs +0 -81
- package/bin/m44-proof-measure.cjs +0 -285
- package/bin/m46-iter-proof.cjs +0 -149
- package/bin/m46-worker-proof.cjs +0 -201
- package/bin/m55-substrate-proof.cjs +0 -134
- package/bin/metrics-rollup.js +0 -200
- package/bin/model-windows.cjs +0 -99
- package/bin/model-windows.test.cjs +0 -75
- package/bin/parallelism-report.cjs +0 -535
- package/bin/runway-estimator.cjs +0 -242
- package/bin/spawn-plan-derive.cjs +0 -163
- package/bin/spawn-plan-status-updater.cjs +0 -292
- package/bin/spawn-plan-writer.cjs +0 -204
- package/bin/supervisor-pid-fingerprint.cjs +0 -126
- package/bin/token-budget.cjs +0 -265
- package/bin/unattended-watch-format.cjs +0 -178
- package/bin/watch-progress.js +0 -155
- package/commands/gsd-t-unattended-stop.md +0 -85
- package/commands/gsd-t-unattended-watch.md +0 -465
- package/commands/gsd-t-unattended.md +0 -476
- package/commands/gsd-t-visualize.md +0 -116
- package/scripts/gsd-t-context-meter.e2e.test.js +0 -364
- package/scripts/gsd-t-context-meter.js +0 -341
- package/scripts/gsd-t-context-meter.test.js +0 -471
- package/scripts/gsd-t-dashboard-server.js +0 -1203
- package/scripts/gsd-t-post-commit-spawn-plan.sh +0 -86
- package/scripts/gsd-t-transcript.html +0 -1821
- package/scripts/hooks/gsd-t-conversation-capture.js +0 -439
- package/scripts/hooks/gsd-t-in-session-usage-hook.js +0 -84
- package/scripts/spawn-plan-fmt-tokens.cjs +0 -80
- 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
|
-
};
|
package/bin/token-budget.cjs
DELETED
|
@@ -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
|
-
}
|