@hegemonart/get-design-done 1.59.6 → 1.59.8
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +55 -0
- package/README.md +4 -13
- package/SKILL.md +1 -1
- package/agents/design-authority-watcher.md +24 -5
- package/bin/gdd-graph +4 -1
- package/docs/i18n/README.de.md +210 -527
- package/docs/i18n/README.fr.md +201 -518
- package/docs/i18n/README.it.md +209 -526
- package/docs/i18n/README.ja.md +207 -524
- package/docs/i18n/README.ko.md +208 -525
- package/docs/i18n/README.zh-CN.md +213 -551
- package/hooks/_hook-emit.js +113 -29
- package/hooks/budget-enforcer.ts +44 -5
- package/hooks/gdd-mcp-circuit-breaker.js +72 -3
- package/hooks/gdd-sessionstart-recap.js +23 -14
- package/hooks/hooks.json +2 -2
- package/package.json +2 -2
- package/reference/bandit-integration.md +13 -2
- package/scripts/bootstrap.cjs +40 -8
- package/scripts/install.cjs +23 -1
- package/scripts/lib/bandit-router.cjs +47 -5
- package/scripts/lib/detect/cli.cjs +13 -3
- package/scripts/lib/install/converters/cursor.cjs +11 -19
- package/scripts/lib/install/doctor-codex-plugin.cjs +1 -1
- package/scripts/lib/install/doctor-cursor-marketplace.cjs +2 -2
- package/scripts/lib/install/installer.cjs +72 -21
- package/scripts/lib/install/merge.cjs +31 -3
- package/scripts/lib/install/runtime-artifact-layout.cjs +42 -8
- package/scripts/lib/manifest/harnesses.json +29 -1
- package/scripts/lib/manifest/skills.json +1 -1
- package/scripts/skill-templates/bandit-reset/SKILL.md +2 -0
- package/scripts/skill-templates/bandit-status/SKILL.md +4 -1
- package/scripts/skill-templates/darkmode/SKILL.md +1 -1
- package/scripts/skill-templates/graphify/SKILL.md +6 -6
- package/scripts/skill-templates/quick/SKILL.md +3 -1
- package/scripts/skill-templates/reflect/SKILL.md +1 -1
- package/scripts/skill-templates/router/SKILL.md +4 -2
- package/sdk/cli/index.js +114 -47
- package/sdk/dashboard/data/source.cjs +50 -4
- package/sdk/event-stream/writer.ts +112 -30
- package/sdk/mcp/gdd-mcp/server.js +49 -36
- package/sdk/mcp/gdd-mcp/tools/shared.ts +20 -2
- package/sdk/mcp/gdd-state/server.js +107 -41
- package/sdk/primitives/lockfile.cjs +26 -5
- package/sdk/state/index.ts +91 -17
- package/sdk/state/lockfile.ts +47 -8
- package/skills/bandit-reset/SKILL.md +2 -0
- package/skills/bandit-status/SKILL.md +4 -1
- package/skills/darkmode/SKILL.md +1 -1
- package/skills/graphify/SKILL.md +6 -6
- package/skills/quick/SKILL.md +3 -1
- package/skills/reflect/SKILL.md +1 -1
- package/skills/router/SKILL.md +4 -2
|
@@ -26,10 +26,12 @@ Fast pipeline run. Skips optional-quality agents for speed while keeping the cor
|
|
|
26
26
|
- Optional stage name (defaults to full pipeline from the current STATE.md position).
|
|
27
27
|
- `--skip <agent-name>` (repeatable) adds to the skip list.
|
|
28
28
|
2. Read `.design/STATE.md` to determine entry stage if none was passed.
|
|
29
|
-
3. For each stage to execute,
|
|
29
|
+
3. For each stage to execute, invoke the stage skill but spawn it with the optional agents in the effective skip list **omitted from the spawn graph** - this skill is the orchestrator, so it simply does not call those agents (the stage skills do not read a `quick_mode` flag; the skipping happens here, by not spawning them). The kept agents run exactly as in the full pipeline.
|
|
30
30
|
4. After each stage, print: "Stage <name> done. Skipped: <list>."
|
|
31
31
|
5. Final summary prints which agents were skipped across the full run.
|
|
32
32
|
|
|
33
|
+
Mechanism note: `{{command_prefix}}quick` is a lighter-touch *invocation* of the normal stages, not a special stage mode. It reduces ceremony by leaving the listed optional-quality agents out of the spawn graph it orchestrates. There is no flag the stage skills parse - if invoked directly (not via this skill) the stages run their full agent set.
|
|
34
|
+
|
|
33
35
|
## Use When
|
|
34
36
|
|
|
35
37
|
- You trust the problem scope (no need for fresh research).
|
|
@@ -37,7 +37,7 @@ Run `design-reflector` on demand against the current (or specified) cycle. Produ
|
|
|
37
37
|
See @skills/reflect/procedures/capability-gap-scan.md for the full procedure.
|
|
38
38
|
The `design-reflector` agent runs the scan automatically as part of its reflection pass; this step lets users dry-run it independently with:
|
|
39
39
|
```
|
|
40
|
-
node scripts/lib/reflector/capability-gap-scan.cjs --dry-run
|
|
40
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/lib/reflector/capability-gap-scan.cjs" --dry-run
|
|
41
41
|
```
|
|
42
42
|
The scan emits `capability_gap` events (`source: "reflector_pattern"`) for recurring patterns lacking a dedicated executable owner; Plan 29-03 aggregates these for `{{command_prefix}}apply-reflections`.
|
|
43
43
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gdd-router
|
|
3
|
-
description: "Routes a /gdd command to fast|quick|full path + S|M|L|XL complexity_class and returns {path, complexity_class, model_tier_overrides, resolved_models, estimated_cost_usd, cache_hits}.
|
|
3
|
+
description: "Routes a /gdd command to fast|quick|full path + S|M|L|XL complexity_class and returns {path, complexity_class, model_tier_overrides, resolved_models, estimated_cost_usd, cache_hits}. A SKILL.md prompt the model executes to emit a routing-decision JSON from rule tables (no separate agent spawn). Optional/advisory - invoked only by the skills that opt into routing; the budget-enforcer hook tolerates its absence. Read by hooks/budget-enforcer.ts."
|
|
4
4
|
argument-hint: "<intent-string> [<target-artifacts-csv>]"
|
|
5
5
|
tools: Read, Bash, Grep
|
|
6
6
|
---
|
|
@@ -69,7 +69,9 @@ Delegate to `skills/cache-manager/SKILL.md` (Plan 10.1-02). The router lists can
|
|
|
69
69
|
|
|
70
70
|
## Integration Point
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
The router is **optional and advisory**, not a universal first step. Only the handful of skills that explicitly opt into routing reference it (today: the root pipeline `SKILL.md` / `{{command_prefix}}handoff`, and `{{command_prefix}}style` documents that it deliberately does *not* invoke the router because it is a leaf invocation). The pipeline stage skills (explore / plan / design / verify) do **not** spawn the router. When a skill does invoke it, the flow is: invoke the router via `Task` or inline invocation; receive the JSON blob; pass it to downstream agents as context so the budget-enforcer hook has the router decision available in tool_input metadata when the first Agent spawn fires.
|
|
73
|
+
|
|
74
|
+
When no skill supplies a router decision, the budget-enforcer hook reads `tool_input.context.router_decision` as absent and falls back to its legacy back-compat path - the router's absence is tolerated by design, never an error.
|
|
73
75
|
|
|
74
76
|
## Failure Modes
|
|
75
77
|
|
package/sdk/cli/index.js
CHANGED
|
@@ -82,44 +82,69 @@ var init_emitter = __esm({
|
|
|
82
82
|
|
|
83
83
|
// sdk/event-stream/writer.ts
|
|
84
84
|
function _findRepoRoot() {
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
return _walkToPackageJson(process.cwd());
|
|
86
|
+
}
|
|
87
|
+
function _walkToPackageJson(startDir) {
|
|
88
|
+
let dir = startDir;
|
|
89
|
+
for (let i = 0; i < 12; i++) {
|
|
87
90
|
if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "package.json"))) return dir;
|
|
88
91
|
const parent = (0, import_node_path.dirname)(dir);
|
|
89
92
|
if (parent === dir) break;
|
|
90
93
|
dir = parent;
|
|
91
94
|
}
|
|
92
|
-
return
|
|
95
|
+
return startDir;
|
|
96
|
+
}
|
|
97
|
+
function _warnRedactUnavailable() {
|
|
98
|
+
if (_redactWarned) return;
|
|
99
|
+
_redactWarned = true;
|
|
100
|
+
try {
|
|
101
|
+
process.stderr.write(
|
|
102
|
+
"[event-stream] WARNING: scripts/lib/redact.cjs could not be loaded \u2014 failing CLOSED: event payloads are dropped (envelope-only) to avoid writing unscrubbed secrets. Run the event writer from inside the plugin tree or set the redact lib on PATH to restore full payloads.\n"
|
|
103
|
+
);
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
93
106
|
}
|
|
94
|
-
|
|
107
|
+
function _loadRedact() {
|
|
108
|
+
const candidates = [];
|
|
109
|
+
const entry = process.argv[1];
|
|
110
|
+
if (typeof entry === "string" && entry.length > 0) {
|
|
111
|
+
const entryAbs = (0, import_node_path.isAbsolute)(entry) ? entry : (0, import_node_path.resolve)(entry);
|
|
112
|
+
const entryRoot = _walkToPackageJson((0, import_node_path.dirname)(entryAbs));
|
|
113
|
+
candidates.push((0, import_node_path.resolve)(entryRoot, "scripts/lib/redact.cjs"));
|
|
114
|
+
}
|
|
115
|
+
const repoRoot = _findRepoRoot();
|
|
116
|
+
candidates.push((0, import_node_path.resolve)(repoRoot, "scripts/lib/redact.cjs"));
|
|
117
|
+
candidates.push((0, import_node_path.resolve)(repoRoot, "..", "..", "scripts/lib/redact.cjs"));
|
|
118
|
+
for (const candidate of candidates) {
|
|
119
|
+
try {
|
|
120
|
+
if (!(0, import_node_fs.existsSync)(candidate)) continue;
|
|
121
|
+
const req = (0, import_node_module.createRequire)(candidate);
|
|
122
|
+
const mod = req(candidate);
|
|
123
|
+
if (mod && typeof mod.redact === "function") return mod.redact;
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
var import_node_fs, import_node_path, import_node_module, _redactWarned, _realRedact, redact, DEFAULT_EVENTS_PATH, DEFAULT_MAX_LINE_BYTES, EventWriter;
|
|
95
130
|
var init_writer = __esm({
|
|
96
131
|
"sdk/event-stream/writer.ts"() {
|
|
97
132
|
"use strict";
|
|
98
133
|
import_node_fs = require("node:fs");
|
|
99
134
|
import_node_path = require("node:path");
|
|
100
135
|
import_node_module = require("node:module");
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const _altCandidate = (0, import_node_path.resolve)(_altRoot, "scripts/lib/redact.cjs");
|
|
111
|
-
if ((0, import_node_fs.existsSync)(_altCandidate)) {
|
|
112
|
-
const _altRequire = (0, import_node_module.createRequire)((0, import_node_path.join)(_altRoot, "package.json"));
|
|
113
|
-
const _altMod = _altRequire(_altCandidate);
|
|
114
|
-
_redact = _altMod.redact;
|
|
115
|
-
} else {
|
|
116
|
-
_redact = (v) => v;
|
|
117
|
-
}
|
|
136
|
+
_redactWarned = false;
|
|
137
|
+
_realRedact = _loadRedact();
|
|
138
|
+
redact = _realRedact !== null ? _realRedact : (v) => {
|
|
139
|
+
_warnRedactUnavailable();
|
|
140
|
+
if (v !== null && typeof v === "object") {
|
|
141
|
+
const ev = v;
|
|
142
|
+
const out = { ...ev };
|
|
143
|
+
out["payload"] = { _redaction_unavailable: true };
|
|
144
|
+
return out;
|
|
118
145
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
redact = _redact;
|
|
146
|
+
return { _redaction_unavailable: true };
|
|
147
|
+
};
|
|
123
148
|
DEFAULT_EVENTS_PATH = ".design/telemetry/events.jsonl";
|
|
124
149
|
DEFAULT_MAX_LINE_BYTES = 64 * 1024;
|
|
125
150
|
EventWriter = class {
|
|
@@ -2435,6 +2460,7 @@ function getLogger() {
|
|
|
2435
2460
|
// sdk/state/index.ts
|
|
2436
2461
|
var import_node_fs5 = require("node:fs");
|
|
2437
2462
|
var import_node_path4 = require("node:path");
|
|
2463
|
+
var import_node_module2 = require("node:module");
|
|
2438
2464
|
|
|
2439
2465
|
// sdk/state/lockfile.ts
|
|
2440
2466
|
var import_node_fs4 = require("node:fs");
|
|
@@ -2501,6 +2527,14 @@ async function acquire(path, opts = {}) {
|
|
|
2501
2527
|
}
|
|
2502
2528
|
const parsed = parseLock(existing);
|
|
2503
2529
|
if (parsed !== null && isStale(parsed, staleMs)) {
|
|
2530
|
+
const confirm = readLockSafe(lockPath);
|
|
2531
|
+
if (confirm === null) {
|
|
2532
|
+
continue;
|
|
2533
|
+
}
|
|
2534
|
+
if (confirm !== existing) {
|
|
2535
|
+
await sleep(pollMs);
|
|
2536
|
+
continue;
|
|
2537
|
+
}
|
|
2504
2538
|
try {
|
|
2505
2539
|
(0, import_node_fs4.unlinkSync)(lockPath);
|
|
2506
2540
|
} catch (delErr) {
|
|
@@ -2559,10 +2593,14 @@ function parseLock(raw) {
|
|
|
2559
2593
|
}
|
|
2560
2594
|
}
|
|
2561
2595
|
function isStale(payload, staleMs) {
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2596
|
+
const pidRecorded = typeof payload.pid === "number" && Number.isInteger(payload.pid) && payload.pid > 0;
|
|
2597
|
+
if (!pidRecorded) {
|
|
2598
|
+
const acquiredAt = Date.parse(payload.acquired_at);
|
|
2599
|
+
if (!Number.isFinite(acquiredAt)) return true;
|
|
2600
|
+
return Date.now() - acquiredAt > staleMs;
|
|
2601
|
+
}
|
|
2602
|
+
if (isPidAlive(payload.pid, payload.host)) return false;
|
|
2603
|
+
return true;
|
|
2566
2604
|
}
|
|
2567
2605
|
function isPidAlive(pid, host) {
|
|
2568
2606
|
if (host !== (0, import_node_os2.hostname)()) {
|
|
@@ -3969,6 +4007,8 @@ function gateFor(from, to) {
|
|
|
3969
4007
|
}
|
|
3970
4008
|
|
|
3971
4009
|
// sdk/state/index.ts
|
|
4010
|
+
var _moduleDir = typeof __dirname !== "undefined" ? __dirname : (0, import_node_path4.dirname)(process.argv[1] || process.cwd());
|
|
4011
|
+
var _require = typeof require !== "undefined" ? require : (0, import_node_module2.createRequire)(process.argv[1] || process.cwd());
|
|
3972
4012
|
function _findPackageRoot(startDir) {
|
|
3973
4013
|
let dir = (0, import_node_path4.resolve)(startDir);
|
|
3974
4014
|
let firstWithPkg = null;
|
|
@@ -3976,7 +4016,7 @@ function _findPackageRoot(startDir) {
|
|
|
3976
4016
|
const pkgPath = (0, import_node_path4.join)(dir, "package.json");
|
|
3977
4017
|
if ((0, import_node_fs5.existsSync)(pkgPath)) {
|
|
3978
4018
|
try {
|
|
3979
|
-
const pkg =
|
|
4019
|
+
const pkg = _require(pkgPath);
|
|
3980
4020
|
if (firstWithPkg === null) firstWithPkg = dir;
|
|
3981
4021
|
if (pkg.name === "@hegemonart/get-design-done") return dir;
|
|
3982
4022
|
} catch {
|
|
@@ -3993,7 +4033,7 @@ var _backendCache = null;
|
|
|
3993
4033
|
function _loadBackend() {
|
|
3994
4034
|
if (_backendCache !== null) return _backendCache === false ? null : _backendCache;
|
|
3995
4035
|
try {
|
|
3996
|
-
const pkgRoot = _findPackageRoot(
|
|
4036
|
+
const pkgRoot = _findPackageRoot(_moduleDir);
|
|
3997
4037
|
if (pkgRoot === null) {
|
|
3998
4038
|
_backendCache = false;
|
|
3999
4039
|
return null;
|
|
@@ -4003,7 +4043,7 @@ function _loadBackend() {
|
|
|
4003
4043
|
_backendCache = false;
|
|
4004
4044
|
return null;
|
|
4005
4045
|
}
|
|
4006
|
-
_backendCache =
|
|
4046
|
+
_backendCache = _require(backendPath);
|
|
4007
4047
|
return _backendCache;
|
|
4008
4048
|
} catch {
|
|
4009
4049
|
_backendCache = false;
|
|
@@ -4128,14 +4168,41 @@ async function transition(path, toStage) {
|
|
|
4128
4168
|
throw new TransitionGateFailed(toStage, gateResult.blockers);
|
|
4129
4169
|
}
|
|
4130
4170
|
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4171
|
+
let lockedFailure = null;
|
|
4172
|
+
let lockedBlockers = gateResult.blockers;
|
|
4173
|
+
try {
|
|
4174
|
+
const nextState = await mutate(path, (s) => {
|
|
4175
|
+
const fromNow = s.position.stage;
|
|
4176
|
+
if (!isStage(fromNow)) {
|
|
4177
|
+
lockedFailure = new TransitionGateFailed(toStage, [
|
|
4178
|
+
`Invalid transition: from="${fromNow}" is not a recognized Stage (changed under lock)`
|
|
4179
|
+
]);
|
|
4180
|
+
throw lockedFailure;
|
|
4181
|
+
}
|
|
4182
|
+
const gateNow = gateFor(fromNow, toStage);
|
|
4183
|
+
if (gateNow === null) {
|
|
4184
|
+
lockedFailure = new TransitionGateFailed(toStage, [
|
|
4185
|
+
`Invalid transition: ${fromNow} \u2192 ${toStage} (changed under lock)`
|
|
4186
|
+
]);
|
|
4187
|
+
throw lockedFailure;
|
|
4188
|
+
}
|
|
4189
|
+
const resultNow = gateNow(s);
|
|
4190
|
+
if (!resultNow.pass) {
|
|
4191
|
+
lockedFailure = new TransitionGateFailed(toStage, resultNow.blockers);
|
|
4192
|
+
throw lockedFailure;
|
|
4193
|
+
}
|
|
4194
|
+
lockedBlockers = resultNow.blockers;
|
|
4195
|
+
s.frontmatter.stage = toStage;
|
|
4196
|
+
s.frontmatter.last_checkpoint = nowIso;
|
|
4197
|
+
s.position.stage = toStage;
|
|
4198
|
+
s.timestamps[`${toStage}_started_at`] = nowIso;
|
|
4199
|
+
return s;
|
|
4200
|
+
});
|
|
4201
|
+
return { pass: true, blockers: lockedBlockers, state: nextState };
|
|
4202
|
+
} catch (err) {
|
|
4203
|
+
if (lockedFailure !== null && err === lockedFailure) throw lockedFailure;
|
|
4204
|
+
throw err;
|
|
4205
|
+
}
|
|
4139
4206
|
}
|
|
4140
4207
|
|
|
4141
4208
|
// scripts/lib/pipeline-runner/state-machine.ts
|
|
@@ -4982,7 +5049,7 @@ function collapseBlankLines(text) {
|
|
|
4982
5049
|
}
|
|
4983
5050
|
|
|
4984
5051
|
// scripts/lib/session-runner/errors.ts
|
|
4985
|
-
var
|
|
5052
|
+
var import_node_module3 = require("node:module");
|
|
4986
5053
|
var import_node_fs8 = require("node:fs");
|
|
4987
5054
|
var import_node_path7 = require("node:path");
|
|
4988
5055
|
function findRepoRoot() {
|
|
@@ -4996,7 +5063,7 @@ function findRepoRoot() {
|
|
|
4996
5063
|
return process.cwd();
|
|
4997
5064
|
}
|
|
4998
5065
|
var REPO_ROOT = findRepoRoot();
|
|
4999
|
-
var nodeRequire = (0,
|
|
5066
|
+
var nodeRequire = (0, import_node_module3.createRequire)((0, import_node_path7.join)(REPO_ROOT, "package.json"));
|
|
5000
5067
|
var transportClassifier = nodeRequire(
|
|
5001
5068
|
(0, import_node_path7.resolve)(REPO_ROOT, "sdk/primitives/error-classifier.cjs")
|
|
5002
5069
|
);
|
|
@@ -5348,7 +5415,7 @@ var TranscriptWriter = class {
|
|
|
5348
5415
|
};
|
|
5349
5416
|
|
|
5350
5417
|
// scripts/lib/session-runner/index.ts
|
|
5351
|
-
var
|
|
5418
|
+
var import_node_module4 = require("node:module");
|
|
5352
5419
|
var import_node_fs10 = require("node:fs");
|
|
5353
5420
|
var import_node_path9 = require("node:path");
|
|
5354
5421
|
function _findRepoRoot2() {
|
|
@@ -5362,7 +5429,7 @@ function _findRepoRoot2() {
|
|
|
5362
5429
|
return process.cwd();
|
|
5363
5430
|
}
|
|
5364
5431
|
var _REPO_ROOT = _findRepoRoot2();
|
|
5365
|
-
var _nodeRequire = (0,
|
|
5432
|
+
var _nodeRequire = (0, import_node_module4.createRequire)((0, import_node_path9.join)(_REPO_ROOT, "package.json"));
|
|
5366
5433
|
var jitteredBackoff = _nodeRequire(
|
|
5367
5434
|
(0, import_node_path9.resolve)(_REPO_ROOT, "sdk/primitives/jittered-backoff.cjs")
|
|
5368
5435
|
);
|
|
@@ -9731,7 +9798,7 @@ ${BUILD_USAGE}`);
|
|
|
9731
9798
|
|
|
9732
9799
|
// sdk/cli/commands/dashboard.ts
|
|
9733
9800
|
var import_node_child_process2 = require("node:child_process");
|
|
9734
|
-
var
|
|
9801
|
+
var import_node_module5 = require("node:module");
|
|
9735
9802
|
var import_node_http = require("node:http");
|
|
9736
9803
|
var import_node_fs23 = require("node:fs");
|
|
9737
9804
|
var import_node_path22 = require("node:path");
|
|
@@ -9767,7 +9834,7 @@ function anchorDirs() {
|
|
|
9767
9834
|
return out;
|
|
9768
9835
|
}
|
|
9769
9836
|
function climbToMarker(startDir) {
|
|
9770
|
-
const req = (0,
|
|
9837
|
+
const req = (0, import_node_module5.createRequire)((0, import_node_path22.join)(startDir, "noop.js"));
|
|
9771
9838
|
let dir = startDir;
|
|
9772
9839
|
let firstWithPkg = null;
|
|
9773
9840
|
for (let i = 0; i < 12; i++) {
|
|
@@ -9803,7 +9870,7 @@ function findPackageRoot() {
|
|
|
9803
9870
|
}
|
|
9804
9871
|
function requireFromRoot(relPath) {
|
|
9805
9872
|
const root = findPackageRoot();
|
|
9806
|
-
const req = (0,
|
|
9873
|
+
const req = (0, import_node_module5.createRequire)((0, import_node_path22.join)(root, "noop.js"));
|
|
9807
9874
|
return req((0, import_node_path22.join)(root, relPath));
|
|
9808
9875
|
}
|
|
9809
9876
|
function resolveRoot(deps, flags) {
|
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
* `degraded[]` (so gsd-health + the TUI can surface what is missing).
|
|
25
25
|
* - Absent .design entirely -> every data section null/[] + degraded
|
|
26
26
|
* populated, still no throw.
|
|
27
|
-
* - Root resolution: opts.root || GDD_PROJECT_ROOT ||
|
|
28
|
-
* ||
|
|
29
|
-
*
|
|
27
|
+
* - Root resolution: opts.root || GDD_PROJECT_ROOT || cwd marker walk-up
|
|
28
|
+
* (.design/.planning/.claude-plugin) || package-root walk-up. The cwd
|
|
29
|
+
* marker walk resolves the USER's project; package-root is a last-resort
|
|
30
|
+
* fallback (it would otherwise always pin the installed plugin's own dir).
|
|
30
31
|
* - The .ts libs (sdk/state, sdk/event-stream) cannot be static-require()d
|
|
31
32
|
* from a .cjs — they are loaded via dynamic import(pathToFileURL),
|
|
32
33
|
* memoized once per process. The .cjs libs are require()d directly via the
|
|
@@ -100,15 +101,59 @@ function importEventStream() {
|
|
|
100
101
|
// ---------------------------------------------------------------------------
|
|
101
102
|
// Root resolution
|
|
102
103
|
// ---------------------------------------------------------------------------
|
|
104
|
+
/**
|
|
105
|
+
* Walk UP from `startCwd` looking for a GDD project marker — `.design/` OR
|
|
106
|
+
* `.planning/` OR `.claude-plugin/plugin.json`. First match wins; returns the
|
|
107
|
+
* directory that holds the marker, or null if none is found before the
|
|
108
|
+
* filesystem root.
|
|
109
|
+
*
|
|
110
|
+
* This mirrors `resolveProjectRoot()` in
|
|
111
|
+
* `sdk/mcp/gdd-mcp/tools/shared.ts` (the canonical D-05 marker walk) so the
|
|
112
|
+
* dashboard resolves the USER's project, not the plugin's own install dir.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} [startCwd]
|
|
115
|
+
* @returns {string|null}
|
|
116
|
+
*/
|
|
117
|
+
function cwdMarkerWalk(startCwd = process.cwd()) {
|
|
118
|
+
let dir = path.resolve(startCwd);
|
|
119
|
+
// Bound the climb defensively (deep trees / odd mounts).
|
|
120
|
+
for (let i = 0; i < 64; i++) {
|
|
121
|
+
if (
|
|
122
|
+
fs.existsSync(path.join(dir, '.design')) ||
|
|
123
|
+
fs.existsSync(path.join(dir, '.planning')) ||
|
|
124
|
+
fs.existsSync(path.join(dir, '.claude-plugin', 'plugin.json'))
|
|
125
|
+
) {
|
|
126
|
+
return dir;
|
|
127
|
+
}
|
|
128
|
+
const parent = path.dirname(dir);
|
|
129
|
+
if (parent === dir) return null; // reached filesystem root
|
|
130
|
+
dir = parent;
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
103
135
|
/**
|
|
104
136
|
* Resolve the project root the dashboard reads from:
|
|
105
|
-
* opts.root || GDD_PROJECT_ROOT (env) ||
|
|
137
|
+
* opts.root || GDD_PROJECT_ROOT (env) || cwd marker walk-up || package-root.
|
|
138
|
+
*
|
|
139
|
+
* D2 fix: `packageRoot()` ALWAYS succeeds (it walks up from __dirname and
|
|
140
|
+
* lands on the installed plugin's own package.json, e.g.
|
|
141
|
+
* node_modules/@hegemonart/get-design-done), so when it preceded the cwd
|
|
142
|
+
* resolution an installed `gdd-dashboard` always showed the PLUGIN's own
|
|
143
|
+
* (empty) data instead of the user's project. We now resolve the user's
|
|
144
|
+
* project FIRST via a cwd-upward marker walk (the same D-05 algorithm the
|
|
145
|
+
* gdd-mcp tools use), and only fall back to the package root as a last
|
|
146
|
+
* resort. Running the dashboard INSIDE the gdd repo still works — the marker
|
|
147
|
+
* walk finds the repo's own .design/.planning, which IS the project root.
|
|
148
|
+
*
|
|
106
149
|
* @param {{root?: string}} [opts]
|
|
107
150
|
* @returns {string}
|
|
108
151
|
*/
|
|
109
152
|
function resolveRoot(opts = {}) {
|
|
110
153
|
if (opts.root) return path.resolve(opts.root);
|
|
111
154
|
if (process.env.GDD_PROJECT_ROOT) return path.resolve(process.env.GDD_PROJECT_ROOT);
|
|
155
|
+
const fromCwd = cwdMarkerWalk();
|
|
156
|
+
if (fromCwd) return fromCwd;
|
|
112
157
|
try {
|
|
113
158
|
return packageRoot();
|
|
114
159
|
} catch {
|
|
@@ -610,6 +655,7 @@ module.exports = {
|
|
|
610
655
|
loadDashboardModel,
|
|
611
656
|
// Exposed for tests + sibling reuse (executors D/F may want the scrapers).
|
|
612
657
|
resolveRoot,
|
|
658
|
+
cwdMarkerWalk,
|
|
613
659
|
scrapeStateFile,
|
|
614
660
|
scrapeEventsFile,
|
|
615
661
|
};
|
|
@@ -38,46 +38,128 @@ import type { BaseEvent } from './types.ts';
|
|
|
38
38
|
// anchor createRequire on the repo-root package.json discovered by
|
|
39
39
|
// walking up from `process.cwd()`.
|
|
40
40
|
function _findRepoRoot(): string {
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
return _walkToPackageJson(process.cwd());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Walk up from `startDir` until a directory containing `package.json` is
|
|
46
|
+
* found; returns `startDir` itself if none is found within the bound.
|
|
47
|
+
*/
|
|
48
|
+
function _walkToPackageJson(startDir: string): string {
|
|
49
|
+
let dir = startDir;
|
|
50
|
+
for (let i = 0; i < 12; i++) {
|
|
43
51
|
if (existsSync(join(dir, 'package.json'))) return dir;
|
|
44
52
|
const parent = dirname(dir);
|
|
45
53
|
if (parent === dir) break;
|
|
46
54
|
dir = parent;
|
|
47
55
|
}
|
|
48
|
-
return
|
|
56
|
+
return startDir;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// S2 fix: redaction now fails CLOSED. Previously, if redact.cjs could not be
|
|
60
|
+
// resolved from the runtime cwd, `_redact` fell through to the IDENTITY
|
|
61
|
+
// function and every event was written UNSCRUBBED — silently leaking secrets
|
|
62
|
+
// into events.jsonl whenever the writer ran outside the plugin tree (hook
|
|
63
|
+
// subprocesses, temp test dirs, unusual install layouts). That is a fail-open
|
|
64
|
+
// security hole.
|
|
65
|
+
//
|
|
66
|
+
// New contract:
|
|
67
|
+
// * redact.cjs loads → normal deep-walk scrubbing (unchanged behavior).
|
|
68
|
+
// * redact.cjs MISSING → fail closed: `redact` returns an envelope-only
|
|
69
|
+
// placeholder that DROPS the payload body (replacing it with
|
|
70
|
+
// `{ _redaction_unavailable: true }`) so no raw payload is ever persisted
|
|
71
|
+
// unscrubbed. A single visible stderr warning is emitted (once per
|
|
72
|
+
// process, guarded by `_redactWarned`) so the failure is observable.
|
|
73
|
+
//
|
|
74
|
+
// Resolution is also improved: we try createRequire anchored on THIS module
|
|
75
|
+
// (via the runtime-resolved module path) before the cwd-anchored walk, so it
|
|
76
|
+
// loads in more layouts.
|
|
77
|
+
|
|
78
|
+
/** Module-level guard so the fail-closed warning prints at most once. */
|
|
79
|
+
let _redactWarned = false;
|
|
80
|
+
|
|
81
|
+
/** Emit the one-time fail-closed warning to stderr (guarded, best-effort). */
|
|
82
|
+
function _warnRedactUnavailable(): void {
|
|
83
|
+
if (_redactWarned) return;
|
|
84
|
+
_redactWarned = true;
|
|
85
|
+
try {
|
|
86
|
+
process.stderr.write(
|
|
87
|
+
'[event-stream] WARNING: scripts/lib/redact.cjs could not be loaded — ' +
|
|
88
|
+
'failing CLOSED: event payloads are dropped (envelope-only) to avoid ' +
|
|
89
|
+
'writing unscrubbed secrets. Run the event writer from inside the ' +
|
|
90
|
+
'plugin tree or set the redact lib on PATH to restore full payloads.\n',
|
|
91
|
+
);
|
|
92
|
+
} catch {
|
|
93
|
+
// If stderr itself is broken we have no recourse; swallow.
|
|
94
|
+
}
|
|
49
95
|
}
|
|
50
96
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Attempt to load redact.cjs from a set of candidate roots. Returns the
|
|
99
|
+
* real `redact` function on success, or `null` if no candidate resolves.
|
|
100
|
+
*/
|
|
101
|
+
function _loadRedact(): ((v: unknown) => unknown) | null {
|
|
102
|
+
const candidates: string[] = [];
|
|
103
|
+
|
|
104
|
+
// We cannot use `import.meta.url` (tsc Node16 classifies this .ts as CJS for
|
|
105
|
+
// typecheck), so we probe several anchors so redact.cjs loads in as many
|
|
106
|
+
// layouts as possible BEFORE the fail-closed path engages:
|
|
107
|
+
//
|
|
108
|
+
// 1) The entry script (`process.argv[1]`). For hook subprocesses (e.g.
|
|
109
|
+
// budget-enforcer.ts spawned by the harness) the entry script lives
|
|
110
|
+
// INSIDE the plugin tree even when cwd is a detached temp dir — this is
|
|
111
|
+
// the same anchor the hook itself uses via resolveHookPath(). Walking up
|
|
112
|
+
// from the entry script to its package.json reliably lands on the plugin
|
|
113
|
+
// root regardless of cwd.
|
|
114
|
+
// 2) A cwd-walked repo root (works when cwd IS inside the plugin tree).
|
|
115
|
+
// 3) The source-relative layout (writer.ts → ../../scripts/lib/redact.cjs).
|
|
116
|
+
const entry = process.argv[1];
|
|
117
|
+
if (typeof entry === 'string' && entry.length > 0) {
|
|
118
|
+
const entryAbs = isAbsolute(entry) ? entry : resolve(entry);
|
|
119
|
+
const entryRoot = _walkToPackageJson(dirname(entryAbs));
|
|
120
|
+
candidates.push(resolve(entryRoot, 'scripts/lib/redact.cjs'));
|
|
121
|
+
}
|
|
122
|
+
const repoRoot = _findRepoRoot();
|
|
123
|
+
candidates.push(resolve(repoRoot, 'scripts/lib/redact.cjs'));
|
|
124
|
+
candidates.push(resolve(repoRoot, '..', '..', 'scripts/lib/redact.cjs'));
|
|
125
|
+
|
|
126
|
+
for (const candidate of candidates) {
|
|
127
|
+
try {
|
|
128
|
+
if (!existsSync(candidate)) continue;
|
|
129
|
+
// Anchor createRequire on the candidate file itself so resolution does
|
|
130
|
+
// not depend on a package.json being present at a particular ancestor.
|
|
131
|
+
const req = createRequire(candidate);
|
|
132
|
+
const mod = req(candidate) as { redact?: (v: unknown) => unknown };
|
|
133
|
+
if (mod && typeof mod.redact === 'function') return mod.redact;
|
|
134
|
+
} catch {
|
|
135
|
+
// Try the next candidate.
|
|
75
136
|
}
|
|
76
137
|
}
|
|
77
|
-
|
|
78
|
-
_redact = (v) => v;
|
|
138
|
+
return null;
|
|
79
139
|
}
|
|
80
|
-
|
|
140
|
+
|
|
141
|
+
const _realRedact = _loadRedact();
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The redaction function used at the write boundary. When the real redactor
|
|
145
|
+
* loaded, this is it. When it did not, this is the FAIL-CLOSED shim: it warns
|
|
146
|
+
* once and returns an envelope-only object with the payload body dropped.
|
|
147
|
+
*/
|
|
148
|
+
const redact: (v: unknown) => unknown =
|
|
149
|
+
_realRedact !== null
|
|
150
|
+
? _realRedact
|
|
151
|
+
: (v: unknown): unknown => {
|
|
152
|
+
_warnRedactUnavailable();
|
|
153
|
+
if (v !== null && typeof v === 'object') {
|
|
154
|
+
// Preserve envelope metadata; drop the payload body entirely so no
|
|
155
|
+
// raw (potentially secret-bearing) content is persisted.
|
|
156
|
+
const ev = v as Record<string, unknown>;
|
|
157
|
+
const out: Record<string, unknown> = { ...ev };
|
|
158
|
+
out['payload'] = { _redaction_unavailable: true };
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
return { _redaction_unavailable: true };
|
|
162
|
+
};
|
|
81
163
|
|
|
82
164
|
/** Default relative path for the persisted event stream. */
|
|
83
165
|
export const DEFAULT_EVENTS_PATH = '.design/telemetry/events.jsonl';
|