@tekyzinc/gsd-t 2.74.12 → 2.76.10
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 +130 -0
- package/README.md +71 -1
- package/bin/advisor-integration.js +93 -0
- package/bin/check-headless-sessions.js +140 -0
- package/bin/context-meter-config.cjs +101 -0
- package/bin/context-meter-config.test.cjs +101 -0
- package/bin/gsd-t.js +710 -16
- package/bin/headless-auto-spawn.js +290 -0
- package/bin/model-selector.js +224 -0
- package/bin/runway-estimator.js +242 -0
- package/bin/token-budget.js +96 -89
- package/bin/token-optimizer.js +471 -0
- package/bin/token-telemetry.js +246 -0
- package/commands/gsd-t-audit.md +3 -3
- package/commands/gsd-t-backlog-list.md +38 -0
- package/commands/gsd-t-brainstorm.md +3 -3
- package/commands/gsd-t-complete-milestone.md +24 -0
- package/commands/gsd-t-debug.md +124 -7
- package/commands/gsd-t-discuss.md +10 -3
- package/commands/gsd-t-doc-ripple.md +32 -4
- package/commands/gsd-t-execute.md +107 -52
- package/commands/gsd-t-help.md +19 -0
- package/commands/gsd-t-integrate.md +67 -4
- package/commands/gsd-t-optimization-apply.md +91 -0
- package/commands/gsd-t-optimization-reject.md +94 -0
- package/commands/gsd-t-partition.md +7 -0
- package/commands/gsd-t-pause.md +3 -0
- package/commands/gsd-t-plan.md +10 -3
- package/commands/gsd-t-prd.md +3 -3
- package/commands/gsd-t-quick.md +71 -9
- package/commands/gsd-t-reflect.md +3 -7
- package/commands/gsd-t-resume.md +36 -0
- package/commands/gsd-t-status.md +31 -0
- package/commands/gsd-t-test-sync.md +7 -0
- package/commands/gsd-t-verify.md +12 -5
- package/commands/gsd-t-visualize.md +3 -7
- package/commands/gsd-t-wave.md +82 -18
- package/docs/GSD-T-README.md +52 -0
- package/docs/architecture.md +95 -0
- package/docs/infrastructure.md +117 -0
- package/docs/methodology.md +36 -0
- package/docs/prd-harness-evolution.md +51 -37
- package/docs/requirements.md +66 -0
- package/package.json +1 -1
- package/scripts/context-meter/count-tokens-client.js +221 -0
- package/scripts/context-meter/count-tokens-client.test.js +308 -0
- package/scripts/context-meter/test-injector.js +55 -0
- package/scripts/context-meter/threshold.js +88 -0
- package/scripts/context-meter/threshold.test.js +255 -0
- package/scripts/context-meter/transcript-parser.js +252 -0
- package/scripts/context-meter/transcript-parser.test.js +320 -0
- package/scripts/gsd-t-context-meter.e2e.test.js +415 -0
- package/scripts/gsd-t-context-meter.js +350 -0
- package/scripts/gsd-t-context-meter.test.js +417 -0
- package/scripts/gsd-t-heartbeat.js +2 -2
- package/scripts/gsd-t-statusline.js +23 -8
- package/templates/CLAUDE-global.md +5 -1
- package/templates/CLAUDE-project.md +26 -6
- package/templates/context-meter-config.json +10 -0
- package/templates/prompts/README.md +1 -1
- package/bin/task-counter.cjs +0 -161
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GSD-T Runway Estimator — Pre-flight context runway projection
|
|
5
|
+
*
|
|
6
|
+
* Reads current context percentage from the M34 context meter state file and
|
|
7
|
+
* historical token-telemetry records (token-metrics.jsonl) to project whether
|
|
8
|
+
* a command about to run will complete before the v3.0.0 stop band (85%).
|
|
9
|
+
*
|
|
10
|
+
* Confidence-weighted: high ≥50 records, medium ≥10, low <10. Low confidence
|
|
11
|
+
* applies a 1.25x conservative skew. On missing history a constant fallback
|
|
12
|
+
* is used (4%/task sonnet-default, 8%/task opus-default). On refusal the
|
|
13
|
+
* estimator never prompts the user — callers hand off to headless-auto-spawn.
|
|
14
|
+
*
|
|
15
|
+
* Zero external dependencies (Node.js built-ins only).
|
|
16
|
+
*
|
|
17
|
+
* Contract: .gsd-t/contracts/runway-estimator-contract.md v1.0.0
|
|
18
|
+
* Consumers: bin/gsd-t.js, commands/gsd-t-execute|wave|integrate|quick|debug.md
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require("fs");
|
|
22
|
+
const path = require("path");
|
|
23
|
+
|
|
24
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
// Mirrors token-budget-contract v3.0.0 — must stay in sync.
|
|
27
|
+
const STOP_THRESHOLD_PCT = 85;
|
|
28
|
+
|
|
29
|
+
// Confidence grading thresholds (frozen in runway-estimator-contract v1.0.0).
|
|
30
|
+
const CONFIDENCE_HIGH_MIN = 50;
|
|
31
|
+
const CONFIDENCE_MEDIUM_MIN = 10;
|
|
32
|
+
|
|
33
|
+
// Conservative skew multiplier applied to low-confidence projections.
|
|
34
|
+
const LOW_CONFIDENCE_SKEW = 1.25;
|
|
35
|
+
|
|
36
|
+
// Conservative constant fallback when no history exists at all.
|
|
37
|
+
const FALLBACK_PCT_PER_TASK_SONNET = 4;
|
|
38
|
+
const FALLBACK_PCT_PER_TASK_OPUS = 8;
|
|
39
|
+
|
|
40
|
+
// Opus-default phases — used when picking a constant fallback for a command
|
|
41
|
+
// with no historical telemetry. Commands not listed default to sonnet.
|
|
42
|
+
const OPUS_DEFAULT_COMMANDS = new Set([
|
|
43
|
+
"gsd-t-debug",
|
|
44
|
+
"gsd-t-integrate",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const STATE_FILE_REL = path.join(".gsd-t", ".context-meter-state.json");
|
|
48
|
+
const METRICS_FILE_REL = path.join(".gsd-t", "token-metrics.jsonl");
|
|
49
|
+
|
|
50
|
+
// ── Exports ──────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
estimateRunway,
|
|
54
|
+
STOP_THRESHOLD_PCT,
|
|
55
|
+
CONFIDENCE_HIGH_MIN,
|
|
56
|
+
CONFIDENCE_MEDIUM_MIN,
|
|
57
|
+
LOW_CONFIDENCE_SKEW,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ── estimateRunway ───────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {{
|
|
64
|
+
* command: string,
|
|
65
|
+
* domain_type?: string,
|
|
66
|
+
* remaining_tasks: number,
|
|
67
|
+
* projectDir?: string,
|
|
68
|
+
* headlessAvailable?: boolean
|
|
69
|
+
* }} opts
|
|
70
|
+
* @returns {{
|
|
71
|
+
* can_start: boolean,
|
|
72
|
+
* current_pct: number,
|
|
73
|
+
* projected_end_pct: number,
|
|
74
|
+
* confidence: 'low'|'medium'|'high',
|
|
75
|
+
* confidence_basis: number,
|
|
76
|
+
* pct_per_task: number,
|
|
77
|
+
* recommendation: 'proceed'|'headless'|'clear-and-resume',
|
|
78
|
+
* reason: string
|
|
79
|
+
* }}
|
|
80
|
+
*/
|
|
81
|
+
function estimateRunway(opts) {
|
|
82
|
+
const command = opts.command;
|
|
83
|
+
const domain_type = opts.domain_type || "";
|
|
84
|
+
const remaining_tasks = Math.max(0, Number(opts.remaining_tasks) || 0);
|
|
85
|
+
const projectDir = opts.projectDir || process.cwd();
|
|
86
|
+
const headlessAvailable = opts.headlessAvailable !== false;
|
|
87
|
+
|
|
88
|
+
const current_pct = readCurrentPct(projectDir);
|
|
89
|
+
const records = readMetrics(projectDir);
|
|
90
|
+
const { pct_per_task, confidence, confidence_basis } = computePctPerTask(
|
|
91
|
+
records,
|
|
92
|
+
command,
|
|
93
|
+
domain_type,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const skew = confidence === "low" ? LOW_CONFIDENCE_SKEW : 1.0;
|
|
97
|
+
const projected_end_pct = round1(
|
|
98
|
+
current_pct + pct_per_task * remaining_tasks * skew,
|
|
99
|
+
);
|
|
100
|
+
const can_start = projected_end_pct < STOP_THRESHOLD_PCT;
|
|
101
|
+
|
|
102
|
+
let recommendation;
|
|
103
|
+
let reason;
|
|
104
|
+
if (can_start) {
|
|
105
|
+
recommendation = "proceed";
|
|
106
|
+
reason = `Projected end ${projected_end_pct}% < ${STOP_THRESHOLD_PCT}% stop threshold`;
|
|
107
|
+
} else if (headlessAvailable) {
|
|
108
|
+
recommendation = "headless";
|
|
109
|
+
reason = `Projected end ${projected_end_pct}% ≥ ${STOP_THRESHOLD_PCT}% — auto-spawn headless`;
|
|
110
|
+
} else {
|
|
111
|
+
recommendation = "clear-and-resume";
|
|
112
|
+
reason = `Projected end ${projected_end_pct}% ≥ ${STOP_THRESHOLD_PCT}% — headless unavailable, clear-and-resume`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
can_start,
|
|
117
|
+
current_pct,
|
|
118
|
+
projected_end_pct,
|
|
119
|
+
confidence,
|
|
120
|
+
confidence_basis,
|
|
121
|
+
pct_per_task: round2(pct_per_task),
|
|
122
|
+
recommendation,
|
|
123
|
+
reason,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Internal: read current pct from M34 state file ──────────────────────────
|
|
128
|
+
|
|
129
|
+
function readCurrentPct(projectDir) {
|
|
130
|
+
try {
|
|
131
|
+
const fp = path.join(projectDir, STATE_FILE_REL);
|
|
132
|
+
const raw = fs.readFileSync(fp, "utf8");
|
|
133
|
+
const s = JSON.parse(raw);
|
|
134
|
+
if (typeof s.pct === "number" && Number.isFinite(s.pct)) {
|
|
135
|
+
return round1(s.pct);
|
|
136
|
+
}
|
|
137
|
+
} catch (_) {
|
|
138
|
+
// Missing or unreadable — warn and fall through.
|
|
139
|
+
try {
|
|
140
|
+
process.stderr.write(
|
|
141
|
+
`runway-estimator: ${STATE_FILE_REL} missing or unreadable — assuming current_pct=0\n`,
|
|
142
|
+
);
|
|
143
|
+
} catch (_) {
|
|
144
|
+
/* ignore */
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Internal: read token-metrics.jsonl ──────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
function readMetrics(projectDir) {
|
|
153
|
+
try {
|
|
154
|
+
const fp = path.join(projectDir, METRICS_FILE_REL);
|
|
155
|
+
const raw = fs.readFileSync(fp, "utf8");
|
|
156
|
+
const out = [];
|
|
157
|
+
for (const line of raw.split("\n")) {
|
|
158
|
+
if (!line.trim()) continue;
|
|
159
|
+
try {
|
|
160
|
+
out.push(JSON.parse(line));
|
|
161
|
+
} catch (_) {
|
|
162
|
+
/* skip malformed */
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
} catch (_) {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Internal: compute pct-per-task with confidence grading ──────────────────
|
|
172
|
+
|
|
173
|
+
function computePctPerTask(records, command, domain_type) {
|
|
174
|
+
// Tier 1: {command, domain_type} pair — sharpest match.
|
|
175
|
+
if (domain_type) {
|
|
176
|
+
const pair = records.filter(
|
|
177
|
+
(r) => r.command === command && r.domain_type === domain_type,
|
|
178
|
+
);
|
|
179
|
+
if (pair.length >= CONFIDENCE_MEDIUM_MIN) {
|
|
180
|
+
return {
|
|
181
|
+
pct_per_task: meanPctDelta(pair),
|
|
182
|
+
confidence: gradeConfidence(pair.length),
|
|
183
|
+
confidence_basis: pair.length,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Tier 2: {command} aggregate.
|
|
189
|
+
const cmd = records.filter((r) => r.command === command);
|
|
190
|
+
if (cmd.length >= CONFIDENCE_MEDIUM_MIN) {
|
|
191
|
+
return {
|
|
192
|
+
pct_per_task: meanPctDelta(cmd),
|
|
193
|
+
confidence: gradeConfidence(cmd.length),
|
|
194
|
+
confidence_basis: cmd.length,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Tier 3: constant fallback — confidence=low, basis=cmd.length (0 or few).
|
|
199
|
+
return {
|
|
200
|
+
pct_per_task: fallbackPctPerTask(command),
|
|
201
|
+
confidence: "low",
|
|
202
|
+
confidence_basis: cmd.length,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function meanPctDelta(records) {
|
|
207
|
+
if (!records.length) return 0;
|
|
208
|
+
let sum = 0;
|
|
209
|
+
let n = 0;
|
|
210
|
+
for (const r of records) {
|
|
211
|
+
const before = Number(r.context_window_pct_before);
|
|
212
|
+
const after = Number(r.context_window_pct_after);
|
|
213
|
+
if (!Number.isFinite(before) || !Number.isFinite(after)) continue;
|
|
214
|
+
const delta = after - before;
|
|
215
|
+
if (delta < 0) continue; // pathological — treat as 0
|
|
216
|
+
sum += delta;
|
|
217
|
+
n += 1;
|
|
218
|
+
}
|
|
219
|
+
if (n === 0) return 0;
|
|
220
|
+
return sum / n;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function gradeConfidence(n) {
|
|
224
|
+
if (n >= CONFIDENCE_HIGH_MIN) return "high";
|
|
225
|
+
if (n >= CONFIDENCE_MEDIUM_MIN) return "medium";
|
|
226
|
+
return "low";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function fallbackPctPerTask(command) {
|
|
230
|
+
if (OPUS_DEFAULT_COMMANDS.has(command)) return FALLBACK_PCT_PER_TASK_OPUS;
|
|
231
|
+
return FALLBACK_PCT_PER_TASK_SONNET;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── Internal: rounding helpers ──────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
function round1(n) {
|
|
237
|
+
return Math.round(n * 10) / 10;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function round2(n) {
|
|
241
|
+
return Math.round(n * 100) / 100;
|
|
242
|
+
}
|
package/bin/token-budget.js
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* GSD-T Token Budget — Session-level token tracking
|
|
4
|
+
* GSD-T Token Budget — Session-level token tracking (three-band model)
|
|
5
5
|
*
|
|
6
|
-
* Reads .gsd-t
|
|
7
|
-
* and returns
|
|
6
|
+
* Reads .gsd-t/.context-meter-state.json (M34) for real context-window
|
|
7
|
+
* readings, tracks session usage, and returns a three-band status signal
|
|
8
|
+
* (normal / warn / stop) that callers use to decide whether to proceed,
|
|
9
|
+
* log a warning, or halt cleanly.
|
|
10
|
+
*
|
|
11
|
+
* v3.0.0 (M35 — clean break from v2.0.0):
|
|
12
|
+
* - The `downgrade` and `conserve` bands were REMOVED. Silent model
|
|
13
|
+
* degradation and silent phase-skipping are anti-features — they
|
|
14
|
+
* violate GSD-T's "quality is non-negotiable" principle.
|
|
15
|
+
* - `getDegradationActions()` now returns `{band, pct, message}` instead
|
|
16
|
+
* of `{threshold, actions, modelOverrides}`. No `modelOverride`, no
|
|
17
|
+
* `skipPhases`, no `checkpoint` side-channel.
|
|
18
|
+
* - `warn` threshold tightened from 60% → 70%. `stop` tightened from
|
|
19
|
+
* 95% → 85% — keeps us clear of the runtime's native ~95% compact.
|
|
8
20
|
*
|
|
9
21
|
* Zero external dependencies (Node.js built-ins only).
|
|
10
22
|
*/
|
|
@@ -28,11 +40,16 @@ const BASE_ESTIMATES = {
|
|
|
28
40
|
default: 6000,
|
|
29
41
|
};
|
|
30
42
|
|
|
43
|
+
// v3.0.0 three-band thresholds. Lower-bound inclusive.
|
|
44
|
+
// pct < 70 → normal
|
|
45
|
+
// 70 ≤ pct < 85 → warn (informational — log, proceed)
|
|
46
|
+
// pct ≥ 85 → stop (halt cleanly, hand off to runway estimator)
|
|
47
|
+
const WARN_THRESHOLD_PCT = 70;
|
|
48
|
+
const STOP_THRESHOLD_PCT = 85;
|
|
49
|
+
|
|
31
50
|
const THRESHOLDS = {
|
|
32
|
-
warn:
|
|
33
|
-
|
|
34
|
-
conserve: 85,
|
|
35
|
-
stop: 95,
|
|
51
|
+
warn: WARN_THRESHOLD_PCT,
|
|
52
|
+
stop: STOP_THRESHOLD_PCT,
|
|
36
53
|
};
|
|
37
54
|
|
|
38
55
|
// ── Exports ──────────────────────────────────────────────────────────────────
|
|
@@ -74,50 +91,57 @@ function estimateCost(model, taskType, options) {
|
|
|
74
91
|
|
|
75
92
|
// ── getSessionStatus ─────────────────────────────────────────────────────────
|
|
76
93
|
|
|
94
|
+
const STATE_FILE_REL = path.join(".gsd-t", ".context-meter-state.json");
|
|
95
|
+
const STATE_STALE_MS = 5 * 60 * 1000;
|
|
96
|
+
|
|
77
97
|
/**
|
|
78
98
|
* @param {string} [projectDir]
|
|
79
99
|
* @returns {{ consumed: number, estimated_remaining: number, pct: number, threshold: string }}
|
|
80
100
|
*
|
|
81
|
-
* v2.
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* 0..100%. This keeps the API stable so commands that ask for thresholds
|
|
87
|
-
* (downgrade/conserve/stop) get a real signal.
|
|
101
|
+
* v2.0.0 (M34): reads `.gsd-t/.context-meter-state.json` produced by the
|
|
102
|
+
* Context Meter PostToolUse hook. When that file is fresh (timestamp within
|
|
103
|
+
* the last 5 minutes), real `input_tokens` drive the response. Otherwise we
|
|
104
|
+
* fall back to a historical heuristic from `.gsd-t/token-log.md`, preserving
|
|
105
|
+
* graceful degradation for projects without the hook installed.
|
|
88
106
|
*/
|
|
89
107
|
function getSessionStatus(projectDir) {
|
|
90
108
|
const dir = projectDir || process.cwd();
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
const real = readContextMeterState(dir);
|
|
110
|
+
if (real) {
|
|
111
|
+
const consumed = real.inputTokens;
|
|
112
|
+
const window = real.modelWindowSize > 0 ? real.modelWindowSize : 200000;
|
|
113
|
+
const estimated_remaining = Math.max(0, window - consumed);
|
|
114
|
+
const pct = Math.round(real.pct * 10) / 10;
|
|
115
|
+
const threshold = resolveThreshold(pct);
|
|
116
|
+
return { consumed, estimated_remaining, pct, threshold };
|
|
117
|
+
}
|
|
118
|
+
return getSessionStatusHeuristic(dir);
|
|
98
119
|
}
|
|
99
120
|
|
|
100
|
-
function
|
|
121
|
+
function readContextMeterState(dir) {
|
|
101
122
|
try {
|
|
102
|
-
const fp = path.join(dir,
|
|
123
|
+
const fp = path.join(dir, STATE_FILE_REL);
|
|
103
124
|
const raw = fs.readFileSync(fp, "utf8");
|
|
104
125
|
const s = JSON.parse(raw);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
} catch (_) {}
|
|
111
|
-
if (process.env.GSD_T_TASK_LIMIT) {
|
|
112
|
-
const n = parseInt(process.env.GSD_T_TASK_LIMIT, 10);
|
|
113
|
-
if (!isNaN(n) && n > 0) limit = n;
|
|
114
|
-
}
|
|
115
|
-
return { count: typeof s.count === "number" ? s.count : 0, limit };
|
|
126
|
+
if (!s || typeof s.inputTokens !== "number" || typeof s.pct !== "number") return null;
|
|
127
|
+
if (!s.timestamp) return null;
|
|
128
|
+
const age = Date.now() - Date.parse(s.timestamp);
|
|
129
|
+
if (isNaN(age) || age > STATE_STALE_MS || age < 0) return null;
|
|
130
|
+
return s;
|
|
116
131
|
} catch (_) {
|
|
117
|
-
return
|
|
132
|
+
return null;
|
|
118
133
|
}
|
|
119
134
|
}
|
|
120
135
|
|
|
136
|
+
function getSessionStatusHeuristic(dir) {
|
|
137
|
+
const window = 200000;
|
|
138
|
+
const consumed = readSessionConsumed(dir);
|
|
139
|
+
const estimated_remaining = Math.max(0, window - consumed);
|
|
140
|
+
const pct = window > 0 ? Math.round((consumed / window) * 100 * 10) / 10 : 0;
|
|
141
|
+
const threshold = resolveThreshold(pct);
|
|
142
|
+
return { consumed, estimated_remaining, pct, threshold };
|
|
143
|
+
}
|
|
144
|
+
|
|
121
145
|
// ── recordUsage ──────────────────────────────────────────────────────────────
|
|
122
146
|
|
|
123
147
|
/**
|
|
@@ -136,15 +160,20 @@ function recordUsage(usage) {
|
|
|
136
160
|
fs.appendFileSync(fp, line);
|
|
137
161
|
}
|
|
138
162
|
|
|
139
|
-
// ── getDegradationActions
|
|
163
|
+
// ── getDegradationActions (v3.0.0 — three-band) ─────────────────────────────
|
|
140
164
|
|
|
141
165
|
/**
|
|
166
|
+
* v3.0.0 three-band response. The name is preserved for caller-identification
|
|
167
|
+
* convenience; the return shape is a CLEAN BREAK from v2.0.0 — no
|
|
168
|
+
* `modelOverrides`, no `actions` list, no `skipPhases`, no `checkpoint`
|
|
169
|
+
* side-channel. Callers that relied on those fields MUST be updated.
|
|
170
|
+
*
|
|
142
171
|
* @param {string} [projectDir]
|
|
143
|
-
* @returns {{
|
|
172
|
+
* @returns {{ band: 'normal'|'warn'|'stop', pct: number, message: string }}
|
|
144
173
|
*/
|
|
145
174
|
function getDegradationActions(projectDir) {
|
|
146
|
-
const { threshold } = getSessionStatus(projectDir);
|
|
147
|
-
return
|
|
175
|
+
const { threshold, pct } = getSessionStatus(projectDir);
|
|
176
|
+
return buildBandResponse(threshold, pct);
|
|
148
177
|
}
|
|
149
178
|
|
|
150
179
|
// ── estimateMilestoneCost ─────────────────────────────────────────────────────
|
|
@@ -156,69 +185,47 @@ function getDegradationActions(projectDir) {
|
|
|
156
185
|
*/
|
|
157
186
|
function estimateMilestoneCost(remainingTasks, projectDir) {
|
|
158
187
|
const status = getSessionStatus(projectDir);
|
|
159
|
-
const
|
|
188
|
+
const window = status.consumed + status.estimated_remaining || 200000;
|
|
160
189
|
const estimatedTokens = remainingTasks.reduce((sum, t) => {
|
|
161
190
|
return sum + estimateCost(t.model, t.taskType, { complexity: t.complexity, projectDir });
|
|
162
191
|
}, 0);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const taskEquivalents = remainingTasks.length;
|
|
166
|
-
const estimatedPct = limit > 0 ? Math.min(100, Math.round((taskEquivalents / limit) * 100 * 10) / 10) : 0;
|
|
167
|
-
const feasible = taskEquivalents <= status.estimated_remaining;
|
|
192
|
+
const estimatedPct = window > 0 ? Math.min(100, Math.round((estimatedTokens / window) * 100 * 10) / 10) : 0;
|
|
193
|
+
const feasible = estimatedTokens <= status.estimated_remaining;
|
|
168
194
|
return { estimatedTokens, estimatedPct, feasible };
|
|
169
195
|
}
|
|
170
196
|
|
|
171
|
-
// ── Internal: threshold resolution
|
|
197
|
+
// ── Internal: threshold resolution (v3.0.0 — three-band) ─────────────────────
|
|
172
198
|
|
|
173
199
|
function resolveThreshold(pct) {
|
|
200
|
+
if (!Number.isFinite(pct)) return "normal";
|
|
174
201
|
if (pct >= THRESHOLDS.stop) return "stop";
|
|
175
|
-
if (pct >= THRESHOLDS.conserve) return "conserve";
|
|
176
|
-
if (pct >= THRESHOLDS.downgrade) return "downgrade";
|
|
177
202
|
if (pct >= THRESHOLDS.warn) return "warn";
|
|
178
203
|
return "normal";
|
|
179
204
|
}
|
|
180
205
|
|
|
181
|
-
function
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
conserve: {
|
|
205
|
-
threshold: "conserve",
|
|
206
|
-
actions: ["Pause doc-ripple", "Pause design brief generation", "Checkpoint all progress"],
|
|
207
|
-
modelOverrides: {
|
|
208
|
-
"sonnet:qa": "sonnet",
|
|
209
|
-
"sonnet:execute": "haiku",
|
|
210
|
-
"sonnet:doc-ripple": "skip",
|
|
211
|
-
"opus:red-team": "sonnet",
|
|
212
|
-
"haiku:*": "haiku",
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
stop: {
|
|
216
|
-
threshold: "stop",
|
|
217
|
-
actions: ["Hard stop", "Save all progress", "Display resume instruction"],
|
|
218
|
-
modelOverrides: {},
|
|
219
|
-
},
|
|
220
|
-
};
|
|
221
|
-
return responses[threshold] || responses.normal;
|
|
206
|
+
function buildBandResponse(band, pct) {
|
|
207
|
+
const safePct = Number.isFinite(pct) ? pct : 0;
|
|
208
|
+
switch (band) {
|
|
209
|
+
case "warn":
|
|
210
|
+
return {
|
|
211
|
+
band: "warn",
|
|
212
|
+
pct: safePct,
|
|
213
|
+
message: `Context ${safePct.toFixed(1)}% — warn band (≥${WARN_THRESHOLD_PCT}%). Informational only; proceed.`,
|
|
214
|
+
};
|
|
215
|
+
case "stop":
|
|
216
|
+
return {
|
|
217
|
+
band: "stop",
|
|
218
|
+
pct: safePct,
|
|
219
|
+
message: `Context ${safePct.toFixed(1)}% — stop band (≥${STOP_THRESHOLD_PCT}%). Halt cleanly; hand off to runway estimator / headless auto-spawn.`,
|
|
220
|
+
};
|
|
221
|
+
case "normal":
|
|
222
|
+
default:
|
|
223
|
+
return {
|
|
224
|
+
band: "normal",
|
|
225
|
+
pct: safePct,
|
|
226
|
+
message: `Context ${safePct.toFixed(1)}% — normal band. Proceed.`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
222
229
|
}
|
|
223
230
|
|
|
224
231
|
// ── Internal: token-log parsing ───────────────────────────────────────────────
|