@tekyzinc/gsd-t 5.0.10 → 5.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [5.0.12] - 2026-07-12
6
+
7
+ ### Fixed — graph require-chain shipped incomplete → every project's graph silently dead
8
+
9
+ `bin/gsd-t-graph-query-cli.cjs` `require()`s `gsd-t-graph-store-resolver.cjs` at load, but that file (added to source in M99, 2026-06-30) was never added to `PROJECT_BIN_TOOLS` — so `update-all` copied the query CLI and 6 of its 7 dependencies and silently omitted the 7th. In every registered project the CLI threw on load → the graph was unqueryable → all graph consumers (execute/wave/debug/quick/impact/plan/scan **and `/gsd-t-architect`**) silently fell back to grep. Found by the Binvoice architect run reporting its own reuse-check was disabled. Same class as the M96 / global-bin propagation gaps.
10
+
11
+ - `bin/gsd-t.js`: added `gsd-t-graph-store-resolver.cjs` to `PROJECT_BIN_TOOLS`; exported `PROJECT_BIN_TOOLS`.
12
+ - `test/graph-require-chain-propagation.test.js` (NEW): statically walks the transitive `require("./…")` chain from the graph entry points and FAILS if any reachable `bin/` file is missing from `PROJECT_BIN_TOOLS` — the mechanical guard that turns this whole drift class into a red test instead of a silent grep-fallback. Proven to fail when the resolver is removed.
13
+ - `.gsd-t/contracts/graph-metrics-contract.md`: refreshed two `bin/gsd-t.js` line citations shifted by the fix.
14
+
15
+ ## [5.0.11] - 2026-07-12
16
+
17
+ ### Fixed — architect hook now ships + installs; de-flaked its test
18
+
19
+ The M101/v5.0.10 architect-oversight hook existed only in `~/.claude/scripts/` — it was never copied into the repo, so `install`/`update-all` would not propagate it to other machines (the "propagate a tool without shipping it = silent no-op" class). This release ships it properly and hardens it.
20
+
21
+ - `scripts/gsd-t-architect-oversight-guard.js`: the hook now lives in the repo (ships with the package). Added an unref'd 1500ms watchdog + a stdin `error` handler so the hook can NEVER hang and block a Write/Edit (fail-open guaranteed even if stdin never EOFs).
22
+ - `bin/gsd-t.js`: `configureArchitectHook()` registers the PreToolUse `Write|Edit` hook via the global-package command pattern (same as the graph/read-intercept hooks); wired into `install`; removed on `uninstall`. Exported for tests.
23
+ - `test/m101-architect-oversight-hook.test.js`: replaced the two spawn-per-assertion fail-open tests (which false-failed under full-suite CPU starvation, not a hook bug) with a source-guard assertion + installer idempotency/coexistence tests. Test resolves the repo hook copy.
24
+ - `.gsd-t/contracts/graph-metrics-contract.md`: refreshed the `doMetrics` line citation (shifted by the installer addition).
25
+
5
26
  ## [5.0.10] - 2026-07-12
6
27
 
7
28
  ### Added — `/gsd-t-architect`: run the Architect's Oversight pass on demand
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # GSD-T: Contract-Driven Development for Claude Code
2
2
 
3
- **v5.0.10** - A methodology for reliable, parallelizable development using Claude Code with optional Agent Teams support.
3
+ **v5.0.12** - 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.
package/bin/gsd-t.js CHANGED
@@ -485,6 +485,15 @@ const READ_INTERCEPT_HOOK_MARKER = "gsd-t-read-intercept";
485
485
  const READ_INTERCEPT_HOOK_COMMAND =
486
486
  'bash -c \'[ -f "$(npm root -g)/@tekyzinc/gsd-t/scripts/gsd-t-read-intercept.js" ] && node "$(npm root -g)/@tekyzinc/gsd-t/scripts/gsd-t-read-intercept.js" || true\'';
487
487
 
