@opengsd/gsd-core 1.20.0 → 1.21.0
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 +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +33 -2
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +3 -3
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +265 -161
- package/commands/gsd/autonomous.md +1 -1
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/new-project.md +2 -2
- package/gsd-core/bin/gsd-tools.cjs +554 -823
- package/gsd-core/bin/lib/capability-activation.cjs +8 -2
- package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
- package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
- package/gsd-core/bin/lib/capability-lock.cjs +19 -27
- package/gsd-core/bin/lib/capability-registry.cjs +74 -71
- package/gsd-core/bin/lib/capability-validator.cjs +1 -0
- package/gsd-core/bin/lib/capability-writer.cjs +7 -4
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
- package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
- package/gsd-core/bin/lib/cli-surface.cjs +545 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
- package/gsd-core/bin/lib/commands.cjs +99 -2
- package/gsd-core/bin/lib/config-file-read.cjs +85 -0
- package/gsd-core/bin/lib/config-loader.cjs +23 -15
- package/gsd-core/bin/lib/config.cjs +139 -101
- package/gsd-core/bin/lib/configuration.cjs +41 -2
- package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
- package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
- package/gsd-core/bin/lib/gap-checker.cjs +12 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
- package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
- package/gsd-core/bin/lib/graphify.cjs +19 -2
- package/gsd-core/bin/lib/gsd-tool-dir.cjs +134 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
- package/gsd-core/bin/lib/init.cjs +20 -18
- package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
- package/gsd-core/bin/lib/install-engine.cjs +20 -28
- package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
- package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
- package/gsd-core/bin/lib/install-profiles.cjs +4 -2
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
- package/gsd-core/bin/lib/markdown-table.cjs +39 -1
- package/gsd-core/bin/lib/milestone.cjs +8 -6
- package/gsd-core/bin/lib/model-resolver.cjs +8 -4
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
- package/gsd-core/bin/lib/phase-id.cjs +26 -9
- package/gsd-core/bin/lib/phase-locator.cjs +8 -3
- package/gsd-core/bin/lib/phase.cjs +170 -45
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
- package/gsd-core/bin/lib/planning-snapshot.cjs +10 -6
- package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
- package/gsd-core/bin/lib/profile-output.cjs +9 -6
- package/gsd-core/bin/lib/project-context.cjs +439 -0
- package/gsd-core/bin/lib/project-root.cjs +9 -230
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
- package/gsd-core/bin/lib/report-parser.cjs +16 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +357 -50
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
- package/gsd-core/bin/lib/roadmap.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +317 -134
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
- package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
- package/gsd-core/bin/lib/state-transition.cjs +103 -46
- package/gsd-core/bin/lib/state.cjs +448 -63
- package/gsd-core/bin/lib/surface.cjs +4 -7
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
- package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
- package/gsd-core/bin/lib/verify.cjs +49 -6
- package/gsd-core/bin/lib/workstream-inventory.cjs +4 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/autonomous-smart-discuss.md +99 -18
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/planner-guidance.md +11 -0
- package/gsd-core/references/planner-human-verify-mode.md +1 -1
- package/gsd-core/references/planner-reversibility.md +2 -0
- package/gsd-core/references/planner-verify-command-grounding.md +22 -6
- package/gsd-core/workflows/autonomous.md +4 -4
- package/gsd-core/workflows/code-review-fix.md +5 -7
- package/gsd-core/workflows/execute-phase.md +21 -7
- package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
- package/gsd-core/workflows/plan-phase.md +18 -19
- package/hooks/dist/gsd-agent-isolation-guard.js +4 -3
- package/hooks/dist/gsd-config-reload.js +2 -2
- package/hooks/dist/gsd-context-monitor.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +9 -6
- package/hooks/dist/gsd-read-injection-scanner.js +2 -2
- package/hooks/dist/gsd-secret-read-guard.js +141 -6
- package/hooks/dist/gsd-statusline.js +3 -2
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/lib/config-read.js +49 -0
- package/hooks/dist/lib/git-cmd.js +3 -1
- package/hooks/dist/lib/git-head-tree.js +474 -0
- package/hooks/dist/lib/isolation-sentinel.js +168 -13
- package/hooks/dist/lib/secret-read-exemptions.js +161 -0
- package/hooks/gsd-agent-isolation-guard.js +4 -3
- package/hooks/gsd-config-reload.js +2 -2
- package/hooks/gsd-context-monitor.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +9 -6
- package/hooks/gsd-read-injection-scanner.js +2 -2
- package/hooks/gsd-secret-read-guard.js +141 -6
- package/hooks/gsd-statusline.js +3 -2
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/lib/config-read.js +49 -0
- package/hooks/lib/git-cmd.js +3 -1
- package/hooks/lib/git-head-tree.js +474 -0
- package/hooks/lib/isolation-sentinel.js +168 -13
- package/hooks/lib/secret-read-exemptions.js +161 -0
- package/package.json +7 -2
- package/pi/package-agents/gsd-executor.md +1 -1
- package/pi/package-agents/gsd-plan-checker.md +1 -1
- package/pi/package-agents/gsd-planner.md +3 -3
- package/pi/package-agents/gsd-verifier.md +2 -2
- package/pi/package-agents/manifest.json +4 -4
- package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
- package/scripts/changeset/cli.cjs +2 -2
- package/scripts/changeset/lint.cjs +28 -3
- package/scripts/changeset/serialize.cjs +12 -2
- package/scripts/ci-compact-v8-coverage.cjs +204 -0
- package/scripts/ci-fix-next-rerun.cjs +149 -0
- package/scripts/ci-next-health.cjs +52 -9
- package/scripts/ci-test-scope.cjs +77 -71
- package/scripts/command-contract-helpers.cjs +56 -7
- package/scripts/docs-guard-registry.cjs +3 -0
- package/scripts/gen-cli-surface-manifest.cjs +287 -0
- package/scripts/gen-platform-conformance-tier.cjs +258 -19
- package/scripts/lib/macos-conformance-tier.generated.cjs +59 -47
- package/scripts/lib/platform-conformance-tier.generated.cjs +23 -64
- package/scripts/lib/shellcheck-fetch.cjs +71 -4
- package/scripts/lint-removed-but-needed.cjs +562 -50
- package/scripts/merge-commit-pr.cjs +186 -0
- package/scripts/release-ancestry.cjs +86 -0
- package/scripts/release-notes/changelog-release-notes.cjs +345 -0
- package/scripts/release-notes/discord-release-summary.cjs +82 -1
- package/scripts/release-pipeline-pr.cjs +92 -0
- package/scripts/require-issue-link-policy.cjs +18 -9
- package/scripts/sync-next-version.cjs +11 -2
- package/scripts/timeout-evidence-from-ci.cjs +964 -0
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-new-project/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -31,7 +31,7 @@ const gate_verdict_cjs_1 = require("./gate-verdict.cjs");
|
|
|
31
31
|
const gate_evidence_cjs_1 = require("./gate-evidence.cjs");
|
|
32
32
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
33
33
|
const planningWorkspaceMod = require("./planning-workspace.cjs");
|
|
34
|
-
const { planningDir } = planningWorkspaceMod;
|
|
34
|
+
const { planningDir, planningRoot } = planningWorkspaceMod;
|
|
35
35
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
36
36
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
37
37
|
const { phaseDirectoryOptions, findPhaseInternal, resolvePhaseDirectoryLookup, matchPhaseDirsForLookup } = phaseLocatorMod;
|
|
@@ -40,12 +40,24 @@ const roadmapModule = require("./roadmap.cjs");
|
|
|
40
40
|
const { getRoadmapPhaseWithFallback } = roadmapModule;
|
|
41
41
|
/**
|
|
42
42
|
* Resolve a caller-supplied path (absolute, or relative to `projectDir`) and require the result to
|
|
43
|
-
* stay inside `projectDir`
|
|
44
|
-
*
|
|
43
|
+
* stay inside `projectDir` or inside the realpath of the project's OWN `.planning` (#5474: it may be
|
|
44
|
+
* a symlink to an out-of-repo store, so its phases realpath outside `projectDir`; ADR-5057 "Two
|
|
45
|
+
* containment roots"). Both decisions go through `tryWithinRoot` (realpath containment, ADR-4650);
|
|
46
|
+
* a symlink inside the store that leaves it, `..` traversal and a name-prefix sibling of the store
|
|
47
|
+
* still fail both. Returns the contained path the predicate produced, or a `GateUsageFailure`
|
|
48
|
+
* (`usage`, `path escapes its allowed directory: <arg>`).
|
|
49
|
+
*
|
|
50
|
+
* Trust assumption: the `.planning` link is trusted like the rest of the project tree. Whatever it
|
|
51
|
+
* resolves to becomes a containment root, so a `.planning` linked to a broad directory (a home
|
|
52
|
+
* directory, `/etc`) makes every path under that directory contained. This is accepted by design
|
|
53
|
+
* (#5474 scope): the link is part of the checkout the user chose to run gates on, and the gates
|
|
54
|
+
* read paths, they do not grant access the user's own shell lacks. Only the project's own
|
|
55
|
+
* `.planning` is honoured; any other symlink target is still refused.
|
|
45
56
|
*/
|
|
46
57
|
function resolveContainedPath(inputPath, projectDir) {
|
|
47
58
|
const candidate = node_path_1.default.isAbsolute(inputPath) ? inputPath : node_path_1.default.join(projectDir, inputPath);
|
|
48
|
-
const contained = (0, security_cjs_1.tryWithinRoot)(candidate, projectDir, security_cjs_1.PathAcceptance.AbsoluteInsideRoot)
|
|
59
|
+
const contained = (0, security_cjs_1.tryWithinRoot)(candidate, projectDir, security_cjs_1.PathAcceptance.AbsoluteInsideRoot) ??
|
|
60
|
+
(0, security_cjs_1.tryWithinRoot)(candidate, planningRoot(projectDir), security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
49
61
|
if (contained === null) {
|
|
50
62
|
return (0, gate_verdict_cjs_1.gateUsageFailure)(gate_verdict_cjs_1.GATE_FAILURE_CODE.USAGE, `path escapes its allowed directory: ${inputPath}`);
|
|
51
63
|
}
|
|
@@ -14,6 +14,9 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
14
14
|
const node_path_1 = __importDefault(require("node:path"));
|
|
15
15
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
16
16
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- config-file-read.cjs is an export= CommonJS module
|
|
18
|
+
const configFileReadMod = require("./config-file-read.cjs");
|
|
19
|
+
const { readConfigFileBounded } = configFileReadMod;
|
|
17
20
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
18
21
|
const capabilityStateMod = require("./capability-state.cjs");
|
|
19
22
|
const { isCapabilityActive } = capabilityStateMod;
|
|
@@ -175,6 +178,20 @@ function safeReadJson(filePath) {
|
|
|
175
178
|
return null;
|
|
176
179
|
}
|
|
177
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Safely read and parse `<planningDir>/config.json` as a bounded regular file (#5759). Returns
|
|
183
|
+
* null on a missing, non-regular (FIFO/device), oversized, unreadable or malformed file — never
|
|
184
|
+
* blocks. Not `safeReadJson`: that reader is for graph artifacts and opens blocking.
|
|
185
|
+
*/
|
|
186
|
+
function safeReadConfigJson(planningDir) {
|
|
187
|
+
try {
|
|
188
|
+
const raw = readConfigFileBounded(node_path_1.default.join(planningDir, 'config.json'));
|
|
189
|
+
return raw === null ? null : JSON.parse(raw);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
178
195
|
/**
|
|
179
196
|
* Build a bidirectional adjacency map from graph nodes and edges.
|
|
180
197
|
* Each node ID maps to an array of { target, edge } entries.
|
|
@@ -390,7 +407,7 @@ const SNAPSHOT_FILENAME = '.last-build-snapshot.json';
|
|
|
390
407
|
* default `<planningDir>/graphs/graph.json` when unset/blank/non-string.
|
|
391
408
|
*/
|
|
392
409
|
function resolveGraphLocation(cwd, planningDir) {
|
|
393
|
-
const config =
|
|
410
|
+
const config = safeReadConfigJson(planningDir);
|
|
394
411
|
const graphify = (config && config['graphify']);
|
|
395
412
|
const configuredValue = graphify && graphify['graph_path'];
|
|
396
413
|
if (typeof configuredValue === 'string' && configuredValue.trim().length > 0) {
|
|
@@ -564,7 +581,7 @@ function graphifyBuild(cwd) {
|
|
|
564
581
|
const graphsDir = node_path_1.default.join(planningDir, 'graphs');
|
|
565
582
|
node_fs_1.default.mkdirSync(graphsDir, { recursive: true });
|
|
566
583
|
// Read build timeout from config -- default 300s per D-02
|
|
567
|
-
const config =
|
|
584
|
+
const config = safeReadConfigJson(planningDir) || {};
|
|
568
585
|
const graphifyConfig = config.graphify;
|
|
569
586
|
const timeoutSec = (graphifyConfig && graphifyConfig.build_timeout) || 300;
|
|
570
587
|
return {
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tool-owned `.gsd/` directory owner (#5757).
|
|
4
|
+
*
|
|
5
|
+
* gsd-core keeps small tool state under `<project>/.gsd/` (the dispatch
|
|
6
|
+
* isolation sentinel, the harness fork-probe cache). A user project does not
|
|
7
|
+
* inherit gsd-core's own root `.gitignore`, and the project may contain a
|
|
8
|
+
* `.gsd` symlink, so every writer needs the same two guarantees before it
|
|
9
|
+
* writes anything there:
|
|
10
|
+
*
|
|
11
|
+
* 1. the directory is a real directory that resolves inside the project
|
|
12
|
+
* (never write through a project-supplied symlink or ancestor redirect);
|
|
13
|
+
* 2. the directory is ignored by git (`.gsd/.gitignore`), without clobbering
|
|
14
|
+
* rules a user already put there.
|
|
15
|
+
*
|
|
16
|
+
* This module is the single owner of both. Callers pass their own ignore-file
|
|
17
|
+
* `marker` comment and the `rules` they need; the owner appends only the rules
|
|
18
|
+
* that are missing.
|
|
19
|
+
*/
|
|
20
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
21
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.GSD_IGNORE_SYMLINK = exports.GSD_DIR_UNSAFE = void 0;
|
|
25
|
+
exports.ignoreAdditions = ignoreAdditions;
|
|
26
|
+
exports.ensureToolOwnedGsdDir = ensureToolOwnedGsdDir;
|
|
27
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
28
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
29
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
30
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
31
|
+
const SYMLINK_IGNORE_MESSAGE = '.gsd/.gitignore is a symlink';
|
|
32
|
+
/** Stable `code` values on the refusal errors; callers translate by code, never by message. */
|
|
33
|
+
exports.GSD_DIR_UNSAFE = 'GSD_DIR_UNSAFE';
|
|
34
|
+
exports.GSD_IGNORE_SYMLINK = 'GSD_IGNORE_SYMLINK';
|
|
35
|
+
function codedError(message, code) {
|
|
36
|
+
return Object.assign(new Error(message), { code });
|
|
37
|
+
}
|
|
38
|
+
function hasLine(text, line) {
|
|
39
|
+
return text.split('\n').some((l) => l.replace(/\r$/, '') === line);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Pure merge: the text to append to an existing ignore file so that `marker`
|
|
43
|
+
* and every rule are present, or '' when nothing is missing. A file that
|
|
44
|
+
* already ignores everything (`*` line) needs nothing.
|
|
45
|
+
*/
|
|
46
|
+
function ignoreAdditions(current, { marker, rules }) {
|
|
47
|
+
if (hasLine(current, '*'))
|
|
48
|
+
return '';
|
|
49
|
+
const missing = rules.filter((rule) => !hasLine(current, rule));
|
|
50
|
+
if (missing.length === 0)
|
|
51
|
+
return '';
|
|
52
|
+
const lead = current === '' || current.endsWith('\n') ? '' : '\n';
|
|
53
|
+
const markerLine = current.includes(marker) ? '' : `${marker}\n`;
|
|
54
|
+
return `${lead}${markerLine}${missing.join('\n')}\n`;
|
|
55
|
+
}
|
|
56
|
+
function isLinkError(err) {
|
|
57
|
+
const e = err;
|
|
58
|
+
return !!e && (e.code === 'ELOOP' || e.message === SYMLINK_IGNORE_MESSAGE);
|
|
59
|
+
}
|
|
60
|
+
function mergeExistingIgnore(ignorePath, options) {
|
|
61
|
+
let fd;
|
|
62
|
+
let tmpPath = null;
|
|
63
|
+
try {
|
|
64
|
+
if (node_fs_1.default.lstatSync(ignorePath).isSymbolicLink())
|
|
65
|
+
throw new Error(SYMLINK_IGNORE_MESSAGE);
|
|
66
|
+
// Read through an fd opened without following links where supported
|
|
67
|
+
// (O_NOFOLLOW is absent on Windows, hence `|| 0`), then replace by rename
|
|
68
|
+
// rather than append through the path, so a link swapped in after the
|
|
69
|
+
// check cannot redirect a write elsewhere.
|
|
70
|
+
fd = node_fs_1.default.openSync(ignorePath, node_fs_1.default.constants.O_RDONLY | (node_fs_1.default.constants.O_NOFOLLOW || 0));
|
|
71
|
+
if (!node_fs_1.default.fstatSync(fd).isFile())
|
|
72
|
+
throw new Error('.gsd/.gitignore is not a regular file');
|
|
73
|
+
const current = node_fs_1.default.readFileSync(fd, 'utf8');
|
|
74
|
+
node_fs_1.default.closeSync(fd);
|
|
75
|
+
fd = undefined;
|
|
76
|
+
const additions = ignoreAdditions(current, options);
|
|
77
|
+
if (additions === '')
|
|
78
|
+
return;
|
|
79
|
+
// `tmpPath` is assigned only once the exclusive create succeeded, so an
|
|
80
|
+
// EEXIST never unlinks an entry this process did not create.
|
|
81
|
+
const candidateTmp = `${ignorePath}.tmp-${process.pid}-${node_crypto_1.default.randomBytes(6).toString('hex')}`;
|
|
82
|
+
const tmpFd = node_fs_1.default.openSync(candidateTmp, 'wx');
|
|
83
|
+
tmpPath = candidateTmp;
|
|
84
|
+
try {
|
|
85
|
+
node_fs_1.default.writeFileSync(tmpFd, current + additions);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
node_fs_1.default.closeSync(tmpFd);
|
|
89
|
+
}
|
|
90
|
+
if (node_fs_1.default.lstatSync(ignorePath).isSymbolicLink())
|
|
91
|
+
throw new Error(SYMLINK_IGNORE_MESSAGE);
|
|
92
|
+
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPath, ignorePath);
|
|
93
|
+
tmpPath = null;
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
if (isLinkError(err))
|
|
97
|
+
throw codedError(SYMLINK_IGNORE_MESSAGE, exports.GSD_IGNORE_SYMLINK);
|
|
98
|
+
// Ignore seeding is best-effort; the caller's state is still useful if
|
|
99
|
+
// this path cannot be read or updated.
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
if (fd !== undefined)
|
|
103
|
+
node_fs_1.default.closeSync(fd);
|
|
104
|
+
if (tmpPath) {
|
|
105
|
+
try {
|
|
106
|
+
node_fs_1.default.unlinkSync(tmpPath);
|
|
107
|
+
}
|
|
108
|
+
catch { /* best-effort cleanup */ }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Ensure `<cwd>/.gsd` exists as a real, project-contained, git-ignored
|
|
114
|
+
* directory and return its path. Throws when `.gsd` is a symlink, resolves
|
|
115
|
+
* outside the project, or `.gsd/.gitignore` is a symlink.
|
|
116
|
+
*/
|
|
117
|
+
function ensureToolOwnedGsdDir(cwd, options) {
|
|
118
|
+
const dir = node_path_1.default.join(cwd, '.gsd');
|
|
119
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
120
|
+
if (node_fs_1.default.lstatSync(dir).isSymbolicLink() ||
|
|
121
|
+
node_path_1.default.relative(node_fs_1.default.realpathSync(cwd), node_fs_1.default.realpathSync(dir)) !== '.gsd') {
|
|
122
|
+
throw codedError('.gsd directory is a symlink or resolves outside the project', exports.GSD_DIR_UNSAFE);
|
|
123
|
+
}
|
|
124
|
+
const ignorePath = node_path_1.default.join(dir, '.gitignore');
|
|
125
|
+
try {
|
|
126
|
+
node_fs_1.default.writeFileSync(ignorePath, '*\n', { flag: 'wx' });
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
if (err.code === 'EEXIST')
|
|
130
|
+
mergeExistingIgnore(ignorePath, options);
|
|
131
|
+
// Any other ignore-file IO failure must not prevent the caller's write.
|
|
132
|
+
}
|
|
133
|
+
return dir;
|
|
134
|
+
}
|
|
@@ -324,7 +324,11 @@ function runRepairAction(cwd, action, paths, args = {}) {
|
|
|
324
324
|
case REMEDY_ACTION.ADD_NYQUIST_KEY:
|
|
325
325
|
case REMEDY_ACTION.ADD_AI_INTEGRATION_PHASE_KEY: {
|
|
326
326
|
const key = action === REMEDY_ACTION.ADD_NYQUIST_KEY ? 'nyquist_validation' : 'ai_integration_phase';
|
|
327
|
-
|
|
327
|
+
// Bounded read (#5759): a FIFO/device/oversized config throws (the repair fails, never blocks).
|
|
328
|
+
const configRaw = configLoaderMod.readConfigFileBounded(configPath);
|
|
329
|
+
if (configRaw === null) {
|
|
330
|
+
throw Object.assign(new Error(`ENOENT: no such file or directory, open '${configPath}'`), { code: 'ENOENT' });
|
|
331
|
+
}
|
|
328
332
|
const configParsed = JSON.parse(configRaw);
|
|
329
333
|
if (!configParsed['workflow'])
|
|
330
334
|
configParsed['workflow'] = {};
|
|
@@ -48,7 +48,7 @@ const secrets_cjs_1 = require("./secrets.cjs");
|
|
|
48
48
|
const scanPhasePlans = require("./plan-scan.cjs");
|
|
49
49
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-document.cjs is an export= CommonJS module
|
|
50
50
|
const planDocument = require("./plan-document.cjs");
|
|
51
|
-
const
|
|
51
|
+
const state_transition_cjs_1 = require("./state-transition.cjs");
|
|
52
52
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
53
53
|
const host_runtime_detection_cjs_1 = require("./host-runtime-detection.cjs");
|
|
54
54
|
const security_cjs_1 = require("./security.cjs");
|
|
@@ -99,7 +99,7 @@ const { resolveModelInternal, resolveGranularityInternal, assertValidGranularity
|
|
|
99
99
|
const { phaseDirectoryOptions, findPhaseInternal, listMilestonePhaseDirs, listAllPhaseDirs, } = phaseLocator;
|
|
100
100
|
const { getRoadmapPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
|
|
101
101
|
const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils;
|
|
102
|
-
const { comparePhaseNum, phaseDirectoryName, customPhaseQuery, customDirectoryOwner, phaseScanTokenSource, phaseKeyFromToken, phaseDirectoryContext, normalizePhaseName, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, PHASE_DEP_REF_SOURCE, extractPhaseDependencyTokens, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase, renderPhaseBranchName, parsePhaseId, renderPhaseId, bracketQualifiedKey, phaseHeadingPrefixSrcFor, parsePhaseChecklistLine, PHASE_HEADING_BASELINE, buildPhaseHeadingScanRegex, } = phaseId;
|
|
102
|
+
const { comparePhaseNum, phaseDirectoryName, customPhaseQuery, customDirectoryOwner, phaseScanTokenSource, phaseNumberTokenSource, phaseKeyFromToken, phaseDirectoryContext, normalizePhaseName, stripProjectCodePrefix, PHASE_NUMBER_TOKEN_SOURCE, PHASE_DEP_REF_SOURCE, extractPhaseDependencyTokens, isForeignPrefixedPhaseQuery, isSentinelPhaseId, extractPhaseToken, scopeToPhase, renderPhaseBranchName, parsePhaseId, renderPhaseId, bracketQualifiedKey, phaseHeadingPrefixSrcFor, parsePhaseChecklistLine, PHASE_HEADING_BASELINE, buildPhaseHeadingScanRegex, } = phaseId;
|
|
103
103
|
const { phaseToken } = phaseIdDisplayMod;
|
|
104
104
|
const { pruneOrphanedWorktrees } = worktreeSafety;
|
|
105
105
|
const { planningPaths, planningDir, planningRoot, todosDir, listAvailableWorkstreams, peekActiveWorkstream, resolveEnvWorkstream, diagnoseUnresolvedActiveWorkstream, describeUnresolvedWorkstreamReason, findContextMdIn, findPhaseSpecIn, resolvePhaseIdConvention, worktreesOptedOut, } = planningWorkspace;
|
|
@@ -540,7 +540,10 @@ function detectHasPriorPhases(cwd, phaseInfo) {
|
|
|
540
540
|
*/
|
|
541
541
|
function readConfigJsonBoolean(cwd, keyPath) {
|
|
542
542
|
try {
|
|
543
|
-
|
|
543
|
+
// Bounded read (#5759): a FIFO/device/oversized config throws and degrades to false below.
|
|
544
|
+
const raw = configLoader.readConfigFileBounded(node_path_1.default.join(planningDir(cwd), 'config.json'));
|
|
545
|
+
if (raw === null)
|
|
546
|
+
return false;
|
|
544
547
|
let cursor = JSON.parse(raw);
|
|
545
548
|
for (const segment of keyPath) {
|
|
546
549
|
if (cursor === null || typeof cursor !== 'object' || Array.isArray(cursor))
|
|
@@ -576,7 +579,10 @@ function readNonEmptyFileOrNull(filePath) {
|
|
|
576
579
|
*/
|
|
577
580
|
function readConfigJsonValue(cwd, keyPath) {
|
|
578
581
|
try {
|
|
579
|
-
|
|
582
|
+
// Bounded read (#5759): a FIFO/device/oversized config throws and degrades to undefined below.
|
|
583
|
+
const raw = configLoader.readConfigFileBounded(node_path_1.default.join(planningDir(cwd), 'config.json'));
|
|
584
|
+
if (raw === null)
|
|
585
|
+
return undefined;
|
|
580
586
|
let cursor = JSON.parse(raw);
|
|
581
587
|
for (const segment of keyPath) {
|
|
582
588
|
if (cursor === null || typeof cursor !== 'object' || Array.isArray(cursor))
|
|
@@ -1058,8 +1064,7 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
1058
1064
|
const stateWarnings = [];
|
|
1059
1065
|
if (phaseInfo?.['directory'] && node_fs_1.default.existsSync(node_path_1.default.join(cwd, phaseInfo['directory']))) {
|
|
1060
1066
|
const diskPlans = listPhasePlanFiles(node_path_1.default.join(cwd, phaseInfo['directory'])).length;
|
|
1061
|
-
const
|
|
1062
|
-
const totalPlansInPhase = totalPlansRaw ? parseInt(totalPlansRaw, 10) : null;
|
|
1067
|
+
const totalPlansInPhase = (0, state_transition_cjs_1.readRecordedPlanTotal)(stateContent);
|
|
1063
1068
|
if (totalPlansInPhase !== null && diskPlans !== totalPlansInPhase) {
|
|
1064
1069
|
stateWarnings.push(`Plan count mismatch: STATE.md says ${totalPlansInPhase}, disk has ${diskPlans}`);
|
|
1065
1070
|
}
|
|
@@ -1286,8 +1291,7 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
1286
1291
|
if (stateContent !== null) {
|
|
1287
1292
|
const stateWarnings = [];
|
|
1288
1293
|
result['state_validation_ran'] = true;
|
|
1289
|
-
const
|
|
1290
|
-
const totalPlansInPhase = totalPlansRaw ? parseInt(totalPlansRaw, 10) : null;
|
|
1294
|
+
const totalPlansInPhase = (0, state_transition_cjs_1.readRecordedPlanTotal)(stateContent);
|
|
1291
1295
|
if (totalPlansInPhase !== null &&
|
|
1292
1296
|
phaseInfo &&
|
|
1293
1297
|
totalPlansInPhase !==
|
|
@@ -2626,7 +2630,7 @@ function cmdInitManager(cwd, raw) {
|
|
|
2626
2630
|
}
|
|
2627
2631
|
}
|
|
2628
2632
|
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
2629
|
-
const phasePattern = new RegExp(`#{2,4}\\s*${phaseHeadingPrefix}(${phaseScanTokenSource(directoryContext)})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
|
|
2633
|
+
const phasePattern = new RegExp(`#{2,4}\\s*${phaseHeadingPrefix}(${phaseScanTokenSource(directoryContext, phaseIdConvention)})(?:\\s*\\([^)\\n]{0,200}\\))?\\s*:\\s*([^\\n]+)`, 'gi');
|
|
2630
2634
|
const phases = [];
|
|
2631
2635
|
const bracketIdsByPhaseNumber = new Map();
|
|
2632
2636
|
let match;
|
|
@@ -2635,9 +2639,7 @@ function cmdInitManager(cwd, raw) {
|
|
|
2635
2639
|
const phaseNum = capturesBracketId ? match[2] : match[1];
|
|
2636
2640
|
if (bracketId)
|
|
2637
2641
|
bracketIdsByPhaseNumber.set(phaseNum, bracketId);
|
|
2638
|
-
const phaseName = (capturesBracketId ? match[3] : match[2])
|
|
2639
|
-
.replace(/\(INSERTED\)/i, '')
|
|
2640
|
-
.trim();
|
|
2642
|
+
const phaseName = (0, phase_entry_grammar_cjs_1.stripInsertedMarker)(capturesBracketId ? match[3] : match[2]);
|
|
2641
2643
|
let displayId;
|
|
2642
2644
|
if (bracketId) {
|
|
2643
2645
|
try {
|
|
@@ -2650,7 +2652,7 @@ function cmdInitManager(cwd, raw) {
|
|
|
2650
2652
|
}
|
|
2651
2653
|
const sectionStart = match.index;
|
|
2652
2654
|
const restOfContent = content.slice(sectionStart);
|
|
2653
|
-
const nextHeader = restOfContent.match(new RegExp(`\\n#{2,4}\\s+${phaseHeadingPrefixNoCapture}${phaseScanTokenSource(directoryContext)}`, 'i'));
|
|
2655
|
+
const nextHeader = restOfContent.match(new RegExp(`\\n#{2,4}\\s+${phaseHeadingPrefixNoCapture}${phaseScanTokenSource(directoryContext, phaseIdConvention)}`, 'i'));
|
|
2654
2656
|
const sectionEnd = nextHeader
|
|
2655
2657
|
? sectionStart + nextHeader.index
|
|
2656
2658
|
: content.length;
|
|
@@ -2849,7 +2851,7 @@ function cmdInitManager(cwd, raw) {
|
|
|
2849
2851
|
}
|
|
2850
2852
|
else {
|
|
2851
2853
|
// Preserve the historical greedy non-bracket fallback byte-for-byte.
|
|
2852
|
-
const _allCompletedPattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`.*${phaseHeadingPrefixNoCapture}(${
|
|
2854
|
+
const _allCompletedPattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`.*${phaseHeadingPrefixNoCapture}(${phaseNumberTokenSource(phaseIdConvention)})[:\\s]`, 'gi');
|
|
2853
2855
|
let _allMatch;
|
|
2854
2856
|
while ((_allMatch = _allCompletedPattern.exec(rawContent)) !== null) {
|
|
2855
2857
|
if (!(0, phase_entry_grammar_cjs_1.isPhaseEntryChecked)(_allMatch[0]))
|
|
@@ -3433,9 +3435,9 @@ function cmdInitProgress(cwd, raw, options = {}) {
|
|
|
3433
3435
|
const roadmapPhaseNums = new Set();
|
|
3434
3436
|
const roadmapPhaseNames = new Map();
|
|
3435
3437
|
const roadmapCheckboxStates = new Map();
|
|
3438
|
+
const progressConvention = resolvePhaseIdConvention(cwd);
|
|
3436
3439
|
try {
|
|
3437
3440
|
const roadmapContent = extractCurrentMilestone(node_fs_1.default.readFileSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'), 'utf-8'), cwd);
|
|
3438
|
-
const progressConvention = resolvePhaseIdConvention(cwd);
|
|
3439
3441
|
// #1729: `(?:\s*\([^)\n]{0,200}\))?` tolerates a pre-colon ( ) tag, owned by
|
|
3440
3442
|
// buildPhaseHeadingScanRegex (phase-id.cts) so this scan also recognizes
|
|
3441
3443
|
// bracket-convention headings instead of hand-rolling a literal `Phase\s+`.
|
|
@@ -3443,7 +3445,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
|
|
|
3443
3445
|
let hm;
|
|
3444
3446
|
while ((hm = headingPattern.exec(roadmapContent)) !== null) {
|
|
3445
3447
|
roadmapPhaseNums.add(hm[hNumGroup]);
|
|
3446
|
-
roadmapPhaseNames.set(hm[hNumGroup],
|
|
3448
|
+
roadmapPhaseNames.set(hm[hNumGroup], (0, phase_entry_grammar_cjs_1.stripInsertedMarker)(hm[hNameGroup]));
|
|
3447
3449
|
}
|
|
3448
3450
|
// #4906 Phase 5 (#4984): NOT migrated onto phaseHeadingPrefixSrcFor — same
|
|
3449
3451
|
// out-of-scope reasoning as roadmapPhaseMatchesExactPrefix/detectPhaseMvpMode
|
|
@@ -3472,7 +3474,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
|
|
|
3472
3474
|
// matched (by absorbing the bracket text). This does NOT migrate the site
|
|
3473
3475
|
// onto phaseHeadingPrefixSrcFor / the bracket convention — that stays
|
|
3474
3476
|
// out of scope per the comment above (#4984 revert, pinned census).
|
|
3475
|
-
const cbPattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`(?:\\[[^\\]\\n]{0,200}\\]\\s*)?Phase\\s+(${phaseScanTokenSource(directoryContext)})[:\\s]`, 'gim', { state: 'captured' });
|
|
3477
|
+
const cbPattern = (0, phase_entry_grammar_cjs_1.phaseEntryRegExp)(`(?:\\[[^\\]\\n]{0,200}\\]\\s*)?Phase\\s+(${phaseScanTokenSource(directoryContext, progressConvention)})[:\\s]`, 'gim', { state: 'captured' });
|
|
3476
3478
|
let cbm;
|
|
3477
3479
|
while ((cbm = cbPattern.exec(roadmapContent)) !== null) {
|
|
3478
3480
|
const cbKey = cbm[2];
|
|
@@ -3494,7 +3496,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
|
|
|
3494
3496
|
// behavior-preserving).
|
|
3495
3497
|
const dirs = listMilestonePhaseDirs(phasesDir, { cwd }).value;
|
|
3496
3498
|
for (const dir of dirs) {
|
|
3497
|
-
const dirMatch = dir.match(new RegExp(`^(${phaseScanTokenSource(directoryContext)})-?(.*)`, 'i'));
|
|
3499
|
+
const dirMatch = dir.match(new RegExp(`^(${phaseScanTokenSource(directoryContext, progressConvention)})-?(.*)`, 'i'));
|
|
3498
3500
|
const query = customPhaseQuery(dir, directoryContext);
|
|
3499
3501
|
const owner = query && customDirectoryOwner(dir, query);
|
|
3500
3502
|
const phaseNumber = owner ? owner.id : dirMatch ? dirMatch[1] : dir;
|
|
@@ -36,9 +36,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
37
37
|
const node_path_1 = __importDefault(require("node:path"));
|
|
38
38
|
const runtime_homes_cjs_1 = require("./runtime-homes.cjs");
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- config-file-read.cjs is an export= CommonJS module
|
|
40
|
+
const configFileReadMod = require("./config-file-read.cjs");
|
|
41
|
+
const { unboundedConfigFileReason, printablePath, readConfigFileBounded } = configFileReadMod;
|
|
39
42
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- install-fs-adapter.cjs is an export= CommonJS module
|
|
40
43
|
const installFsAdapter = require("./install-fs-adapter.cjs");
|
|
41
|
-
const { installFs } = installFsAdapter;
|
|
44
|
+
const { installFs, isRealInstallFs } = installFsAdapter;
|
|
42
45
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- model-resolver.cjs is an export= CommonJS module
|
|
43
46
|
const modelResolver = require("./model-resolver.cjs");
|
|
44
47
|
const { EFFORT_SET: GSD_EFFORT_SET } = modelResolver;
|
|
@@ -54,19 +57,36 @@ const { mergeEffortTierDefaults } = modelCatalog;
|
|
|
54
57
|
function _readGsdConfigFile(absPath, label) {
|
|
55
58
|
if (!installFs().existsSync(absPath))
|
|
56
59
|
return null;
|
|
60
|
+
// #5759: refuse a FIFO/device/socket or an oversized file BEFORE the adapter's blocking read
|
|
61
|
+
// (shared guard; a path that is not on the real fs is left to the adapter).
|
|
57
62
|
let raw;
|
|
58
63
|
try {
|
|
59
|
-
|
|
64
|
+
if (isRealInstallFs()) {
|
|
65
|
+
// The real fs: the descriptor-bound reader (non-blocking open, fstat, cap).
|
|
66
|
+
const text = readConfigFileBounded(absPath);
|
|
67
|
+
if (text === null)
|
|
68
|
+
return null;
|
|
69
|
+
raw = text;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// An injected adapter cannot give a descriptor: stat the real path first (best effort).
|
|
73
|
+
const unbounded = unboundedConfigFileReason(absPath);
|
|
74
|
+
if (unbounded !== null) {
|
|
75
|
+
process.stderr.write(`gsd: warning — could not read ${label} (${printablePath(absPath)}): ${unbounded}\n`);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
raw = installFs().readFileSync(absPath, 'utf-8');
|
|
79
|
+
}
|
|
60
80
|
}
|
|
61
81
|
catch (err) {
|
|
62
|
-
process.stderr.write(`gsd: warning — could not read ${label} (${absPath}): ${err.message}\n`);
|
|
82
|
+
process.stderr.write(`gsd: warning — could not read ${label} (${printablePath(absPath)}): ${printablePath(err.message)}\n`);
|
|
63
83
|
return null;
|
|
64
84
|
}
|
|
65
85
|
try {
|
|
66
86
|
return JSON.parse(raw);
|
|
67
87
|
}
|
|
68
88
|
catch (err) {
|
|
69
|
-
process.stderr.write(`gsd: warning — invalid JSON in ${label} (${absPath}): ${err.message}\n`);
|
|
89
|
+
process.stderr.write(`gsd: warning — invalid JSON in ${label} (${printablePath(absPath)}): ${printablePath(err.message)}\n`);
|
|
70
90
|
return null;
|
|
71
91
|
}
|
|
72
92
|
}
|
|
@@ -122,7 +122,7 @@ function pruneEmptyCorpusParents(start, stop) {
|
|
|
122
122
|
}
|
|
123
123
|
function syncRuntimeSurfaceCorpus(source, destination, configDir, manifestPrefix) {
|
|
124
124
|
if (hasExistingSymlinkBetween(node_path_1.default.resolve(configDir), destination, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
125
|
-
throw new Error(
|
|
125
|
+
throw new Error(runtimeArtifactInstallPlan.syncRuntimeSurfaceCorpusSymlinkRefusalMessage(destination, configDir));
|
|
126
126
|
}
|
|
127
127
|
assertCorpusTreeHasNoSymlinks(destination);
|
|
128
128
|
// Remove only paths the previous manifest proves GSD owned and which the
|
|
@@ -160,15 +160,7 @@ function syncRuntimeSurfaceCorpus(source, destination, configDir, manifestPrefix
|
|
|
160
160
|
* The compatibility marker does not replace that installed-corpus authority.
|
|
161
161
|
*/
|
|
162
162
|
function provisionRuntimeSurfaceCorpus(layout, configDir, scope) {
|
|
163
|
-
const required =
|
|
164
|
-
if ((0, install_scope_cjs_1.isGlobalScope)(scope)) {
|
|
165
|
-
for (const kind of layout.kinds) {
|
|
166
|
-
if (kind.kind === 'commands' || kind.kind === 'skills')
|
|
167
|
-
required.add('commands');
|
|
168
|
-
if (kind.kind === 'agents' || kind.kind === 'kimi-agents')
|
|
169
|
-
required.add('agents');
|
|
170
|
-
}
|
|
171
|
-
}
|
|
163
|
+
const required = runtimeArtifactInstallPlan.requiredRuntimeSurfaceSources(layout, scope);
|
|
172
164
|
if (required.size === 0)
|
|
173
165
|
return;
|
|
174
166
|
const corpusRoot = node_path_1.default.join(configDir, 'gsd-core');
|
|
@@ -229,11 +221,8 @@ const USER_OWNED_ARTIFACTS = ['USER-PROFILE.md'];
|
|
|
229
221
|
* with the computed pathPrefix for the install.
|
|
230
222
|
*/
|
|
231
223
|
function applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix) {
|
|
232
|
-
|
|
233
|
-
content =
|
|
234
|
-
content = content.replace(/\.\/\.claude\//g, `./${getDirName(runtime)}/`);
|
|
235
|
-
content = content.replace(/~\/\.opencode\//g, pathPrefix);
|
|
236
|
-
content = content.replace(/~\/\.kilo\//g, pathPrefix);
|
|
224
|
+
// #5715: one pass with literal replacement (see rewriteOpencodeFamilyConfigDirForms).
|
|
225
|
+
content = runtimeArtifactConversion._rewriteOpencodeFamilyConfigDirForms(content, pathPrefix, getDirName(runtime));
|
|
237
226
|
// #5637: the typed-text capture heredoc ends at a terminator the typed text cannot know.
|
|
238
227
|
return (0, args_terminator_cjs_1.stampInstalledContent)(content);
|
|
239
228
|
}
|
|
@@ -560,7 +549,7 @@ function _resolveDevPreferencesSkillTarget(targetDir, runtime, scope = 'global')
|
|
|
560
549
|
// legacy dev-preferences migration lands in the SAME tree the installer
|
|
561
550
|
// and surface-apply use. Runtimes with no `home` override are unaffected.
|
|
562
551
|
hasHomeOverride = skillsKindEntry.home != null;
|
|
563
|
-
installRoot =
|
|
552
|
+
installRoot = runtimeArtifactInstallPlan.resolveRuntimeArtifactInstallRoot(targetDir, skillsKindEntry);
|
|
564
553
|
skillDir = node_path_1.default.join(runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, skillsKindEntry.destSubpath), stemName);
|
|
565
554
|
}
|
|
566
555
|
else {
|
|
@@ -669,7 +658,7 @@ function _copyStaged(stagedDir, destDir, kind, configDir, runtime) {
|
|
|
669
658
|
// that case this defense-in-depth check must confine against the resolved
|
|
670
659
|
// alternate root instead, matching the upstream gate's own root selection in
|
|
671
660
|
// createRuntimeArtifactInstallPlan.
|
|
672
|
-
const installRoot = (
|
|
661
|
+
const installRoot = runtimeArtifactInstallPlan.resolveRuntimeArtifactInstallRoot(configDir, kind);
|
|
673
662
|
// Strict-subpath + NUL containment via the canonical gate (shared with the
|
|
674
663
|
// layout-driven install plan); throws if destDir escapes the install root.
|
|
675
664
|
// destDir here is an absolute path; path.resolve(installRoot, absoluteDest) returns it unchanged, so the gate's strict-subpath check still correctly confines it to installRoot.
|
|
@@ -1303,7 +1292,7 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, res
|
|
|
1303
1292
|
// previously lived inline in bin/install.js.
|
|
1304
1293
|
const behaviors = hostBehaviorsFor(runtime);
|
|
1305
1294
|
const projectDir = scope === 'global' ? process.cwd() : configDir;
|
|
1306
|
-
if (behaviors
|
|
1295
|
+
if (runtimeArtifactInstallPlan.shouldInstallCombinedFamily(behaviors)) {
|
|
1307
1296
|
// #2329: combined-family runtimes (OpenCode/Kilo) bypass
|
|
1308
1297
|
// _runLegacyInstallMigrations below entirely (early return), so their
|
|
1309
1298
|
// legacy-directory cleanup needs its own pre-materialization hook here.
|
|
@@ -1381,12 +1370,12 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, res
|
|
|
1381
1370
|
// A fake adapter can change what those probes observe for paths that
|
|
1382
1371
|
// were never real to begin with; it cannot make this `if` pass for a
|
|
1383
1372
|
// path the real filesystem would refuse.
|
|
1384
|
-
const installRoot = (
|
|
1373
|
+
const installRoot = runtimeArtifactInstallPlan.resolveRuntimeArtifactInstallRoot(configDir, kind);
|
|
1385
1374
|
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
|
1386
1375
|
// Threat model from #1704 / ADR-1239 Phase B preserved: path-traversal and
|
|
1387
1376
|
// resolved-target-equals-root still refuse regardless of opt-in.
|
|
1388
1377
|
if (hasExistingSymlinkBetween(node_path_1.default.resolve(installRoot), dest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
1389
|
-
throw new Error(
|
|
1378
|
+
throw new Error(runtimeArtifactInstallPlan.installRuntimeArtifactsSymlinkRefusalMessage(dest, installRoot));
|
|
1390
1379
|
}
|
|
1391
1380
|
// #2875 defect fix (--minimal regression closed): a restricted profile
|
|
1392
1381
|
// (e.g. --minimal) can legitimately stage ZERO agents — no skill in
|
|
@@ -1580,6 +1569,7 @@ function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPre
|
|
|
1580
1569
|
const skillsKindEntry = layout.kinds.find((k) => k.kind === 'skills');
|
|
1581
1570
|
if (!skillsKindEntry)
|
|
1582
1571
|
return 0;
|
|
1572
|
+
const installRoot = runtimeArtifactInstallPlan.resolveRuntimeArtifactInstallRoot(targetDir, skillsKindEntry);
|
|
1583
1573
|
// #3712: combined-family runtimes take installRuntimeArtifacts' early return
|
|
1584
1574
|
// BEFORE its guard runs, and this writer honors `skillsKindEntry.home` below and
|
|
1585
1575
|
// then prunes that destination. opencode/kilo declare no `home` today, so there
|
|
@@ -1606,13 +1596,12 @@ function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPre
|
|
|
1606
1596
|
// OpenCode/Kilo writer lands in the SAME tree the installer and surface-apply use.
|
|
1607
1597
|
// Runtimes with no `home` override (opencode, kilo today) are unaffected. Must stay
|
|
1608
1598
|
// in lockstep with the sibling writers — the destination-parity test enforces it.
|
|
1609
|
-
const installRoot = skillsKindEntry.home ?? targetDir;
|
|
1610
1599
|
const dest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, skillsKindEntry.destSubpath);
|
|
1611
1600
|
// Symlink-escape guard: reject if any path component between installRoot and
|
|
1612
1601
|
// dest is a symlink that would redirect writes outside the install root.
|
|
1613
1602
|
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
|
1614
1603
|
if (hasExistingSymlinkBetween(node_path_1.default.resolve(installRoot), dest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
1615
|
-
throw new Error(
|
|
1604
|
+
throw new Error(runtimeArtifactInstallPlan.installOpencodeFamilySkillsSymlinkRefusalMessage(dest, installRoot));
|
|
1616
1605
|
}
|
|
1617
1606
|
installFs().mkdirSync(dest, { recursive: true });
|
|
1618
1607
|
// Preserve user-owned GSD-prefixed skill dirs across the gsd-* prune.
|
|
@@ -1653,7 +1642,8 @@ function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPre
|
|
|
1653
1642
|
let content = installFs().readFileSync(node_path_1.default.join(rawDir, entry.name), 'utf8');
|
|
1654
1643
|
content = applyOpencodeFamilyPathPrefix(content, runtime, pathPrefix);
|
|
1655
1644
|
content = processAttribution(content, resolveAttribution(runtime));
|
|
1656
|
-
|
|
1645
|
+
// #5715: the converter runs after the prefix pass; shield the emitted prefix from it.
|
|
1646
|
+
content = runtimeArtifactConversion._shieldEmittedPrefix(content, pathPrefix, (c) => converter(c, skillName));
|
|
1657
1647
|
const skillDir = node_path_1.default.join(dest, skillName);
|
|
1658
1648
|
installFs().mkdirSync(skillDir, { recursive: true });
|
|
1659
1649
|
installFs().writeFileSync(node_path_1.default.join(skillDir, 'SKILL.md'), content);
|
|
@@ -1800,14 +1790,14 @@ function installAgentsKindStandalone(runtime, targetDir, scope, resolvedProfile,
|
|
|
1800
1790
|
const stagedAgentFiles = installFs().existsSync(stagedDir)
|
|
1801
1791
|
? installFs().readdirSync(stagedDir).filter((f) => f.endsWith('.md'))
|
|
1802
1792
|
: [];
|
|
1803
|
-
const installRoot = (
|
|
1793
|
+
const installRoot = runtimeArtifactInstallPlan.resolveRuntimeArtifactInstallRoot(targetDir, agentsKindEntry);
|
|
1804
1794
|
const dest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, agentsKindEntry.destSubpath);
|
|
1805
1795
|
// Symlink-escape guard — same gate _copyStaged/installOpencodeFamilySkills apply
|
|
1806
1796
|
// to their own writes (#2393 GSD_ALLOW_SYMLINKED_DEST opt-in preserved). Runs
|
|
1807
1797
|
// even when nothing will be written this call — the stale-agent prune below
|
|
1808
1798
|
// (`_removeGsdEntries`) still touches `dest` whenever it already exists.
|
|
1809
1799
|
if (hasExistingSymlinkBetween(node_path_1.default.resolve(installRoot), dest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
1810
|
-
throw new Error(
|
|
1800
|
+
throw new Error(runtimeArtifactInstallPlan.installAgentsKindStandaloneSymlinkRefusalMessage(dest, installRoot));
|
|
1811
1801
|
}
|
|
1812
1802
|
// #2875 defect fix, corrected: the ORIGINAL fix returned `null` (no-op)
|
|
1813
1803
|
// whenever a restricted profile (e.g. --minimal) staged ZERO agents,
|
|
@@ -1881,9 +1871,11 @@ function installOpencodeFamilyCommands(runtime, destDir, srcDir, pathPrefix, res
|
|
|
1881
1871
|
// already satisfies the fold-to-descriptor requirement. Only the SKILLS
|
|
1882
1872
|
// converter site above (installOpencodeFamilySkills) has a real
|
|
1883
1873
|
// `converter` string to key on via SKILLS_CONVERTER_REGISTRY.
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1874
|
+
// #5715: the converter runs after the prefix pass; shield the emitted prefix from it.
|
|
1875
|
+
const frontmatterConverter = hostBehaviorsFor(runtime).frontmatterDialect === 'kilo'
|
|
1876
|
+
? (c) => runtimeArtifactConversion.convertClaudeToKiloFrontmatter(c)
|
|
1877
|
+
: (c) => runtimeArtifactConversion.convertClaudeToOpencodeFrontmatter(c);
|
|
1878
|
+
content = runtimeArtifactConversion._shieldEmittedPrefix(content, pathPrefix, frontmatterConverter);
|
|
1887
1879
|
installFs().writeFileSync(node_path_1.default.join(destDir, destName), content);
|
|
1888
1880
|
}
|
|
1889
1881
|
}
|
|
@@ -259,4 +259,8 @@ function mkInstallTempDir(prefix) {
|
|
|
259
259
|
installFs().mkdirSync(dir, { recursive: true });
|
|
260
260
|
return dir;
|
|
261
261
|
}
|
|
262
|
-
|
|
262
|
+
/** True when no fake adapter is active, i.e. installFs() is the real `node:fs` (#5759). */
|
|
263
|
+
function isRealInstallFs() {
|
|
264
|
+
return current === REAL_ADAPTER;
|
|
265
|
+
}
|
|
266
|
+
module.exports = { installFs, withInstallFs, mkInstallTempDir, isRealInstallFs };
|