@opengsd/gsd-core 1.4.2 → 1.5.0-rc.1
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/plugin.json +1 -1
- package/README.md +3 -3
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-plan-checker.md +11 -2
- package/agents/gsd-planner.md +11 -7
- package/agents/gsd-verifier.md +3 -3
- package/bin/install.js +947 -149
- package/commands/gsd/autonomous.md +5 -1
- package/commands/gsd/ns-manage.md +8 -1
- package/commands/gsd/ns-project.md +5 -0
- package/commands/gsd/ns-review.md +4 -1
- package/commands/gsd/ns-workflow.md +7 -1
- package/commands/gsd/plan-review-convergence.md +6 -5
- package/commands/gsd/surface.md +12 -5
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +198 -101
- package/gsd-core/bin/lib/audit-command-router.cjs +61 -0
- package/gsd-core/bin/lib/capability-registry.cjs +417 -0
- package/gsd-core/bin/lib/capability-state.cjs +336 -0
- package/gsd-core/bin/lib/cli-exit.cjs +22 -3
- package/gsd-core/bin/lib/config-loader.cjs +716 -0
- package/gsd-core/bin/lib/configuration.cjs +4 -34
- package/gsd-core/bin/lib/core-utils.cjs +198 -0
- package/gsd-core/bin/lib/core.cjs +57 -1817
- package/gsd-core/bin/lib/federated-config.cjs +182 -0
- package/gsd-core/bin/lib/graphify-command-router.cjs +74 -0
- package/gsd-core/bin/lib/init.cjs +58 -12
- package/gsd-core/bin/lib/install-profiles.cjs +157 -3
- package/gsd-core/bin/lib/installer-migrations/004-prune-stale-pristine-snapshots.cjs +123 -0
- package/gsd-core/bin/lib/intel-command-router.cjs +116 -0
- package/gsd-core/bin/lib/io.cjs +167 -0
- package/gsd-core/bin/lib/loop-host-contract.cjs +105 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +460 -0
- package/gsd-core/bin/lib/model-resolver.cjs +426 -0
- package/gsd-core/bin/lib/phase-id.cjs +215 -0
- package/gsd-core/bin/lib/phase-locator.cjs +148 -0
- package/gsd-core/bin/lib/profile-pipeline.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +443 -0
- package/gsd-core/bin/lib/roadmap.cjs +4 -1
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +96 -9
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +2 -1
- package/gsd-core/bin/lib/runtime-homes.cjs +40 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +1 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +13 -0
- package/gsd-core/bin/lib/state.cjs +114 -2
- package/gsd-core/bin/lib/surface.cjs +66 -14
- package/gsd-core/bin/lib/verify.cjs +1 -1
- package/gsd-core/bin/shared/model-catalog.json +5 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +3 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +57 -7
- package/gsd-core/references/planner-reviews.md +5 -2
- package/gsd-core/templates/phase-prompt.md +7 -7
- package/gsd-core/templates/project.md +19 -2
- package/gsd-core/templates/summary-complex.md +1 -0
- package/gsd-core/templates/summary-minimal.md +1 -0
- package/gsd-core/templates/summary-standard.md +1 -0
- package/gsd-core/templates/summary.md +1 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous.md +86 -15
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +6 -4
- package/gsd-core/workflows/code-review.md +3 -2
- package/gsd-core/workflows/complete-milestone.md +11 -5
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +8 -1
- package/gsd-core/workflows/do.md +1 -1
- package/gsd-core/workflows/docs-update.md +1 -1
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
- package/gsd-core/workflows/execute-phase.md +8 -1
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/explore.md +1 -1
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +1 -1
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +1 -1
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +1 -1
- package/gsd-core/workflows/new-project.md +1 -1
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
- package/gsd-core/workflows/plan-phase.md +27 -2
- package/gsd-core/workflows/plan-review-convergence.md +76 -63
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +2 -2
- package/gsd-core/workflows/reapply-patches.md +18 -1
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +1 -1
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +1 -1
- package/gsd-core/workflows/settings-integrations.md +1 -1
- package/gsd-core/workflows/settings.md +1 -1
- package/gsd-core/workflows/ship.md +8 -1
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +1 -1
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +1 -1
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/update.md +20 -15
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +8 -1
- package/package.json +7 -2
- package/scripts/base64-scan.sh +1 -1
- package/scripts/changeset/cli.cjs +9 -2
- package/scripts/changeset/lint.cjs +38 -2
- package/scripts/ci-test-scope.cjs +21 -10
- package/scripts/gen-capability-registry.cjs +1808 -0
- package/scripts/gen-loop-host-contract.cjs +471 -0
- package/scripts/lint-regression-test-names.allowlist.json +269 -0
- package/scripts/lint-regression-test-names.cjs +117 -0
- package/scripts/lint-test-file-count.allowlist.json +12 -4
- package/scripts/prompt-injection-scan.sh +4 -4
- package/scripts/run-tests.cjs +79 -16
- package/scripts/secret-scan.sh +3 -3
- package/scripts/run-cross-platform-tests.cjs +0 -67
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Federated Config — Defensive merge of capability-declared config keys
|
|
4
|
+
*
|
|
5
|
+
* ADR-857 phase 3b: wires the Capability Registry's configSchema into
|
|
6
|
+
* loadConfig as a provably-empty no-op channel until capability keys are
|
|
7
|
+
* migrated out of the central config-schema.
|
|
8
|
+
*
|
|
9
|
+
* Exported function:
|
|
10
|
+
* mergeFederatedConfig({ configSchema, isCentralKey, userConfig })
|
|
11
|
+
* → { values, validKeys, warnings }
|
|
12
|
+
*
|
|
13
|
+
* Design:
|
|
14
|
+
* - For each key in configSchema:
|
|
15
|
+
* If isCentralKey(key) → SKIP; push a pending-migration warning.
|
|
16
|
+
* Else if slice is malformed → SKIP; push a warning. Never throw.
|
|
17
|
+
* Else (valid federated key absent from central):
|
|
18
|
+
* resolvedValue = nested userConfig lookup if present & type-matches; else slice.default.
|
|
19
|
+
* Add key→resolvedValue to values; add key to validKeys.
|
|
20
|
+
* - Guard all object writes with inline literal __proto__/constructor/prototype checks.
|
|
21
|
+
* - Zero external dependencies; no ajv; hand-rolled type checks only.
|
|
22
|
+
*
|
|
23
|
+
* ADR-857 no-op guarantee:
|
|
24
|
+
* With the current registry, every UI key is still present in the central
|
|
25
|
+
* config-schema, so isCentralKey() returns true for all of them and values
|
|
26
|
+
* is always empty. The channel is live but carries no traffic until a key
|
|
27
|
+
* is atomically removed from the central schema (the cutover step).
|
|
28
|
+
*
|
|
29
|
+
* Dependencies: none (zero-dep module).
|
|
30
|
+
*/
|
|
31
|
+
// ─── Allowed slice types (mirrors gen-capability-registry.cjs VALID_CONFIG_SLICE_TYPES) ──
|
|
32
|
+
const VALID_SLICE_TYPES = new Set(['boolean', 'string', 'number', 'enum']);
|
|
33
|
+
// ─── Internal helpers ──────────────────────────────────────────────────────────
|
|
34
|
+
/**
|
|
35
|
+
* Returns true if `slice` has a non-empty type, a `default` property, and a
|
|
36
|
+
* non-empty string description. Does NOT throw.
|
|
37
|
+
*/
|
|
38
|
+
function _isWellFormedSlice(slice) {
|
|
39
|
+
if (typeof slice !== 'object' || slice === null || Array.isArray(slice))
|
|
40
|
+
return false;
|
|
41
|
+
const s = slice;
|
|
42
|
+
if (typeof s['type'] !== 'string' || s['type'].length === 0)
|
|
43
|
+
return false;
|
|
44
|
+
if (!VALID_SLICE_TYPES.has(s['type']))
|
|
45
|
+
return false;
|
|
46
|
+
if (!Object.prototype.hasOwnProperty.call(s, 'default'))
|
|
47
|
+
return false;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Returns true if `value` matches the declared type in the slice.
|
|
52
|
+
* For enum, also validates against slice.values if present.
|
|
53
|
+
*/
|
|
54
|
+
function _typeMatches(value, slice) {
|
|
55
|
+
switch (slice.type) {
|
|
56
|
+
case 'boolean': return typeof value === 'boolean';
|
|
57
|
+
case 'string': return typeof value === 'string';
|
|
58
|
+
case 'number': return typeof value === 'number';
|
|
59
|
+
case 'enum':
|
|
60
|
+
// Must be a string AND, if values list is present, must be in it
|
|
61
|
+
if (typeof value !== 'string')
|
|
62
|
+
return false;
|
|
63
|
+
if (Array.isArray(slice.values) && slice.values.length > 0) {
|
|
64
|
+
return slice.values.includes(value);
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
default: return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Traverse a dotted key path through a nested config object.
|
|
72
|
+
* E.g. key="workflow.ui_phase", obj={workflow:{ui_phase:false}} → {found:true, value:false}
|
|
73
|
+
* Returns {found:false} if any segment is missing or not an own property.
|
|
74
|
+
* Handles 1, 2, or N segments generically.
|
|
75
|
+
*/
|
|
76
|
+
function _getNestedValue(obj, key) {
|
|
77
|
+
const segments = key.split('.');
|
|
78
|
+
let current = obj;
|
|
79
|
+
for (let i = 0; i < segments.length; i++) {
|
|
80
|
+
const seg = segments[i];
|
|
81
|
+
// Inline literal prototype-pollution guard
|
|
82
|
+
if (seg === '__proto__' || seg === 'constructor' || seg === 'prototype') {
|
|
83
|
+
return { found: false, value: undefined };
|
|
84
|
+
}
|
|
85
|
+
if (typeof current !== 'object' || current === null) {
|
|
86
|
+
return { found: false, value: undefined };
|
|
87
|
+
}
|
|
88
|
+
const cur = current;
|
|
89
|
+
if (!Object.prototype.hasOwnProperty.call(cur, seg)) {
|
|
90
|
+
return { found: false, value: undefined };
|
|
91
|
+
}
|
|
92
|
+
current = cur[seg];
|
|
93
|
+
}
|
|
94
|
+
return { found: true, value: current };
|
|
95
|
+
}
|
|
96
|
+
// ─── Public API ────────────────────────────────────────────────────────────────
|
|
97
|
+
/**
|
|
98
|
+
* Defensive merge of capability-declared config slices into the loadConfig
|
|
99
|
+
* return value.
|
|
100
|
+
*
|
|
101
|
+
* DEFENSIVE contract (never throws, even on bad capability data):
|
|
102
|
+
* - Null/undefined/non-object input → returns empty result.
|
|
103
|
+
* - Null/undefined/non-object userConfig → treated as {} (no overrides).
|
|
104
|
+
* - Central keys are skipped with a pending-migration warning.
|
|
105
|
+
* - Malformed slices are skipped with a warning.
|
|
106
|
+
* - User-supplied values with wrong types (or out-of-enum values) fall back
|
|
107
|
+
* to the slice default (a type-mismatch warning is pushed but the key is
|
|
108
|
+
* still federated with its default; this is best-effort degraded operation).
|
|
109
|
+
*/
|
|
110
|
+
function mergeFederatedConfig(input) {
|
|
111
|
+
// FIX 4: Guard null/undefined/non-object input
|
|
112
|
+
if (input === null || input === undefined || typeof input !== 'object') {
|
|
113
|
+
return { values: Object.create(null), validKeys: [], warnings: [] };
|
|
114
|
+
}
|
|
115
|
+
const { configSchema, isCentralKey } = input;
|
|
116
|
+
// FIX 4: Guard null/undefined/non-object userConfig — treat as {}
|
|
117
|
+
const userConfig = (input.userConfig !== null && input.userConfig !== undefined && typeof input.userConfig === 'object' && !Array.isArray(input.userConfig))
|
|
118
|
+
? input.userConfig
|
|
119
|
+
: {};
|
|
120
|
+
// FIX 6b: Use null-prototype object for all return paths
|
|
121
|
+
const values = Object.create(null);
|
|
122
|
+
const validKeys = [];
|
|
123
|
+
const warnings = [];
|
|
124
|
+
if (typeof configSchema !== 'object' || configSchema === null) {
|
|
125
|
+
return { values: Object.create(null), validKeys: [], warnings: [] };
|
|
126
|
+
}
|
|
127
|
+
for (const key of Object.keys(configSchema)) {
|
|
128
|
+
// S2: inline literal prototype-pollution guard (CodeQL barrier)
|
|
129
|
+
// Guard both the full key AND all dotted-path segments
|
|
130
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype')
|
|
131
|
+
continue;
|
|
132
|
+
const _keySegments = key.split('.');
|
|
133
|
+
if (_keySegments.some((s) => s === '__proto__' || s === 'constructor' || s === 'prototype'))
|
|
134
|
+
continue;
|
|
135
|
+
const slice = configSchema[key];
|
|
136
|
+
// If this key is still in the central schema → pending migration, skip
|
|
137
|
+
try {
|
|
138
|
+
if (isCentralKey(key)) {
|
|
139
|
+
warnings.push('federated-config: key "' + key + '" is still in the central config-schema (pending-migration); ' +
|
|
140
|
+
'skipping federated resolution until the central schema entry is removed');
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// isCentralKey threw — treat as unknown, skip defensively
|
|
146
|
+
warnings.push('federated-config: isCentralKey("' + key + '") threw; skipping key');
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
// Validate slice shape — skip malformed entries
|
|
150
|
+
if (!_isWellFormedSlice(slice)) {
|
|
151
|
+
warnings.push('federated-config: config slice for key "' + key + '" is malformed (missing or invalid type/default); skipping');
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const sliceEntry = slice;
|
|
155
|
+
// FIX 1: Resolve value using NESTED dotted-path lookup through userConfig
|
|
156
|
+
let resolvedValue = sliceEntry.default;
|
|
157
|
+
const { found: userHasKey, value: userValue } = _getNestedValue(userConfig, key);
|
|
158
|
+
if (userHasKey && userValue !== undefined) {
|
|
159
|
+
// FIX 5b: For enum, validate against slice.values if present; otherwise check type
|
|
160
|
+
if (_typeMatches(userValue, sliceEntry)) {
|
|
161
|
+
resolvedValue = userValue;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
const typeDesc = sliceEntry.type === 'enum' && Array.isArray(sliceEntry.values)
|
|
165
|
+
? 'enum(' + sliceEntry.values.join('|') + ')'
|
|
166
|
+
: sliceEntry.type;
|
|
167
|
+
warnings.push('federated-config: user-supplied value for "' + key + '" has wrong type or invalid enum value ' +
|
|
168
|
+
'(expected ' + typeDesc + ', got ' + typeof userValue +
|
|
169
|
+
(typeof userValue === 'string' ? ' "' + String(userValue) + '"' : '') +
|
|
170
|
+
'); falling back to slice default');
|
|
171
|
+
// resolvedValue stays as slice default
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// S2: inline literal guard before writing to values
|
|
175
|
+
if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
|
|
176
|
+
values[key] = resolvedValue;
|
|
177
|
+
validKeys.push(key);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { values, validKeys, warnings };
|
|
181
|
+
}
|
|
182
|
+
module.exports = { mergeFederatedConfig };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Graphify command router — CLI subcommand dispatcher for `gsd-tools graphify`.
|
|
4
|
+
*
|
|
5
|
+
* ADR-959 (phase 4d-impl-2) pilot: first real capability command cutover.
|
|
6
|
+
* Extracted from the hardcoded `case 'graphify':` arm in gsd-tools.cjs.
|
|
7
|
+
* Behaviour is preserved byte-for-behaviour from the prior inline case;
|
|
8
|
+
* the dispatch path now flows: default → dispatchCapabilityCommand →
|
|
9
|
+
* require(graphify-command-router.cjs) → routeGraphifyCommand.
|
|
10
|
+
*
|
|
11
|
+
* Router signature: { args, cwd, raw, error } — identical to the 12 existing
|
|
12
|
+
* host routers. No new handler/arg convention; the capability registry
|
|
13
|
+
* discovers this router by name.
|
|
14
|
+
*
|
|
15
|
+
* Arg indexing (preserved exactly from the original case):
|
|
16
|
+
* args[0] = 'graphify' (family — matched by dispatchCapabilityCommand)
|
|
17
|
+
* args[1] = subcommand (query | status | diff | build)
|
|
18
|
+
* args[2] = term (query) | 'snapshot' (build snapshot)
|
|
19
|
+
* args.indexOf('--budget') + 1 = budget value
|
|
20
|
+
*
|
|
21
|
+
* Test seam: pass `_graphify` in the options object to inject a recording mock
|
|
22
|
+
* instead of the real graphify module. The `_`-prefix follows the repo's
|
|
23
|
+
* established seam convention (see other routers). Production callers omit it.
|
|
24
|
+
*/
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
26
|
+
const graphify = require("./graphify.cjs");
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
|
+
const core = require("./core.cjs");
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
30
|
+
const io = require("./io.cjs");
|
|
31
|
+
const { ERROR_REASON } = io;
|
|
32
|
+
// ─── Implementation ───────────────────────────────────────────────────────────
|
|
33
|
+
function routeGraphifyCommand({ args, cwd, raw, error, _graphify }) {
|
|
34
|
+
const subcommand = args[1];
|
|
35
|
+
const g = _graphify ?? graphify;
|
|
36
|
+
if (subcommand === 'query') {
|
|
37
|
+
const term = args[2];
|
|
38
|
+
if (!term) {
|
|
39
|
+
error('Usage: gsd-tools graphify query <term>', ERROR_REASON.USAGE);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const budgetIdx = args.indexOf('--budget');
|
|
43
|
+
let budget = null;
|
|
44
|
+
if (budgetIdx !== -1) {
|
|
45
|
+
const rawBudget = args[budgetIdx + 1];
|
|
46
|
+
if (rawBudget === undefined || Number.isNaN(parseInt(rawBudget, 10))) {
|
|
47
|
+
error('Usage: gsd-tools graphify query <term> [--budget <N>]', ERROR_REASON.USAGE);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
budget = parseInt(rawBudget, 10);
|
|
51
|
+
}
|
|
52
|
+
core.output(g.graphifyQuery(cwd, term, { budget }), raw);
|
|
53
|
+
}
|
|
54
|
+
else if (subcommand === 'status') {
|
|
55
|
+
core.output(g.graphifyStatus(cwd), raw);
|
|
56
|
+
}
|
|
57
|
+
else if (subcommand === 'diff') {
|
|
58
|
+
core.output(g.graphifyDiff(cwd), raw);
|
|
59
|
+
}
|
|
60
|
+
else if (subcommand === 'build') {
|
|
61
|
+
if (args[2] === 'snapshot') {
|
|
62
|
+
core.output(g.writeSnapshot(cwd), raw);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
core.output(g.graphifyBuild(cwd), raw);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
error('Unknown graphify subcommand. Available: build, query, status, diff', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
module.exports = {
|
|
73
|
+
routeGraphifyCommand,
|
|
74
|
+
};
|
|
@@ -1718,16 +1718,22 @@ function buildSkillManifest(cwd, skillsDir = null) {
|
|
|
1718
1718
|
roots.push(rootSummary);
|
|
1719
1719
|
continue;
|
|
1720
1720
|
}
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1721
|
+
// Track skill names seen within this root to deduplicate dual-routed concretes
|
|
1722
|
+
// (e.g. spec-phase nested under both gsd-ns-workflow and gsd-ns-manage).
|
|
1723
|
+
const seenNamesInRoot = new Set();
|
|
1724
|
+
function pushSkillEntry(
|
|
1725
|
+
// relPath must use forward slashes on all platforms (manifest paths are
|
|
1726
|
+
// posix-style for cross-platform stability; flat entries use template
|
|
1727
|
+
// literals that always produce '/'; nested entries are joined below
|
|
1728
|
+
// with explicit '/' separators rather than path.join).
|
|
1729
|
+
relPath, content) {
|
|
1729
1730
|
const frontmatter = extractFrontmatter(content);
|
|
1730
|
-
const
|
|
1731
|
+
const dirPart = relPath.replace(/\/SKILL\.md$/, '');
|
|
1732
|
+
const stem = dirPart.includes('/') ? dirPart.split('/').pop() : dirPart;
|
|
1733
|
+
const name = frontmatter['name'] || stem;
|
|
1734
|
+
if (seenNamesInRoot.has(name))
|
|
1735
|
+
return false; // dedupe dual-routed concretes
|
|
1736
|
+
seenNamesInRoot.add(name);
|
|
1731
1737
|
const description = frontmatter['description'] || '';
|
|
1732
1738
|
const triggers = [];
|
|
1733
1739
|
const bodyMatch = content.match(/^---[\s\S]*?---\s*\n([\s\S]*)$/);
|
|
@@ -1746,14 +1752,54 @@ function buildSkillManifest(cwd, skillsDir = null) {
|
|
|
1746
1752
|
name,
|
|
1747
1753
|
description,
|
|
1748
1754
|
triggers,
|
|
1749
|
-
path:
|
|
1750
|
-
file_path:
|
|
1755
|
+
path: dirPart,
|
|
1756
|
+
file_path: relPath,
|
|
1751
1757
|
root: rootInfo.root,
|
|
1752
1758
|
scope: rootInfo.scope,
|
|
1753
1759
|
installed: rootInfo.scope !== 'import-only',
|
|
1754
1760
|
deprecated: !!rootInfo.deprecated,
|
|
1755
1761
|
});
|
|
1756
|
-
|
|
1762
|
+
return true;
|
|
1763
|
+
}
|
|
1764
|
+
let skillCount = 0;
|
|
1765
|
+
for (const entry of entries) {
|
|
1766
|
+
if (!entry.isDirectory())
|
|
1767
|
+
continue;
|
|
1768
|
+
const skillMdPath = node_path_1.default.join(rootPath, entry.name, 'SKILL.md');
|
|
1769
|
+
const content = (0, shell_command_projection_cjs_1.platformReadSync)(skillMdPath);
|
|
1770
|
+
if (content !== null) {
|
|
1771
|
+
if (pushSkillEntry(`${entry.name}/SKILL.md`, content))
|
|
1772
|
+
skillCount++;
|
|
1773
|
+
}
|
|
1774
|
+
// Nested layout: <entry>/skills/<stem>/SKILL.md
|
|
1775
|
+
// Used by cline, qwen, hermes, augment, trae, antigravity (#69 nested=true).
|
|
1776
|
+
// Descend exactly one level into <entry>/skills/ — no deeper recursion.
|
|
1777
|
+
// Scope to gsd-ns-* routers only: never vacuum up an unrelated user skill
|
|
1778
|
+
// that happens to have its own `skills/` subdirectory.
|
|
1779
|
+
if (!entry.name.startsWith('gsd-ns-'))
|
|
1780
|
+
continue;
|
|
1781
|
+
const nestedSkillsDir = node_path_1.default.join(rootPath, entry.name, 'skills');
|
|
1782
|
+
let nestedEntries = [];
|
|
1783
|
+
try {
|
|
1784
|
+
nestedEntries = node_fs_1.default.readdirSync(nestedSkillsDir, { withFileTypes: true });
|
|
1785
|
+
}
|
|
1786
|
+
catch {
|
|
1787
|
+
// No skills/ subdir — flat layout or unreadable; nothing to do.
|
|
1788
|
+
nestedEntries = [];
|
|
1789
|
+
}
|
|
1790
|
+
for (const nested of nestedEntries) {
|
|
1791
|
+
if (!nested.isDirectory())
|
|
1792
|
+
continue;
|
|
1793
|
+
const nestedSkillMd = node_path_1.default.join(nestedSkillsDir, nested.name, 'SKILL.md');
|
|
1794
|
+
const nestedContent = (0, shell_command_projection_cjs_1.platformReadSync)(nestedSkillMd);
|
|
1795
|
+
if (nestedContent === null)
|
|
1796
|
+
continue;
|
|
1797
|
+
// Use forward-slash separator explicitly so manifest paths are posix-style
|
|
1798
|
+
// on all platforms, matching the flat-layout behaviour above.
|
|
1799
|
+
const relPath = `${entry.name}/skills/${nested.name}/SKILL.md`;
|
|
1800
|
+
if (pushSkillEntry(relPath, nestedContent))
|
|
1801
|
+
skillCount++;
|
|
1802
|
+
}
|
|
1757
1803
|
}
|
|
1758
1804
|
rootSummary.skill_count = skillCount;
|
|
1759
1805
|
roots.push(rootSummary);
|
|
@@ -157,10 +157,44 @@ function computeClosure(base, manifest) {
|
|
|
157
157
|
}
|
|
158
158
|
return closed;
|
|
159
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Compute the capability skills to add for a given profile mode from the registry.
|
|
162
|
+
* Returns an array of skill stems contributed by capabilities whose profileMembership
|
|
163
|
+
* includes the given mode. Guards against prototype pollution and malformed registry.
|
|
164
|
+
*/
|
|
165
|
+
function _capabilitySkillsForMode(mode, registry) {
|
|
166
|
+
const BANNED = ['__proto__', 'constructor', 'prototype'];
|
|
167
|
+
const clusters = registry.capabilityClusters;
|
|
168
|
+
const membership = registry.profileMembership;
|
|
169
|
+
if (!clusters || typeof clusters !== 'object' || !membership || typeof membership !== 'object') {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
const result = [];
|
|
173
|
+
for (const capId of Object.keys(clusters)) {
|
|
174
|
+
if (BANNED.includes(capId))
|
|
175
|
+
continue;
|
|
176
|
+
const mem = membership[capId];
|
|
177
|
+
if (!mem || typeof mem !== 'object')
|
|
178
|
+
continue;
|
|
179
|
+
const profiles = mem.profiles;
|
|
180
|
+
if (!Array.isArray(profiles))
|
|
181
|
+
continue;
|
|
182
|
+
if (!profiles.includes(mode))
|
|
183
|
+
continue;
|
|
184
|
+
const skills = clusters[capId];
|
|
185
|
+
if (!Array.isArray(skills))
|
|
186
|
+
continue;
|
|
187
|
+
for (const s of skills) {
|
|
188
|
+
if (typeof s === 'string' && s.length > 0)
|
|
189
|
+
result.push(s);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
160
194
|
/**
|
|
161
195
|
* Resolve a profile (or composed profiles) to a typed result object.
|
|
162
196
|
*/
|
|
163
|
-
function resolveProfile({ modes, manifest, _profilesOverride } = {}) {
|
|
197
|
+
function resolveProfile({ modes, manifest, _profilesOverride, registry } = {}) {
|
|
164
198
|
const profiles = _profilesOverride || PROFILES;
|
|
165
199
|
const activeModes = (modes && modes.length > 0) ? modes : ['full'];
|
|
166
200
|
const normalizedModes = activeModes
|
|
@@ -185,7 +219,11 @@ function resolveProfile({ modes, manifest, _profilesOverride } = {}) {
|
|
|
185
219
|
// This profile is full — sentinel short-circuit
|
|
186
220
|
return { name: 'full', skills: '*', agents: new Set() };
|
|
187
221
|
}
|
|
188
|
-
|
|
222
|
+
// ADR-857 phase 4c: union capability skills for this mode BEFORE closure so
|
|
223
|
+
// their requires: chains expand too.
|
|
224
|
+
const capSkills = registry ? _capabilitySkillsForMode(mode, registry) : [];
|
|
225
|
+
const baseWithCap = [...base, ...capSkills];
|
|
226
|
+
const closure = computeClosure(baseWithCap, man);
|
|
189
227
|
for (const s of closure)
|
|
190
228
|
unionSkills.add(s);
|
|
191
229
|
}
|
|
@@ -318,9 +356,103 @@ function stageAgentsForProfile(srcAgentsDir, resolvedProfile) {
|
|
|
318
356
|
ensureExitCleanup();
|
|
319
357
|
return stageDir;
|
|
320
358
|
}
|
|
321
|
-
|
|
359
|
+
/**
|
|
360
|
+
* Build the namespace router → concrete sub-skill mapping (#69). The
|
|
361
|
+
* authoritative source is each `ns-*.md` router file's `requires:` frontmatter
|
|
362
|
+
* list. A concrete skill may be routed by more than one router (e.g. spec-phase
|
|
363
|
+
* is shared by ns-workflow and ns-ideate); it is nested — and physically
|
|
364
|
+
* duplicated — under every owning router.
|
|
365
|
+
*/
|
|
366
|
+
function buildNamespaceBundleMap(srcCommandsDir) {
|
|
367
|
+
const routerStems = new Set();
|
|
368
|
+
const routerChildren = new Map();
|
|
369
|
+
const childToRouters = new Map();
|
|
370
|
+
if (!node_fs_1.default.existsSync(srcCommandsDir)) {
|
|
371
|
+
return { routerStems, routerChildren, childToRouters };
|
|
372
|
+
}
|
|
373
|
+
for (const entry of node_fs_1.default.readdirSync(srcCommandsDir, { withFileTypes: true })) {
|
|
374
|
+
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
375
|
+
continue;
|
|
376
|
+
if (!entry.name.startsWith('ns-'))
|
|
377
|
+
continue;
|
|
378
|
+
const stem = entry.name.slice(0, -3);
|
|
379
|
+
let children = [];
|
|
380
|
+
try {
|
|
381
|
+
children = parseRequires(node_fs_1.default.readFileSync(node_path_1.default.join(srcCommandsDir, entry.name), 'utf8'));
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
children = [];
|
|
385
|
+
}
|
|
386
|
+
routerStems.add(stem);
|
|
387
|
+
routerChildren.set(stem, children);
|
|
388
|
+
for (const child of children) {
|
|
389
|
+
const owners = childToRouters.get(child) || [];
|
|
390
|
+
owners.push(stem);
|
|
391
|
+
childToRouters.set(child, owners);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return { routerStems, routerChildren, childToRouters };
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Rewrite a converted namespace-router SKILL.md so its routing table points at
|
|
398
|
+
* nested sub-skill files instead of bare Skill-tool names (#69). Each table row
|
|
399
|
+
* whose final cell carries a `gsd-<stem>` token (optionally with `--flag`
|
|
400
|
+
* suffixes) is rewritten to `Read \`skills/<stem>/SKILL.md\`` (flags preserved
|
|
401
|
+
* as a note), the `Invoke` column header becomes `Read`, and the
|
|
402
|
+
* "Invoke … using the Skill tool" trailer becomes a file-read instruction.
|
|
403
|
+
* Only lines beginning with a table pipe are touched, so the `|` inside the
|
|
404
|
+
* `description:` frontmatter field is never matched.
|
|
405
|
+
*/
|
|
406
|
+
function transformRouterBodyToNested(converted) {
|
|
407
|
+
const lines = converted.split('\n');
|
|
408
|
+
const out = lines.map((line) => {
|
|
409
|
+
if (/Invoke the matched skill directly using the Skill tool\./.test(line)) {
|
|
410
|
+
return line.replace(/Invoke the matched skill directly using the Skill tool\./, "Read the matched sub-skill's SKILL.md and follow its instructions. The `skills/<name>/SKILL.md` paths in the right column are relative to this skill's own directory.");
|
|
411
|
+
}
|
|
412
|
+
if (!/^\s*\|/.test(line))
|
|
413
|
+
return line;
|
|
414
|
+
if (/^\s*\|[\s:|-]+\|\s*$/.test(line))
|
|
415
|
+
return line;
|
|
416
|
+
if (/\|\s*Invoke\s*\|/.test(line)) {
|
|
417
|
+
return line.replace(/\|\s*Invoke\s*\|/, '| Read |');
|
|
418
|
+
}
|
|
419
|
+
const cells = line.split('|');
|
|
420
|
+
const lastIdx = cells.length - 2;
|
|
421
|
+
if (lastIdx < 1)
|
|
422
|
+
return line;
|
|
423
|
+
const cell = cells[lastIdx];
|
|
424
|
+
const m = cell.match(/gsd-([a-z0-9-]+)((?:\s+--[a-z0-9-]+)*)/i);
|
|
425
|
+
if (!m)
|
|
426
|
+
return line;
|
|
427
|
+
const stem = m[1];
|
|
428
|
+
const flags = m[2].trim();
|
|
429
|
+
cells[lastIdx] = flags
|
|
430
|
+
? ` Read \`skills/${stem}/SKILL.md\` (${flags}) `
|
|
431
|
+
: ` Read \`skills/${stem}/SKILL.md\` `;
|
|
432
|
+
return cells.join('|');
|
|
433
|
+
});
|
|
434
|
+
return out.join('\n');
|
|
435
|
+
}
|
|
436
|
+
function stageSkillsForRuntimeAsSkills(srcCommandsDir, resolvedProfile, converter, prefix, nested = false) {
|
|
322
437
|
if (!node_fs_1.default.existsSync(srcCommandsDir))
|
|
323
438
|
return srcCommandsDir;
|
|
439
|
+
// Nesting applies to the `full` install AND to any surface whose skill set
|
|
440
|
+
// still contains every namespace router (a full/reset surface). It must NOT
|
|
441
|
+
// depend on the `'*'` sentinel alone: applySurface() materializes `full` into
|
|
442
|
+
// a concrete Set, so a sentinel-only gate would re-flatten the layout on every
|
|
443
|
+
// surface apply/reset (#69 adversarial-review finding). A partial surface that
|
|
444
|
+
// drops a whole router cluster falls back to flat automatically.
|
|
445
|
+
const bundles = nested ? buildNamespaceBundleMap(srcCommandsDir) : null;
|
|
446
|
+
let doNest = false;
|
|
447
|
+
if (nested && bundles && bundles.routerStems.size > 0) {
|
|
448
|
+
if (resolvedProfile.skills === '*') {
|
|
449
|
+
doNest = true;
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
const present = resolvedProfile.skills;
|
|
453
|
+
doNest = [...bundles.routerStems].every((r) => present.has(r));
|
|
454
|
+
}
|
|
455
|
+
}
|
|
324
456
|
const stageDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'gsd-profile-runtime-skills-'));
|
|
325
457
|
try {
|
|
326
458
|
const entries = node_fs_1.default.readdirSync(srcCommandsDir, { withFileTypes: true });
|
|
@@ -335,6 +467,27 @@ function stageSkillsForRuntimeAsSkills(srcCommandsDir, resolvedProfile, converte
|
|
|
335
467
|
const content = node_fs_1.default.readFileSync(node_path_1.default.join(srcCommandsDir, entry.name), 'utf8');
|
|
336
468
|
const skillName = `${prefix}${stem}`;
|
|
337
469
|
const converted = converter(content, skillName);
|
|
470
|
+
if (doNest && bundles.routerStems.has(stem)) {
|
|
471
|
+
// Router skill: rewrite its routing table to the nested Read pattern and
|
|
472
|
+
// emit it as the single top-level bundle entry.
|
|
473
|
+
const destDir = node_path_1.default.join(stageDir, skillName);
|
|
474
|
+
node_fs_1.default.mkdirSync(destDir, { recursive: true });
|
|
475
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(destDir, 'SKILL.md'), transformRouterBodyToNested(converted));
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (doNest && bundles.childToRouters.has(stem)) {
|
|
479
|
+
// Concrete skill routed by one or more namespace routers: nest a copy
|
|
480
|
+
// under each owning router's skills/ subdir so it drops out of the
|
|
481
|
+
// top-level eager listing while staying readable by file path (#69).
|
|
482
|
+
for (const routerStem of bundles.childToRouters.get(stem)) {
|
|
483
|
+
const destDir = node_path_1.default.join(stageDir, `${prefix}${routerStem}`, 'skills', stem);
|
|
484
|
+
node_fs_1.default.mkdirSync(destDir, { recursive: true });
|
|
485
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(destDir, 'SKILL.md'), converted);
|
|
486
|
+
}
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
// Flat top-level skill (default behaviour; also the unrouted fallback when
|
|
490
|
+
// nesting is active).
|
|
338
491
|
const destDir = node_path_1.default.join(stageDir, skillName);
|
|
339
492
|
node_fs_1.default.mkdirSync(destDir, { recursive: true });
|
|
340
493
|
node_fs_1.default.writeFileSync(node_path_1.default.join(destDir, 'SKILL.md'), converted);
|
|
@@ -575,6 +728,7 @@ module.exports = {
|
|
|
575
728
|
readActiveProfile,
|
|
576
729
|
writeActiveProfile,
|
|
577
730
|
// Shared internals
|
|
731
|
+
parseRequires,
|
|
578
732
|
cleanupStagedSkills,
|
|
579
733
|
// Back-compat / deprecated
|
|
580
734
|
MINIMAL_SKILL_ALLOWLIST,
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Installer migration 004: remove stale gsd-pristine/get-shit-done/ snapshot // gsd-allow-legacy-name
|
|
4
|
+
* files after the get-shit-done → gsd-core rename (#604, #934). // gsd-allow-legacy-name
|
|
5
|
+
*
|
|
6
|
+
* Background: migration 003 removed legacy runtime files from
|
|
7
|
+
* get-shit-done/ but did not touch gsd-pristine/get-shit-done/, the // gsd-allow-legacy-name
|
|
8
|
+
* parallel directory that holds pristine snapshots captured before the rename.
|
|
9
|
+
* These snapshot files are GSD-managed (written by the installer, never by the
|
|
10
|
+
* user) and reference stale get-shit-done/... key paths that no longer exist // gsd-allow-legacy-name
|
|
11
|
+
* in the active layout. When verify-reapply-patches.cjs looks up a backup entry
|
|
12
|
+
* keyed under gsd-core/... it finds no matching gsd-pristine/ snapshot, falls
|
|
13
|
+
* to over-broad mode, and reports false FAIL_INSTALLED_MISSING / // gsd-allow-legacy-name
|
|
14
|
+
* FAIL_USER_LINES_MISSING for every backed-up pre-rename file (#934).
|
|
15
|
+
*
|
|
16
|
+
* Fix: walk gsd-pristine/get-shit-done/ and emit remove-managed for each file. // gsd-allow-legacy-name
|
|
17
|
+
* These files are always GSD-written snapshots — users never place their own
|
|
18
|
+
* files inside gsd-pristine/ — so the classification override
|
|
19
|
+
* (managed-pristine) is safe: there is no user content to protect.
|
|
20
|
+
*
|
|
21
|
+
* Checksum safety: migration 003's body is left untouched. Adding this
|
|
22
|
+
* separate migration avoids modifying 003's checksum, which would break
|
|
23
|
+
* upgrade state for any user who already applied 003 (root cause of #670).
|
|
24
|
+
*
|
|
25
|
+
* Per-file approach: the migration framework has no recursive directory-removal
|
|
26
|
+
* primitive — all actions operate on individual files. Empty directory shells
|
|
27
|
+
* left after removal can be cleaned up manually; this is the intentional ADR-0008
|
|
28
|
+
* limitation.
|
|
29
|
+
*/
|
|
30
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
31
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
32
|
+
};
|
|
33
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
34
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
35
|
+
function walkPristineFiles(root, relDir, baseResolved, results) {
|
|
36
|
+
const dir = node_path_1.default.join(root, relDir);
|
|
37
|
+
let entries;
|
|
38
|
+
try {
|
|
39
|
+
entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return; // directory absent or unreadable — nothing to do
|
|
43
|
+
}
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
// Do not follow symlinks — skip to avoid out-of-tree traversal.
|
|
46
|
+
if (entry.isSymbolicLink())
|
|
47
|
+
continue;
|
|
48
|
+
const relPath = node_path_1.default.posix.join(relDir, entry.name);
|
|
49
|
+
// Bounds check: ensure the resolved path stays under configDir.
|
|
50
|
+
const resolved = node_path_1.default.resolve(root, relPath);
|
|
51
|
+
if (resolved !== baseResolved && !resolved.startsWith(baseResolved + node_path_1.default.sep))
|
|
52
|
+
continue;
|
|
53
|
+
if (entry.isDirectory()) {
|
|
54
|
+
walkPristineFiles(root, relPath, baseResolved, results);
|
|
55
|
+
}
|
|
56
|
+
else if (entry.isFile()) {
|
|
57
|
+
results.push(relPath);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const REASON = 'stale pristine snapshot from legacy get-shit-done/ dir, orphaned by rename migration 003 (#604, #934)'; // gsd-allow-legacy-name
|
|
62
|
+
const migration = {
|
|
63
|
+
id: '2026-06-09-prune-stale-pristine-get-shit-done', // gsd-allow-legacy-name
|
|
64
|
+
title: 'Remove stale gsd-pristine/get-shit-done/ snapshot files (#934)', // gsd-allow-legacy-name
|
|
65
|
+
description: 'Migration 003 removed runtime files from get-shit-done/ but left the matching pristine snapshot ' + // gsd-allow-legacy-name
|
|
66
|
+
'directory gsd-pristine/get-shit-done/ intact. Those snapshots reference stale key paths and cause ' + // gsd-allow-legacy-name
|
|
67
|
+
'verify-reapply-patches false positives (#934). Remove all files under gsd-pristine/get-shit-done/ ' + // gsd-allow-legacy-name
|
|
68
|
+
'as they are GSD-managed snapshots, never user content.',
|
|
69
|
+
introducedIn: '1.4.3',
|
|
70
|
+
scopes: ['global', 'local'],
|
|
71
|
+
destructive: true,
|
|
72
|
+
plan(ctx) {
|
|
73
|
+
const pristineGsdRoot = node_path_1.default.join(ctx.configDir, 'gsd-pristine', 'get-shit-done'); // gsd-allow-legacy-name
|
|
74
|
+
// Idempotency: if the stale pristine subdir doesn't exist, nothing to do.
|
|
75
|
+
if (!node_fs_1.default.existsSync(pristineGsdRoot))
|
|
76
|
+
return [];
|
|
77
|
+
// Safety: reject symlinks in ANY ancestor component of the path we will walk
|
|
78
|
+
// to prevent following a symlink out of configDir. Check both gsd-pristine/
|
|
79
|
+
// and gsd-pristine/get-shit-done/ — either being a symlink could redirect // gsd-allow-legacy-name
|
|
80
|
+
// the walk to an out-of-tree location.
|
|
81
|
+
const pristineParent = node_path_1.default.join(ctx.configDir, 'gsd-pristine');
|
|
82
|
+
try {
|
|
83
|
+
if (node_fs_1.default.lstatSync(pristineParent).isSymbolicLink())
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
if (node_fs_1.default.lstatSync(pristineGsdRoot).isSymbolicLink())
|
|
91
|
+
return []; // gsd-allow-legacy-name
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const baseResolved = node_path_1.default.resolve(ctx.configDir);
|
|
97
|
+
const relPaths = [];
|
|
98
|
+
walkPristineFiles(ctx.configDir, node_path_1.default.posix.join('gsd-pristine', 'get-shit-done'), baseResolved, relPaths); // gsd-allow-legacy-name
|
|
99
|
+
const actions = [];
|
|
100
|
+
for (const relPath of relPaths) {
|
|
101
|
+
// Bounds-check each relPath before emitting any action.
|
|
102
|
+
const resolved = node_path_1.default.resolve(ctx.configDir, relPath);
|
|
103
|
+
if (resolved !== baseResolved && !resolved.startsWith(baseResolved + node_path_1.default.sep))
|
|
104
|
+
continue;
|
|
105
|
+
// These files are GSD-managed pristine snapshots — the installer writes
|
|
106
|
+
// them during install/upgrade; users never place personal files inside
|
|
107
|
+
// gsd-pristine/. Pass classification: 'managed-pristine' explicitly so
|
|
108
|
+
// the framework does not downgrade remove-managed to preserve-user when
|
|
109
|
+
// the manifest has no entry (these paths were never in the manifest since
|
|
110
|
+
// they live under gsd-pristine/, not the tracked runtime dir).
|
|
111
|
+
actions.push({
|
|
112
|
+
type: 'remove-managed',
|
|
113
|
+
relPath,
|
|
114
|
+
reason: REASON,
|
|
115
|
+
ownershipEvidence: 'GSD-written pristine snapshot under gsd-pristine/get-shit-done/; ' + // gsd-allow-legacy-name
|
|
116
|
+
'installer is the sole author of gsd-pristine/ contents; no user content lives here',
|
|
117
|
+
classification: 'managed-pristine',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return actions;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
module.exports = migration;
|