488
+ // M101 — architect-oversight PreToolUse hook on Write|Edit. Same global-package
489
+ // pattern as the intercept hooks: runs the script from the globally-installed
490
+ // package, and the script fails-open (no-op) in non-GSD-T projects / prose writes,
491
+ // so it is safe to register globally with a Write|Edit matcher. It reminds the model
492
+ // to run the Architect's Oversight Six-Stage Pass before writing code (M101).
493
+ const ARCHITECT_HOOK_MARKER = "gsd-t-architect-oversight-guard";
494
+ const ARCHITECT_HOOK_COMMAND =
495
+ 'bash -c \'[ -f "$(npm root -g)/@tekyzinc/gsd-t/scripts/gsd-t-architect-oversight-guard.js" ] && node "$(npm root -g)/@tekyzinc/gsd-t/scripts/gsd-t-architect-oversight-guard.js" || true\'';
496
+
488
497
  // Append entries to {projectDir}/.gitignore. Each entry added only if absent.
489
498
  // Idempotent. Returns true if any entries were added, false otherwise.
490
499
  function ensureGitignoreEntries(projectDir, entries) {
@@ -928,6 +937,60 @@ function configureReadInterceptHook(settingsPath) {
928
937
  return { installed: true, action };
929
938
  }
930
939
 
940
+ // M101 — register the architect-oversight PreToolUse hook (matcher "Write|Edit").
941
+ // Idempotent: find-by-marker, refresh stale command, else add. Mirrors the M98
942
+ // read-intercept installer but on PreToolUse. The script fails-open so this is
943
+ // safe globally (silent in non-GSD-T projects and on prose/doc writes).
944
+ function configureArchitectHook(settingsPath) {
945
+ const targetPath = settingsPath || SETTINGS_JSON;
946
+ let settings = {};
947
+ if (fs.existsSync(targetPath)) {
948
+ try {
949
+ settings = JSON.parse(fs.readFileSync(targetPath, "utf8"));
950
+ if (!settings || typeof settings !== "object") settings = {};
951
+ } catch {
952
+ warn("settings.json has invalid JSON — cannot configure architect hook");
953
+ return { installed: false, action: "noop" };
954
+ }
955
+ }
956
+ if (!settings.hooks) settings.hooks = {};
957
+ if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];
958
+
959
+ const cmd = ARCHITECT_HOOK_COMMAND;
960
+ let action = "noop";
961
+ let found = false;
962
+ for (const entry of settings.hooks.PreToolUse) {
963
+ if (!entry || !Array.isArray(entry.hooks)) continue;
964
+ for (const h of entry.hooks) {
965
+ if (!h || typeof h.command !== "string") continue;
966
+ if (h.command === cmd || h.command.includes(ARCHITECT_HOOK_MARKER)) {
967
+ found = true;
968
+ if (h.command !== cmd) { h.command = cmd; action = "updated"; }
969
+ if (entry.matcher !== "Write|Edit") { entry.matcher = "Write|Edit"; action = action === "noop" ? "updated" : action; }
970
+ }
971
+ }
972
+ }
973
+ if (!found) {
974
+ settings.hooks.PreToolUse.push({
975
+ matcher: "Write|Edit",
976
+ hooks: [{ type: "command", command: cmd }],
977
+ });
978
+ action = "added";
979
+ }
980
+ if (action === "noop") return { installed: true, action: "noop" };
981
+ if (isSymlink(targetPath)) {
982
+ warn("Skipping settings.json write — target is a symlink");
983
+ return { installed: false, action: "noop" };
984
+ }
985
+ try {
986
+ fs.writeFileSync(targetPath, JSON.stringify(settings, null, 2));
987
+ } catch (e) {
988
+ warn(`Failed to write settings.json: ${e.message}`);
989
+ return { installed: false, action: "noop" };
990
+ }
991
+ return { installed: true, action };
992
+ }
993
+
931
994
  // M98 — remove the GSD-T intercept PostToolUse hooks (grep + read) from settings.json.
