@tekyzinc/gsd-t 5.8.10 → 5.9.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 +29 -0
- package/README.md +1 -1
- package/bin/gsd-t-concise-rewrite.cjs +223 -0
- package/bin/gsd-t-fallback-detect.cjs +624 -0
- package/bin/gsd-t-install-check.cjs +342 -0
- package/bin/gsd-t-verify-gate.cjs +2 -0
- package/bin/gsd-t.js +193 -5
- package/package.json +1 -1
- package/scripts/gsd-t-concise-hook.js +200 -0
- package/scripts/gsd-t-fallback-guard.js +237 -0
- package/scripts/gsd-t-install-heal.js +183 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [5.9.10] - 2026-08-07
|
|
6
|
+
|
|
7
|
+
### Added — projects repair their own install, and fallbacks need approval by name
|
|
8
|
+
|
|
9
|
+
**Every registered project was running a broken install.** Binvoice had 20 of its 38 tools. The verify gate was a stale copy in all 33 projects, so every one of them has been running an outdated quality gate.
|
|
10
|
+
|
|
11
|
+
The cause was one line: a tool missing from the package was skipped in silence, and the pass then reported "copied N tools" — where N looked fine. The report was the concealment. Two more copies of the same defect sat in the utility-script and template installers.
|
|
12
|
+
|
|
13
|
+
**Install self-repair (M108).** Each session now checks its project's tools before any work starts, restores what is missing from the installed package, and refuses to continue if it cannot. Not a workaround: it fixes the failure, then the work proceeds with everything present. Every repair is logged to a shared file, and `gsd-t install-check --report` names any tool that has gone missing in more than one project — which points at the installer rather than the project. A per-project fix that also tells GSD-T what to mend in itself.
|
|
14
|
+
|
|
15
|
+
- `bin/gsd-t-install-check.cjs`: reads the expected tool list from the installer (never a second copy that can go stale), compares, repairs, halts when it can't
|
|
16
|
+
- `scripts/gsd-t-install-heal.js`: runs it at session start, first in the chain; silent when the install is already complete
|
|
17
|
+
- `bin/gsd-t.js`: three silent skips now report every undelivered file; new `install-check` command
|
|
18
|
+
- `test/m108-install-heal.test.js`: 12 tests, including one asserting the silent skip never comes back
|
|
19
|
+
|
|
20
|
+
**Fallback gate (M106).** A fallback — anything that continues after a failure — can no longer be written unless it was approved by name. The rule has been written down since v5.0.14 and kept being broken: a lookup for a post's author failed, the code substituted the last known seller, and posts were recorded as written by someone who never wrote them. That shipped while the rule was live.
|
|
21
|
+
|
|
22
|
+
Two things about the rule changed as a result. The damage is fabricated data **plus a disabled alarm** — author detection broke because a code change broke it, and the substitution meant nobody ever went looking. And consent must be specific: a fallback buried inside a long plan approved as a whole is not approved, because whoever wrote the plan controls the burying.
|
|
23
|
+
|
|
24
|
+
Detection matches the shape of the code, not its wording, so rewording cannot evade it. It runs when code is written, when a plan is reviewed, before a commit, and on demand across existing code. A one-time baseline records what was already there so the gate judges new work only.
|
|
25
|
+
|
|
26
|
+
- `bin/gsd-t-fallback-detect.cjs`, `scripts/gsd-t-fallback-guard.js`, `test/m106-fallback-gate.test.js`
|
|
27
|
+
|
|
28
|
+
**Shorter replies (M107).** A second Claude rewrites replies over 60 words — answer first, no preamble, no backstory, no jargon — before you read them. The rewrite is thrown away and the original kept if it drops a question, a code block, every file path, or invents a number.
|
|
29
|
+
|
|
30
|
+
- `bin/gsd-t-concise-rewrite.cjs`, `scripts/gsd-t-concise-hook.js`, `test/m107-concise-rewrite.test.js`
|
|
31
|
+
|
|
32
|
+
Switch either gate off per project: `.gsd-t/fallback-gate.json` or `.gsd-t/concise.json` with `{"enabled": false}`.
|
|
33
|
+
|
|
5
34
|
## [5.8.10] - 2026-08-05
|
|
6
35
|
|
|
7
36
|
### Changed — the architect now interviews you before it assesses
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GSD-T: Contract-Driven Development for Claude Code
|
|
2
2
|
|
|
3
|
-
**v5.
|
|
3
|
+
**v5.9.10** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
|
|
4
4
|
|
|
5
5
|
**Eliminates context rot** — task-level fresh dispatch (one subagent per task, ~10-20% context each) means compaction never triggers.
|
|
6
6
|
**Compaction-proof debug loops** — `gsd-t headless --debug-loop` runs test-fix-retest cycles as separate `claude -p` sessions. A JSONL debug ledger persists all hypothesis/fix/learning history across fresh sessions. Anti-repetition preamble injection prevents retrying failed hypotheses. Escalation tiers (sonnet → opus → human) and a hard iteration ceiling enforced externally.
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* gsd-t-concise-rewrite.cjs
|
|
4
|
+
*
|
|
5
|
+
* M107-D1 — Rewrites a reply into its shortest honest form.
|
|
6
|
+
*
|
|
7
|
+
* [RULE] concise-rewrite-never-changes-a-fact
|
|
8
|
+
* [RULE] concise-rewrite-halts-never-silently-degrades
|
|
9
|
+
* [RULE] concise-rewrite-preserves-questions-and-warnings
|
|
10
|
+
*
|
|
11
|
+
* A fresh Claude, given only the text and the rules, cuts what the writer is
|
|
12
|
+
* attached to. It has no memory of the work, so it has nothing to defend.
|
|
13
|
+
*
|
|
14
|
+
* ─── Usage ──────────────────────────────────────────────────────────────────
|
|
15
|
+
* node gsd-t-concise-rewrite.cjs --text "<reply>" [--project <dir>] [--json]
|
|
16
|
+
* echo "<reply>" | node gsd-t-concise-rewrite.cjs --stdin
|
|
17
|
+
*
|
|
18
|
+
* ─── Exit codes ─────────────────────────────────────────────────────────────
|
|
19
|
+
* 0 rewritten (or skipped because it was already short)
|
|
20
|
+
* 4 the rewrite could not be done — the ORIGINAL is returned, marked
|
|
21
|
+
* 64 bad input
|
|
22
|
+
*
|
|
23
|
+
* On failure it returns the ORIGINAL text and says so. It never returns
|
|
24
|
+
* nothing, never truncates, never guesses. Showing a long reply is a poor
|
|
25
|
+
* outcome; showing a silently mangled one is a wrong one.
|
|
26
|
+
*
|
|
27
|
+
* Zero dependencies beyond the `claude` CLI on PATH.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
"use strict";
|
|
31
|
+
|
|
32
|
+
const fs = require("fs");
|
|
33
|
+
const path = require("path");
|
|
34
|
+
const { spawnSync } = require("child_process");
|
|
35
|
+
|
|
36
|
+
const EXIT_OK = 0;
|
|
37
|
+
const EXIT_FAILED = 4;
|
|
38
|
+
const EXIT_BAD_INPUT = 64;
|
|
39
|
+
|
|
40
|
+
const DEFAULTS = {
|
|
41
|
+
enabled: true,
|
|
42
|
+
skipUnder: 60, // words — below this, rewriting buys nothing
|
|
43
|
+
model: "sonnet",
|
|
44
|
+
timeoutMs: 45000,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const RULES = `You rewrite a reply so it can be read in as little time as possible.
|
|
48
|
+
|
|
49
|
+
The reader is a slow reader. Every extra line costs him real time. Jargon costs
|
|
50
|
+
him more than length — he has to stop and translate it, and then ask for it
|
|
51
|
+
again in plain words.
|
|
52
|
+
|
|
53
|
+
Rewrite the reply below following these rules exactly:
|
|
54
|
+
|
|
55
|
+
1. ANSWER FIRST. The answer is the first thing. Nothing before it.
|
|
56
|
+
2. NO PREAMBLE. Cut any sentence that announces a point instead of making it.
|
|
57
|
+
3. NO BACKSTORY. Cut every explanation of why something failed before, what
|
|
58
|
+
cannot work, how it works today, or what was rejected — unless the reply is
|
|
59
|
+
answering a direct question about that. He asks when he wants it.
|
|
60
|
+
4. NO JARGON. Plain words. If a technical term is genuinely needed, put the
|
|
61
|
+
plain meaning first and the term in brackets after it.
|
|
62
|
+
5. CUT ANYTHING NOT ASKED FOR.
|
|
63
|
+
6. Prefer a short list or a small table over a paragraph.
|
|
64
|
+
|
|
65
|
+
KEEP THESE. They are not optional, and dropping any one of them means the
|
|
66
|
+
rewrite is rejected and thrown away:
|
|
67
|
+
|
|
68
|
+
- The first line, if it is a dated status banner.
|
|
69
|
+
- EVERY question being asked of the reader. If the reply ends by asking him
|
|
70
|
+
something, that question MUST appear in your rewrite, as its own line, at
|
|
71
|
+
the end. This is the single most common way a rewrite is rejected.
|
|
72
|
+
- Any warning, failure, or thing that went wrong.
|
|
73
|
+
- File paths and links, exactly as written.
|
|
74
|
+
- Code blocks, exactly as written.
|
|
75
|
+
- Specific numbers and names.
|
|
76
|
+
|
|
77
|
+
Never:
|
|
78
|
+
- change a fact or a number
|
|
79
|
+
- drop a warning, a failure, or a question
|
|
80
|
+
- add anything that was not in the original
|
|
81
|
+
- soften a bad outcome
|
|
82
|
+
|
|
83
|
+
Return ONLY the rewritten reply. No commentary about what you changed.`;
|
|
84
|
+
|
|
85
|
+
function readConfig(projectDir) {
|
|
86
|
+
const p = path.join(projectDir, ".gsd-t", "concise.json");
|
|
87
|
+
if (!fs.existsSync(p)) return { ...DEFAULTS };
|
|
88
|
+
try {
|
|
89
|
+
return { ...DEFAULTS, ...JSON.parse(fs.readFileSync(p, "utf8")) };
|
|
90
|
+
} catch (_) {
|
|
91
|
+
// Unreadable config → defaults. Rewriting with defaults is the safe
|
|
92
|
+
// direction; it cannot lose content, only shorten it.
|
|
93
|
+
return { ...DEFAULTS };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function wordCount(s) {
|
|
98
|
+
return (String(s).trim().match(/\S+/g) || []).length;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Facts that must survive the rewrite. If any disappears, the rewrite is
|
|
103
|
+
* rejected and the original is returned — a rewrite that drops a warning or a
|
|
104
|
+
* question is worse than a long reply.
|
|
105
|
+
*/
|
|
106
|
+
function extractInvariants(text) {
|
|
107
|
+
return {
|
|
108
|
+
questions: (text.match(/[^.!?\n]*\?/g) || []).map((q) => q.trim()).filter((q) => q.length > 10),
|
|
109
|
+
numbers: text.match(/\b\d[\d,._]*\b/g) || [],
|
|
110
|
+
paths: text.match(/[\w./-]+\.(?:js|cjs|mjs|ts|tsx|json|md|py|sh)\b/g) || [],
|
|
111
|
+
codeBlocks: (text.match(/```/g) || []).length / 2,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function checkInvariants(original, rewritten) {
|
|
116
|
+
const a = extractInvariants(original);
|
|
117
|
+
const b = extractInvariants(rewritten);
|
|
118
|
+
const lost = [];
|
|
119
|
+
|
|
120
|
+
if (a.questions.length > 0 && b.questions.length === 0) {
|
|
121
|
+
lost.push("a question to the reader was dropped");
|
|
122
|
+
}
|
|
123
|
+
const lostPaths = a.paths.filter((p) => !rewritten.includes(p));
|
|
124
|
+
if (lostPaths.length > 0 && lostPaths.length === a.paths.length && a.paths.length > 0) {
|
|
125
|
+
lost.push("every file path was dropped");
|
|
126
|
+
}
|
|
127
|
+
if (a.codeBlocks > 0 && b.codeBlocks < a.codeBlocks) {
|
|
128
|
+
lost.push("a code block was dropped");
|
|
129
|
+
}
|
|
130
|
+
// A number appearing in the rewrite that was never in the original means
|
|
131
|
+
// something was invented.
|
|
132
|
+
const invented = b.numbers.filter((n) => n.length > 2 && !a.numbers.includes(n));
|
|
133
|
+
if (invented.length > 0) {
|
|
134
|
+
lost.push(`a number appeared that was not in the original: ${invented[0]}`);
|
|
135
|
+
}
|
|
136
|
+
return lost;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Ask a fresh Claude to do the rewrite. */
|
|
140
|
+
function rewrite(text, cfg) {
|
|
141
|
+
const prompt = `${RULES}\n\n--- REPLY TO REWRITE ---\n${text}`;
|
|
142
|
+
const run = spawnSync("claude",
|
|
143
|
+
["-p", prompt, "--model", cfg.model, "--dangerously-skip-permissions"],
|
|
144
|
+
{ encoding: "utf8", timeout: cfg.timeoutMs, maxBuffer: 8 * 1024 * 1024 });
|
|
145
|
+
|
|
146
|
+
if (run.error) return { ok: false, error: run.error.message };
|
|
147
|
+
if (run.status !== 0) return { ok: false, error: `claude exited ${run.status}: ${(run.stderr || "").slice(0, 200)}` };
|
|
148
|
+
const out = (run.stdout || "").trim();
|
|
149
|
+
if (!out) return { ok: false, error: "the rewriter returned nothing" };
|
|
150
|
+
return { ok: true, text: out };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function parseArgs(argv) {
|
|
154
|
+
const args = { project: process.cwd() };
|
|
155
|
+
for (let i = 2; i < argv.length; i++) {
|
|
156
|
+
const a = argv[i];
|
|
157
|
+
if (a === "--json") args.json = true;
|
|
158
|
+
else if (a === "--stdin") args.stdin = true;
|
|
159
|
+
else if (a === "--text") args.text = argv[++i];
|
|
160
|
+
else if (a === "--project") args.project = argv[++i];
|
|
161
|
+
}
|
|
162
|
+
return args;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function emit(result, asJson) {
|
|
166
|
+
if (asJson) process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
167
|
+
else process.stdout.write(result.text + "\n");
|
|
168
|
+
process.exit(result.exitCode);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function main() {
|
|
172
|
+
const args = parseArgs(process.argv);
|
|
173
|
+
const projectDir = path.resolve(args.project);
|
|
174
|
+
const cfg = readConfig(projectDir);
|
|
175
|
+
|
|
176
|
+
let text = args.text;
|
|
177
|
+
if (args.stdin) {
|
|
178
|
+
try { text = fs.readFileSync(0, "utf8"); } catch (_) { text = ""; }
|
|
179
|
+
}
|
|
180
|
+
if (typeof text !== "string" || !text.trim()) {
|
|
181
|
+
emit({ ok: false, exitCode: EXIT_BAD_INPUT, text: "", error: "no text given" }, true);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const before = wordCount(text);
|
|
185
|
+
|
|
186
|
+
if (!cfg.enabled) {
|
|
187
|
+
emit({ ok: true, exitCode: EXIT_OK, text, skipped: "switched off", words: before }, args.json);
|
|
188
|
+
}
|
|
189
|
+
if (before < cfg.skipUnder) {
|
|
190
|
+
emit({ ok: true, exitCode: EXIT_OK, text, skipped: "already short", words: before }, args.json);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const r = rewrite(text, cfg);
|
|
194
|
+
if (!r.ok) {
|
|
195
|
+
emit({
|
|
196
|
+
ok: false, exitCode: EXIT_FAILED, text,
|
|
197
|
+
error: r.error,
|
|
198
|
+
note: "Rewrite failed — this is the original, unchanged.",
|
|
199
|
+
words: before,
|
|
200
|
+
}, args.json);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const lost = checkInvariants(text, r.text);
|
|
204
|
+
if (lost.length > 0) {
|
|
205
|
+
emit({
|
|
206
|
+
ok: false, exitCode: EXIT_FAILED, text,
|
|
207
|
+
error: `the rewrite lost something: ${lost.join("; ")}`,
|
|
208
|
+
note: "Rewrite rejected — this is the original, unchanged.",
|
|
209
|
+
words: before,
|
|
210
|
+
}, args.json);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const after = wordCount(r.text);
|
|
214
|
+
emit({
|
|
215
|
+
ok: true, exitCode: EXIT_OK, text: r.text,
|
|
216
|
+
words: before, wordsAfter: after,
|
|
217
|
+
saved: before - after,
|
|
218
|
+
}, args.json);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (require.main === module) main();
|
|
222
|
+
|
|
223
|
+
module.exports = { wordCount, extractInvariants, checkInvariants, readConfig, RULES };
|