@tekyzinc/gsd-t 5.10.12 → 5.11.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 +59 -0
- package/README.md +1 -1
- package/bin/cli-preflight-checks/branch-guard.cjs +109 -30
- package/bin/cli-preflight.cjs +20 -0
- package/bin/gsd-t-concise-rewrite.cjs +27 -2
- package/bin/gsd-t-fallback-detect.cjs +5 -1
- package/bin/gsd-t-install-check.cjs +41 -1
- package/bin/gsd-t-pick-worktree.cjs +289 -0
- package/bin/gsd-t-worktree-detect.cjs +62 -0
- package/bin/gsd-t-worktree-provision.cjs +336 -0
- package/bin/gsd-t.js +116 -5
- package/docs/architecture.md +8 -1
- package/package.json +1 -1
- package/scripts/gsd-t-concise-hook.js +53 -4
- package/scripts/statusline-command.sh +11 -2
- package/templates/CLAUDE-global.md +4 -0
- package/templates/CLAUDE-project.md +1 -1
- package/scripts/gsd-t-worktree-guard.js +0 -183
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,65 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to GSD-T are documented here. Updated with each release.
|
|
4
4
|
|
|
5
|
+
## [5.11.10] - 2026-08-09
|
|
6
|
+
|
|
7
|
+
### Fixed — four safety gates were passing without checking anything
|
|
8
|
+
|
|
9
|
+
A branch warning turned into an audit. Every fault below is the same shape: a
|
|
10
|
+
check that verifies nothing and reports a pass.
|
|
11
|
+
|
|
12
|
+
- The branch check was dead in every scaffolded project. It matched only the
|
|
13
|
+
phrase `Expected branch:`, while the project template wrote a table row
|
|
14
|
+
labelled `Branch`. Nothing fills that row mechanically, so both sides
|
|
15
|
+
changed: the row is renamed and the reader accepts either shape.
|
|
16
|
+
- The same check blocked the workflow the house rules mandate — a worktree on a
|
|
17
|
+
feature branch is correct by design, yet was flagged. In a worktree a named
|
|
18
|
+
branch now passes and says why; a detached HEAD fails, because commits made
|
|
19
|
+
with no branch attached are easily lost.
|
|
20
|
+
- An unreadable `CLAUDE.md` passed as "no rules". It halts now.
|
|
21
|
+
- The entire preflight gate was decorative in every installed project:
|
|
22
|
+
`cli-preflight.cjs` shipped without its six checks, found an empty directory,
|
|
23
|
+
and returned `ok:true`. Measured live: `checks run: 0`. Directories under
|
|
24
|
+
`bin/` now ship, and an empty registry fails instead of passing.
|
|
25
|
+
- Install-repair overwrote in-progress edits in GSD-T's own repo, where `bin/`
|
|
26
|
+
is the source rather than a copy of the published build. It now refuses that
|
|
27
|
+
repo, identified by its package name.
|
|
28
|
+
|
|
29
|
+
### Fixed — the reply shortener never ran once, then hung when it did
|
|
30
|
+
|
|
31
|
+
Two faults stacked, so neither was visible. The hook read only the last record
|
|
32
|
+
of a turn; a turn almost always ends with a tool call, so it found no prose and
|
|
33
|
+
gave up. Underneath that, the rewrite itself hung: given a reply full of file
|
|
34
|
+
paths and commands, the spawned Claude read them as work to do and went off
|
|
35
|
+
running them — 45 seconds, killed on timeout, empty output and empty stderr,
|
|
36
|
+
every turn. Shortening prose needs no tools, so they are now forbidden.
|
|
37
|
+
Measured end to end: 41s and nothing, to 10.9s and a correct rewrite. A timeout
|
|
38
|
+
now writes to stderr; silence is what let it hide.
|
|
39
|
+
|
|
40
|
+
### Added — a new worktree arrives runnable
|
|
41
|
+
|
|
42
|
+
`git worktree add` brings only tracked files, so `.env`, local settings and
|
|
43
|
+
installed dependencies stayed behind and the folder was born unable to run.
|
|
44
|
+
Provisioning carries local config (permissions preserved, so a 600 secret does
|
|
45
|
+
not become world-readable), skips per-session `.gsd-t` state, and reinstalls
|
|
46
|
+
dependencies from the worktree's own lockfile.
|
|
47
|
+
|
|
48
|
+
- `bin/cli-preflight-checks/branch-guard.cjs`: reads both rule shapes, worktree-aware, halts on an unreadable file
|
|
49
|
+
- `bin/cli-preflight.cjs`: an empty check registry fails instead of passing
|
|
50
|
+
- `bin/gsd-t-worktree-detect.cjs`: shared worktree test using git's own answer
|
|
51
|
+
- `bin/gsd-t-worktree-provision.cjs`: carry config, skip state, install deps
|
|
52
|
+
- `bin/gsd-t-install-check.cjs`: refuses to "repair" the GSD-T source repo
|
|
53
|
+
- `bin/gsd-t-concise-rewrite.cjs`: no tools, and a loud timeout
|
|
54
|
+
- `scripts/gsd-t-concise-hook.js`: finds the turn's prose past its tool calls
|
|
55
|
+
- `bin/gsd-t.js`: directories under `bin/` now propagate
|
|
56
|
+
- `templates/CLAUDE-project.md`: the branch row the reader can actually find
|
|
57
|
+
- `test/m112-worktree-provision.test.js`, `test/m107-concise-rewrite.test.js`: 24 new tests
|
|
58
|
+
|
|
59
|
+
Known limitation: `contracts-stable` has never fired once (its pattern expects
|
|
60
|
+
emphasis after `Status`, every real file writes it before) and `ports-free`
|
|
61
|
+
reads a config key nothing writes. Both are recorded and left for a separate
|
|
62
|
+
pass.
|
|
63
|
+
|
|
5
64
|
## [5.10.10] - 2026-08-07
|
|
6
65
|
|
|
7
66
|
### Added — a project's CLAUDE.md is written from what actually happened in it
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GSD-T: Contract-Driven Development for Claude Code
|
|
2
2
|
|
|
3
|
-
**v5.10
|
|
3
|
+
**v5.11.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.
|
|
@@ -3,40 +3,67 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* branch-guard — verify current git branch matches CLAUDE.md "Expected branch" rule.
|
|
5
5
|
*
|
|
6
|
-
* Severity: error (blocks). If CLAUDE.md
|
|
7
|
-
* passes with
|
|
6
|
+
* Severity: error (blocks). If CLAUDE.md declares no expected-branch rule, the
|
|
7
|
+
* check passes with a message that NAMES the gap ("NOT CHECKED — …") rather than
|
|
8
|
+
* one that reads like approval: a pass that compared nothing must say so.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
+
* The rule governs the MAIN checkout. Inside a linked worktree (a side copy of
|
|
11
|
+
* the repo) the house rules require a feature branch, so being off the declared
|
|
12
|
+
* branch there is correct, not a violation — the check passes and says why. The
|
|
13
|
+
* one genuinely dangerous state in a side copy is a detached HEAD, where commits
|
|
14
|
+
* belong to no branch and are easily lost; that still fails.
|
|
15
|
+
*
|
|
16
|
+
* An unreadable CLAUDE.md HALTS. Treating a permissions or corruption failure as
|
|
17
|
+
* "no rules to enforce" is a real failure wearing a pass.
|
|
18
|
+
*
|
|
19
|
+
* Pure inspection — runs read-only git commands and reads CLAUDE.md.
|
|
10
20
|
*/
|
|
11
21
|
|
|
12
22
|
const fs = require('fs');
|
|
13
23
|
const path = require('path');
|
|
14
24
|
const { execSync } = require('child_process');
|
|
25
|
+
const { isLinkedWorktree } = require('../gsd-t-worktree-detect.cjs');
|
|
15
26
|
|
|
16
27
|
const ID = 'branch-guard';
|
|
17
28
|
|
|
29
|
+
// Distinguishes "the file is not there" (ordinary) from "the file is there and
|
|
30
|
+
// could not be read" (a failure). Collapsing both to null is what let a real
|
|
31
|
+
// error pass as a clean run.
|
|
32
|
+
const NO_FILE = Symbol('no-claude-md');
|
|
33
|
+
|
|
18
34
|
function _readClaudeMd(projectDir) {
|
|
19
35
|
const file = path.join(projectDir, 'CLAUDE.md');
|
|
20
|
-
if (!fs.existsSync(file)) return
|
|
21
|
-
try
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
36
|
+
if (!fs.existsSync(file)) return NO_FILE;
|
|
37
|
+
// Deliberately no try/catch: an unreadable rules file is a real failure and
|
|
38
|
+
// must reach the caller.
|
|
39
|
+
return fs.readFileSync(file, 'utf8');
|
|
26
40
|
}
|
|
27
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Pull the expected branch out of CLAUDE.md, in either shape a project writes it.
|
|
44
|
+
*
|
|
45
|
+
* Nothing fills this in mechanically — /gsd-t-setup is a prose command, so a
|
|
46
|
+
* person or an assistant types the line by hand. Accepting only one shape is how
|
|
47
|
+
* the guard came to sit dead in every scaffolded project: the template wrote a
|
|
48
|
+
* table row and the reader looked for a sentence.
|
|
49
|
+
*
|
|
50
|
+
* Sentence: "Expected branch: main" "**Expected branch**: `develop`"
|
|
51
|
+
* Table row: "| Expected branch | `main` |"
|
|
52
|
+
*
|
|
53
|
+
* Returns the branch name, or an empty string when the project states no rule.
|
|
54
|
+
*/
|
|
28
55
|
function _extractExpectedBranch(text) {
|
|
29
|
-
if (!text) return
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return
|
|
56
|
+
if (!text || typeof text !== 'string') return '';
|
|
57
|
+
|
|
58
|
+
const row = text.match(
|
|
59
|
+
/^\s*\|\s*\**\s*expected\s+branch\s*\**\s*\|\s*`?([^\s`|*]+)`?\s*\|/im
|
|
60
|
+
);
|
|
61
|
+
if (row) return row[1].trim();
|
|
62
|
+
|
|
63
|
+
const line = text.match(/\*{0,2}\s*expected\s+branch\s*\*{0,2}\s*:\s*\**\s*`?([^\s`*\n|]+)`?/i);
|
|
64
|
+
if (line) return line[1].trim();
|
|
65
|
+
|
|
66
|
+
return '';
|
|
40
67
|
}
|
|
41
68
|
|
|
42
69
|
function _currentBranch(projectDir) {
|
|
@@ -50,18 +77,36 @@ function _currentBranch(projectDir) {
|
|
|
50
77
|
}
|
|
51
78
|
|
|
52
79
|
function run({ projectDir }) {
|
|
53
|
-
|
|
54
|
-
|
|
80
|
+
let md;
|
|
81
|
+
try {
|
|
82
|
+
md = _readClaudeMd(projectDir);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
msg:
|
|
87
|
+
'CLAUDE.md exists but could not be read (' +
|
|
88
|
+
((err && err.message) || err) +
|
|
89
|
+
') — the branch rule cannot be checked; fix the file or its permissions',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (md === NO_FILE) {
|
|
55
94
|
return {
|
|
56
95
|
ok: true,
|
|
57
|
-
msg: 'no CLAUDE.md
|
|
96
|
+
msg: 'NOT CHECKED — no CLAUDE.md in this project',
|
|
58
97
|
};
|
|
59
98
|
}
|
|
99
|
+
|
|
100
|
+
// A project stating no rule is a fact about the project, and the verdict below
|
|
101
|
+
// reports exactly that. Nothing was looked up and lost, and no value stands in
|
|
102
|
+
// for a missing one.
|
|
60
103
|
const expected = _extractExpectedBranch(md);
|
|
61
|
-
|
|
104
|
+
const declaresARule = expected.length > 0;
|
|
105
|
+
|
|
106
|
+
if (declaresARule === false) {
|
|
62
107
|
return {
|
|
63
108
|
ok: true,
|
|
64
|
-
msg: 'no expected
|
|
109
|
+
msg: 'NOT CHECKED — this project declares no expected branch',
|
|
65
110
|
};
|
|
66
111
|
}
|
|
67
112
|
|
|
@@ -71,16 +116,49 @@ function run({ projectDir }) {
|
|
|
71
116
|
} catch (err) {
|
|
72
117
|
return {
|
|
73
118
|
ok: false,
|
|
74
|
-
msg: 'git branch --show-current failed: ' + (err && err.message || err),
|
|
119
|
+
msg: 'git branch --show-current failed: ' + ((err && err.message) || err),
|
|
75
120
|
details: { expected },
|
|
76
121
|
};
|
|
77
122
|
}
|
|
78
123
|
|
|
79
|
-
|
|
124
|
+
// A side copy of the repo is MEANT to be on its own branch — the house rules
|
|
125
|
+
// require it. Only the main checkout is held to the declared branch.
|
|
126
|
+
let inWorktree;
|
|
127
|
+
try {
|
|
128
|
+
inWorktree = isLinkedWorktree(projectDir);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
msg: 'could not tell whether this is a worktree: ' + ((err && err.message) || err),
|
|
133
|
+
details: { expected, actual },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (inWorktree === true) {
|
|
138
|
+
if (actual === '') {
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
msg: 'not on a branch in this worktree — commits made here would be lost',
|
|
142
|
+
details: { expected, actual: '', worktree: true },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
ok: true,
|
|
147
|
+
msg:
|
|
148
|
+
'worktree on ' +
|
|
149
|
+
actual +
|
|
150
|
+
' — the expected-branch rule (' +
|
|
151
|
+
expected +
|
|
152
|
+
') governs the main checkout only',
|
|
153
|
+
details: { expected, actual, worktree: true },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (actual === '') {
|
|
80
158
|
return {
|
|
81
159
|
ok: false,
|
|
82
160
|
msg: 'detached HEAD or empty branch (expected ' + expected + ')',
|
|
83
|
-
details: { expected, actual: '' },
|
|
161
|
+
details: { expected, actual: '', worktree: false },
|
|
84
162
|
};
|
|
85
163
|
}
|
|
86
164
|
|
|
@@ -88,14 +166,14 @@ function run({ projectDir }) {
|
|
|
88
166
|
return {
|
|
89
167
|
ok: true,
|
|
90
168
|
msg: 'on expected branch ' + expected,
|
|
91
|
-
details: { expected, actual },
|
|
169
|
+
details: { expected, actual, worktree: false },
|
|
92
170
|
};
|
|
93
171
|
}
|
|
94
172
|
|
|
95
173
|
return {
|
|
96
174
|
ok: false,
|
|
97
175
|
msg: 'on ' + actual + ', expected ' + expected,
|
|
98
|
-
details: { expected, actual },
|
|
176
|
+
details: { expected, actual, worktree: false },
|
|
99
177
|
};
|
|
100
178
|
}
|
|
101
179
|
|
|
@@ -107,4 +185,5 @@ module.exports = {
|
|
|
107
185
|
_extractExpectedBranch,
|
|
108
186
|
_readClaudeMd,
|
|
109
187
|
_currentBranch,
|
|
188
|
+
NO_FILE,
|
|
110
189
|
};
|
package/bin/cli-preflight.cjs
CHANGED
|
@@ -57,6 +57,26 @@ function runPreflight(opts) {
|
|
|
57
57
|
checkResults.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
58
58
|
notes.sort();
|
|
59
59
|
|
|
60
|
+
// A run that loaded no checks verified nothing, and must never report a clean
|
|
61
|
+
// pass. The global install shipped this runner without its checks directory
|
|
62
|
+
// for months: it recorded "checks dir unreadable" in a note and returned
|
|
63
|
+
// ok:true anyway, so every preflight in every installed project passed
|
|
64
|
+
// without looking at anything. A note nobody reads is not a report.
|
|
65
|
+
const nothingRan = registry.length === 0;
|
|
66
|
+
if (nothingRan) {
|
|
67
|
+
notes.push(
|
|
68
|
+
'NOT CHECKED — no preflight checks were loaded, so nothing was verified. ' +
|
|
69
|
+
'The checks directory is missing from this install; reinstall GSD-T.'
|
|
70
|
+
);
|
|
71
|
+
notes.sort();
|
|
72
|
+
return {
|
|
73
|
+
schemaVersion: SCHEMA_VERSION,
|
|
74
|
+
ok: false,
|
|
75
|
+
checks: checkResults,
|
|
76
|
+
notes,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
60
80
|
const ok = !checkResults.some((c) => c.ok === false && c.severity === 'error');
|
|
61
81
|
|
|
62
82
|
return {
|
|
@@ -136,14 +136,39 @@ function checkInvariants(original, rewritten) {
|
|
|
136
136
|
return lost;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// Shortening prose needs no tools. Left able to use them, the rewriter read the
|
|
140
|
+
// file paths and commands INSIDE the reply as work to do and went off running
|
|
141
|
+
// them — 41 seconds, killed on timeout, empty output and empty stderr, on every
|
|
142
|
+
// turn for two days. Forbidding tools returns the same rewrite in about 7
|
|
143
|
+
// seconds. The list is explicit rather than a mode flag so a newly added tool
|
|
144
|
+
// cannot quietly re-open the same hole.
|
|
145
|
+
const NO_TOOLS = [
|
|
146
|
+
"Bash", "Read", "Write", "Edit", "NotebookEdit", "Glob", "Grep",
|
|
147
|
+
"Task", "Agent", "WebFetch", "WebSearch", "TodoWrite",
|
|
148
|
+
].join(",");
|
|
149
|
+
|
|
139
150
|
/** Ask a fresh Claude to do the rewrite. */
|
|
140
151
|
function rewrite(text, cfg) {
|
|
141
152
|
const prompt = `${RULES}\n\n--- REPLY TO REWRITE ---\n${text}`;
|
|
142
153
|
const run = spawnSync("claude",
|
|
143
|
-
["-p", prompt, "--model", cfg.model, "--dangerously-skip-permissions"
|
|
154
|
+
["-p", prompt, "--model", cfg.model, "--dangerously-skip-permissions",
|
|
155
|
+
"--disallowed-tools", NO_TOOLS],
|
|
144
156
|
{ encoding: "utf8", timeout: cfg.timeoutMs, maxBuffer: 8 * 1024 * 1024 });
|
|
145
157
|
|
|
146
|
-
if (run.error)
|
|
158
|
+
if (run.error) {
|
|
159
|
+
// A timeout is the failure that hid for two days: every turn paid the full
|
|
160
|
+
// wait, produced nothing, and said nothing. Write it to stderr so the cost
|
|
161
|
+
// is visible even though the reply still goes through untouched.
|
|
162
|
+
const timedOut = run.error.code === "ETIMEDOUT";
|
|
163
|
+
if (timedOut) {
|
|
164
|
+
process.stderr.write(
|
|
165
|
+
`[gsd-t] the concise rewriter timed out after ${Math.round(cfg.timeoutMs / 1000)}s ` +
|
|
166
|
+
`and produced nothing — every turn is paying that wait. Switch it off with ` +
|
|
167
|
+
`.gsd-t/concise.json {"enabled": false} until it is fixed.\n`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return { ok: false, error: run.error.message, timedOut };
|
|
171
|
+
}
|
|
147
172
|
if (run.status !== 0) return { ok: false, error: `claude exited ${run.status}: ${(run.stderr || "").slice(0, 200)}` };
|
|
148
173
|
const out = (run.stdout || "").trim();
|
|
149
174
|
if (!out) return { ok: false, error: "the rewriter returned nothing" };
|
|
@@ -375,7 +375,11 @@ function scanStructural(clean, file, findings) {
|
|
|
375
375
|
}
|
|
376
376
|
|
|
377
377
|
// retry loop that gives up and proceeds
|
|
378
|
-
|
|
378
|
+
// `\b` before the alternation is not enough: "entries" ends in "tries", so
|
|
379
|
+
// `for (const e of entries)` matched — a plain iteration read as a retry loop.
|
|
380
|
+
// Requiring a word boundary on BOTH sides keeps the real shapes (attempt,
|
|
381
|
+
// retry, retries, tries) and drops words that merely end in one.
|
|
382
|
+
const retryRe = /\b(?:for|while)\s*\([^)]*\b(?:attempt|retry|retries|tries)\b[^)]*\)\s*\{/gi;
|
|
379
383
|
while ((m = retryRe.exec(clean)) !== null) {
|
|
380
384
|
const braceIdx = clean.indexOf("{", m.index);
|
|
381
385
|
if (braceIdx === -1) continue;
|
|
@@ -116,6 +116,28 @@ function packageVersion(pkgRoot) {
|
|
|
116
116
|
* Returns { missing, stale, present } — stale means the file exists but its
|
|
117
117
|
* content differs from the package's copy.
|
|
118
118
|
*/
|
|
119
|
+
/**
|
|
120
|
+
* Is this directory GSD-T's own source repo (or a worktree of it)?
|
|
121
|
+
*
|
|
122
|
+
* Identified by the package name in its own package.json — the one fact that
|
|
123
|
+
* cannot drift, since it IS what npm publishes. A path check would miss a
|
|
124
|
+
* worktree under ~/Worktrees, and a marker file would need maintaining.
|
|
125
|
+
*
|
|
126
|
+
* An unreadable or absent package.json means "not the source repo": an ordinary
|
|
127
|
+
* project that happens to have no manifest still deserves its repair.
|
|
128
|
+
*/
|
|
129
|
+
function isOwnSourceRepo(dir) {
|
|
130
|
+
const manifest = path.join(dir, "package.json");
|
|
131
|
+
if (!fs.existsSync(manifest)) return false;
|
|
132
|
+
let parsed;
|
|
133
|
+
try {
|
|
134
|
+
parsed = JSON.parse(fs.readFileSync(manifest, "utf8"));
|
|
135
|
+
} catch (_) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return parsed && parsed.name === "@tekyzinc/gsd-t";
|
|
139
|
+
}
|
|
140
|
+
|
|
119
141
|
function inspect(projectDir, pkgRoot, tools) {
|
|
120
142
|
const binDir = path.join(projectDir, "bin");
|
|
121
143
|
const missing = [];
|
|
@@ -245,6 +267,24 @@ function main() {
|
|
|
245
267
|
process.exit(EXIT_OK);
|
|
246
268
|
}
|
|
247
269
|
|
|
270
|
+
// In a project, bin/ holds COPIES of the package's tools, so restoring one
|
|
271
|
+
// from the package is a repair. In GSD-T's own repo those same files are the
|
|
272
|
+
// SOURCE, and "repairing" them overwrites work in progress with the last
|
|
273
|
+
// published build. That happened: on 2026-08-09 a test run triggered a repair
|
|
274
|
+
// here and silently reverted two edited files mid-session, with the change
|
|
275
|
+
// discoverable only by reading the diff before committing.
|
|
276
|
+
if (isOwnSourceRepo(projectDir)) {
|
|
277
|
+
const msg =
|
|
278
|
+
`${projectDir} is the GSD-T source repo — bin/ here is the source, not a ` +
|
|
279
|
+
`copy of it. Repairing would overwrite your work with the published build.`;
|
|
280
|
+
if (args.json) {
|
|
281
|
+
process.stdout.write(JSON.stringify({ ok: true, exitCode: EXIT_OK, skipped: msg }, null, 2) + "\n");
|
|
282
|
+
} else {
|
|
283
|
+
process.stderr.write(`[gsd-t] ${msg}\n`);
|
|
284
|
+
}
|
|
285
|
+
process.exit(EXIT_OK);
|
|
286
|
+
}
|
|
287
|
+
|
|
248
288
|
let pkgRoot;
|
|
249
289
|
try {
|
|
250
290
|
pkgRoot = findPackageRoot();
|
|
@@ -339,4 +379,4 @@ function main() {
|
|
|
339
379
|
|
|
340
380
|
if (require.main === module) main();
|
|
341
381
|
|
|
342
|
-
module.exports = { inspect, expectedTools, findPackageRoot, readLog, installerSuspects, LOG_PATH };
|
|
382
|
+
module.exports = { inspect, expectedTools, findPackageRoot, readLog, installerSuspects, isOwnSourceRepo, LOG_PATH };
|