932
995
  // Used during uninstall. Marker-based; leaves all other hooks intact. Idempotent.
933
996
  function removeInterceptHooks(settingsPath) {
@@ -941,17 +1004,28 @@ function removeInterceptHooks(settingsPath) {
941
1004
  warn("settings.json has invalid JSON — cannot remove intercept hooks");
942
1005
  return false;
943
1006
  }
944
- if (!settings.hooks || !Array.isArray(settings.hooks.PostToolUse)) return false;
1007
+ if (!settings.hooks) return false;
945
1008
 
946
- const markers = [GRAPH_INTERCEPT_HOOK_MARKER, READ_INTERCEPT_HOOK_MARKER];
947
- const before = settings.hooks.PostToolUse.length;
948
- settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter((entry) => {
949
- if (!entry || !Array.isArray(entry.hooks)) return true;
950
- return !entry.hooks.some(
951
- (h) => h && typeof h.command === "string" && markers.some((m) => h.command.includes(m))
952
- );
953
- });
954
- if (before - settings.hooks.PostToolUse.length === 0) return false;
1009
+ const postMarkers = [GRAPH_INTERCEPT_HOOK_MARKER, READ_INTERCEPT_HOOK_MARKER];
1010
+ const preMarkers = [ARCHITECT_HOOK_MARKER];
1011
+ let removed = 0;
1012
+
1013
+ const stripByMarkers = (arr, markers) => {
1014
+ if (!Array.isArray(arr)) return arr;
1015
+ const before = arr.length;
1016
+ const out = arr.filter((entry) => {
1017
+ if (!entry || !Array.isArray(entry.hooks)) return true;
1018
+ return !entry.hooks.some(
1019
+ (h) => h && typeof h.command === "string" && markers.some((m) => h.command.includes(m))
1020
+ );
1021
+ });
1022
+ removed += before - out.length;
1023
+ return out;
1024
+ };
1025
+
1026
+ settings.hooks.PostToolUse = stripByMarkers(settings.hooks.PostToolUse, postMarkers);
1027
+ settings.hooks.PreToolUse = stripByMarkers(settings.hooks.PreToolUse, preMarkers);
1028
+ if (removed === 0) return false;
955
1029
 
956
1030
  if (isSymlink(targetPath)) {
957
1031
  warn("Skipping settings.json write — target is a symlink");
@@ -1915,6 +1989,13 @@ async function doInstall(opts = {}) {
1915
1989
  else info("Read-intercept hook already configured");
1916
1990
  }
1917
1991
 
1992
+ const archHook = configureArchitectHook(SETTINGS_JSON);
1993
+ if (archHook.installed) {
1994
+ if (archHook.action === "added") success("Architect-oversight hook added (Six-Stage Pass reminder before writing code — M101)");
1995
+ else if (archHook.action === "updated") success("Architect-oversight hook refreshed");
1996
+ else info("Architect-oversight hook already configured");
1997
+ }
1998
+
1918
1999
  heading("Graph Engine (CGC)");
1919
2000
  installCgc();
1920
2001
 
@@ -2901,6 +2982,13 @@ const PROJECT_BIN_TOOLS = [
2901
2982
  // [[project_code_graph_universal_consumer]] [[feedback_graph_is_architectural_anchor]]
2902
2983
  "gsd-t-graph-query-cli.cjs", "gsd-t-graph-index.cjs", "gsd-t-graph-freshness.cjs",
2903
2984
  "gsd-t-graph-edge-extract.cjs", "gsd-t-graph-scip-upgrade.cjs", "gsd-t-scip-reader.cjs",
2985
+ // M99 (added 2026-06-30, resolver added to the copy list 2026-07-12) — the query
2986
+ // CLI `require`s gsd-t-graph-store-resolver.cjs at load time (line ~76). It was
2987
+ // added to the source in M99 but NEVER added here, so update-all copied the CLI
2988
+ // and 6 of its 7 deps and silently omitted this one → the CLI threw on require →
2989
+ // graph dead → grep fallback (found by the Binvoice architect run 2026-07-12).
2990
+ // Same class as [[project_global_bin_propagation_gap]] / [[project_m96_graph_runs_in_projects]].
2991
+ "gsd-t-graph-store-resolver.cjs",
2904
2992
  // M96 — multi-location resolver for the store engine (better-sqlite3), so a
2905
2993
  // copied tool finds the engine from the GSD-T global package, not the project's
2906
2994
  // own (usually absent) node_modules. Fail-loud with remediation if all miss.
@@ -4831,6 +4919,7 @@ module.exports = {
4831
4919
  ensureDir,
4832
4920
  copyFile,
4833
4921
  copyBinToolsToProject,
4922
+ PROJECT_BIN_TOOLS,
4834
4923
  hasPlaywright,
4835
4924
  hasSwagger,
4836
4925
  hasApi,
@@ -4909,6 +4998,7 @@ module.exports = {
4909
4998
  // M97/M98: intercept hook installers
4910
4999
  configureGraphInterceptHook,
4911
5000
  configureReadInterceptHook,
5001
+ configureArchitectHook,
4912
5002
  removeInterceptHooks,
4913
5003
  promptForApiKeyIfMissing,
4914
5004
  resolveApiKeyEnvVar,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "5.0.10",
3
+ "version": "5.0.12",
4
4
  "description": "GSD-T: Contract-Driven Development for Claude Code — 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
5
5
  "author": "Tekyz, Inc.",
6
6
  "license": "MIT",
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * GSD-T PreToolUse hook (Write|Edit) — Architect's Oversight trigger (M101).
4
+ *
5
+ * The Architect's Oversight Doctrine lives in ~/.claude/CLAUDE.md, but a doctrine
6
+ * carried in context can be MISSED under load. This hook fires the TRIGGER at the
7
+ * exact moment it matters — right before code is written/edited — and injects a
8
+ * ONE-LINE pointer back to the doctrine. It carries NO nuance (that stays in
9
+ * CLAUDE.md); it only guarantees the Six-Stage Pass is CONSIDERED before a build.
10
+ *
11
+ * Design (mirrors the doctrine's §Enforcement three-layer split):
12
+ * - CLAUDE.md = the definition (what the doctrine IS)
13
+ * - THIS hook = the trigger (don't forget it right now, no context bloat)
14
+ * - workflow = the execution (actually run the stages, with evidence)
15
+ *
16
+ * Receives JSON on stdin: { tool_name, tool_input: { file_path, ... }, cwd, ... }
17
+ * Emits (stdout, PreToolUse additionalContext): a single reminder line, or nothing.
18
+ *
19
+ * NON-NEGOTIABLE: fail-open. Never block the tool, never throw, never exit non-zero.
20
+ * A malformed payload, missing field, or any error → silent pass-through (exit 0,
21
+ * no output). This hook can only ADD a reminder; it can never stop a write.
22
+ *
23
+ * Scope gates (emit ONLY when all hold), to avoid noise:
24
+ * 1. GSD-T project — cwd contains .gsd-t/ (matches the auto-route/profile gate).
25
+ * 2. The target is CODE, not prose — skip .md / .txt / pseudocode / docs, since
26
+ * the doctrine's own artifacts (CLAUDE.md, pseudocode, contracts) are writes
27
+ * too, and reminding while writing the reminder is noise. Code = the moment
28
+ * the Binvoice waste would have been caught.
29
+ *
30
+ * Zero-dep by design: runs from ~/.claude/scripts where the package may be absent.
31
+ */
32
+
33
+ const fs = require("fs");
34
+ const path = require("path");
35
+
36
+ // One line. A pointer, not the doctrine. Keeps context cost ~nil.
37
+ const REMINDER =
38
+ "[GSD-T ARCHITECT] About to write/edit code — run the Architect's Oversight " +
39
+ "Six-Stage Pass FIRST (Objective → Conflict → Reuse[query the graph] → " +
40
+ "Simplicity → Reuse-forecast → Risk), each answered with evidence not conviction. " +
41
+ "Is this the simplest design, and does something reusable already exist? " +
42
+ "See ~/.claude/CLAUDE.md § Architect's Oversight Doctrine.";
43
+
44
+ // File extensions that are PROSE/config, not code — skip the reminder for these.
45
+ // (The doctrine's own artifacts are markdown; reminding while authoring them is noise.)
46
+ const PROSE_EXT = new Set([
47
+ ".md", ".markdown", ".txt", ".rst", ".adoc",
48
+ ".json", ".yaml", ".yml", ".toml", ".lock",
49
+ ".csv", ".tsv", ".svg", ".png", ".jpg", ".jpeg", ".gif", ".pdf",
50
+ ]);
51
+
52
+ /**
53
+ * Decide whether to emit the reminder for a given cwd + target file.
54
+ * Pure + total: never throws, returns a boolean.
55
+ * @param {string} cwd
56
+ * @param {string} filePath — the Write/Edit target
57
+ * @returns {boolean}
58
+ */
59
+ function shouldRemind(cwd, filePath) {
60
+ try {
61
+ if (!cwd || typeof cwd !== "string") return false;
62
+ // Gate 1 — GSD-T project only.
63
+ if (!fs.existsSync(path.join(cwd, ".gsd-t"))) return false;
64
+ // No target path → can't classify → stay quiet (fail-open toward silence).
65
+ if (!filePath || typeof filePath !== "string") return false;
66
+
67
+ const lower = filePath.toLowerCase();
68
+ const ext = path.extname(lower);
69
+
70
+ // Gate 2 — skip prose/config/asset writes (incl. the doctrine's own artifacts).
71
+ if (PROSE_EXT.has(ext)) return false;
72
+ // Skip anything under a docs/ or pseudocode/ tree even if code-extensioned.
73
+ if (/[\\/](docs|pseudocode)[\\/]/.test(lower)) return false;
74
+
75
+ return true;
76
+ } catch (_) {
77
+ return false; // fail-open: any error → no reminder, never block
78
+ }
79
+ }
80
+
81
+ if (require.main === module) {
82
+ let input = "";
83
+ let done = false;
84
+ process.stdin.setEncoding("utf8");
85
+ process.stdin.on("data", (c) => { input += c; });
86
+
87
+ const finish = () => {
88
+ if (done) return; // run once — whichever of end/error/watchdog fires first
89
+ done = true;
90
+ try {
91
+ let data = null;
92
+ try { data = JSON.parse(input); } catch (_) { process.exit(0); }
93
+ if (!data || typeof data !== "object") process.exit(0);
94
+
95
+ const cwd = (typeof data.cwd === "string" && data.cwd) ? data.cwd : process.cwd();
96
+ const ti = data.tool_input && typeof data.tool_input === "object" ? data.tool_input : {};
97
+ const filePath = typeof ti.file_path === "string" ? ti.file_path : "";
98
+
99
+ if (shouldRemind(cwd, filePath)) {
100
+ process.stdout.write(REMINDER + "\n");
101
+ }
102
+ } catch (_) {
103
+ // Belt-and-suspenders: never block a write.
104
+ }
105
+ process.exit(0);
106
+ };
107
+
108
+ process.stdin.on("end", finish);
109
+ process.stdin.on("error", finish); // stdin read error → fail-open, never hang
110
+ // Watchdog: if stdin never delivers EOF (spawn-under-load edge, closed pipe),
111
+ // proceed anyway with whatever we have. A hook must NEVER hang and block a write.
112
+ const wd = setTimeout(finish, 1500);
113
+ if (wd.unref) wd.unref(); // don't keep the event loop alive solely for the watchdog
114
+ }
115
+
116
+ module.exports = { shouldRemind, REMINDER };