@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
|
@@ -14,6 +14,8 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
14
14
|
const text_lines_cjs_1 = require("./text-lines.cjs");
|
|
15
15
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
16
16
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
17
|
+
const phase_entry_grammar_cjs_1 = require("./phase-entry-grammar.cjs");
|
|
18
|
+
const install_manifest_record_cjs_1 = require("./install-manifest-record.cjs");
|
|
17
19
|
const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
|
|
18
20
|
const security_cjs_1 = require("./security.cjs");
|
|
19
21
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -835,6 +837,7 @@ function cmdEffortSync(cwd, raw, opts) {
|
|
|
835
837
|
// cmdEffortSyncCodex and cmdEffortSyncOpencode, so a fault mid-write can
|
|
836
838
|
// never leave an agent file truncated or empty.
|
|
837
839
|
let fileFailureCount = 0;
|
|
840
|
+
const rewrittenFiles = [];
|
|
838
841
|
for (const file of files) {
|
|
839
842
|
const agentName = file.replace(/\.md$/, '');
|
|
840
843
|
const filePath = node_path_1.default.join(agentsDir, file);
|
|
@@ -852,6 +855,8 @@ function cmdEffortSync(cwd, raw, opts) {
|
|
|
852
855
|
fileFailureCount++;
|
|
853
856
|
continue;
|
|
854
857
|
}
|
|
858
|
+
// #5683: hash BEFORE any rewrite — only a GSD-owned file gets re-recorded.
|
|
859
|
+
const preHash = (0, install_manifest_record_cjs_1.tryFileHash)(filePath);
|
|
855
860
|
// Resolve using install-time logic: home defaults merged with project config.
|
|
856
861
|
const universalEffort = resolveInstallTimeEffort(effortCfg, agentName);
|
|
857
862
|
// #3533 (10d): 'inherit' means the key must NOT exist. An absent key is
|
|
@@ -915,6 +920,7 @@ function cmdEffortSync(cwd, raw, opts) {
|
|
|
915
920
|
}
|
|
916
921
|
catch { /* non-fatal: proceed with default tmp-file mode */ }
|
|
917
922
|
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPathInherit, filePath);
|
|
923
|
+
rewrittenFiles.push({ path: filePath, preHash });
|
|
918
924
|
}
|
|
919
925
|
catch {
|
|
920
926
|
try {
|
|
@@ -992,6 +998,7 @@ function cmdEffortSync(cwd, raw, opts) {
|
|
|
992
998
|
}
|
|
993
999
|
catch { /* non-fatal: proceed with default tmp-file mode */ }
|
|
994
1000
|
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPathSet, filePath);
|
|
1001
|
+
rewrittenFiles.push({ path: filePath, preHash });
|
|
995
1002
|
}
|
|
996
1003
|
catch {
|
|
997
1004
|
try {
|
|
@@ -1006,6 +1013,9 @@ function cmdEffortSync(cwd, raw, opts) {
|
|
|
1006
1013
|
changes.push({ agent: agentName, from: currentEffort, to: newEffortValue });
|
|
1007
1014
|
synced++;
|
|
1008
1015
|
}
|
|
1016
|
+
// #5683: keep the install manifest in step with the agents just rewritten.
|
|
1017
|
+
if (!dryRun)
|
|
1018
|
+
(0, install_manifest_record_cjs_1.recordManifestFileHashes)(node_path_1.default.dirname(agentsDir), rewrittenFiles);
|
|
1009
1019
|
output({ synced, skipped, changes, dry_run: dryRun, agents_dir: agentsDir }, raw, fileFailureCount > 0 ? 'failed' : synced > 0 ? 'changed' : 'ok');
|
|
1010
1020
|
}
|
|
1011
1021
|
/**
|
|
@@ -1048,6 +1058,7 @@ function cmdEffortSyncCodex(raw, dryRun, configDir) {
|
|
|
1048
1058
|
const refused = [];
|
|
1049
1059
|
const writeFailures = [];
|
|
1050
1060
|
const readFailures = [];
|
|
1061
|
+
const rewrittenFiles = [];
|
|
1051
1062
|
let synced = 0;
|
|
1052
1063
|
let skipped = 0;
|
|
1053
1064
|
for (const file of files) {
|
|
@@ -1066,6 +1077,7 @@ function cmdEffortSyncCodex(raw, dryRun, configDir) {
|
|
|
1066
1077
|
readFailures.push({ agent: agentName, file: filePath, error: err instanceof Error ? err.message : String(err) });
|
|
1067
1078
|
continue;
|
|
1068
1079
|
}
|
|
1080
|
+
const preHash = (0, install_manifest_record_cjs_1.tryFileHash)(filePath); // #5683
|
|
1069
1081
|
const parsed = (0, codex_agent_toml_cjs_1.parseCodexAgentToml)(content);
|
|
1070
1082
|
if (!parsed.ok) {
|
|
1071
1083
|
// Never partially rewritten (40-design.md, ADR-2313 reader/writer
|
|
@@ -1134,6 +1146,7 @@ function cmdEffortSyncCodex(raw, dryRun, configDir) {
|
|
|
1134
1146
|
}
|
|
1135
1147
|
catch { /* non-fatal: proceed with default tmp-file mode */ }
|
|
1136
1148
|
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPath, filePath);
|
|
1149
|
+
rewrittenFiles.push({ path: filePath, preHash });
|
|
1137
1150
|
}
|
|
1138
1151
|
catch (err) {
|
|
1139
1152
|
// Reported, not thrown — the remaining agents still get processed.
|
|
@@ -1150,6 +1163,8 @@ function cmdEffortSyncCodex(raw, dryRun, configDir) {
|
|
|
1150
1163
|
changes.push(...pendingChanges);
|
|
1151
1164
|
synced++;
|
|
1152
1165
|
}
|
|
1166
|
+
if (!dryRun)
|
|
1167
|
+
(0, install_manifest_record_cjs_1.recordManifestFileHashes)(node_path_1.default.dirname(agentsDir), rewrittenFiles);
|
|
1153
1168
|
output({ synced, skipped, changes, dry_run: dryRun, agents_dir: agentsDir, refused, write_failures: writeFailures, read_failures: readFailures }, raw, writeFailures.length > 0 || readFailures.length > 0 ? 'failed' : synced > 0 ? 'changed' : 'ok');
|
|
1154
1169
|
}
|
|
1155
1170
|
/**
|
|
@@ -1198,6 +1213,7 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
|
|
|
1198
1213
|
const changes = [];
|
|
1199
1214
|
const writeFailures = [];
|
|
1200
1215
|
const readFailures = [];
|
|
1216
|
+
const rewrittenFiles = [];
|
|
1201
1217
|
let synced = 0;
|
|
1202
1218
|
let skipped = 0;
|
|
1203
1219
|
for (const file of files) {
|
|
@@ -1216,6 +1232,7 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
|
|
|
1216
1232
|
readFailures.push({ agent: agentName, file: filePath, error: err instanceof Error ? err.message : String(err) });
|
|
1217
1233
|
continue;
|
|
1218
1234
|
}
|
|
1235
|
+
const preHash = (0, install_manifest_record_cjs_1.tryFileHash)(filePath); // #5683
|
|
1219
1236
|
// `target === null` covers both "no effort configured" (inherit) and "a
|
|
1220
1237
|
// level OpenCode does not accept" — both must produce NO `variant:` key,
|
|
1221
1238
|
// exactly what install writes.
|
|
@@ -1312,6 +1329,7 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
|
|
|
1312
1329
|
}
|
|
1313
1330
|
catch { /* non-fatal: proceed with default tmp-file mode */ }
|
|
1314
1331
|
(0, shell_command_projection_cjs_1.retryRenameSync)(tmpPath, filePath);
|
|
1332
|
+
rewrittenFiles.push({ path: filePath, preHash });
|
|
1315
1333
|
}
|
|
1316
1334
|
catch (err) {
|
|
1317
1335
|
try {
|
|
@@ -1340,6 +1358,8 @@ function cmdEffortSyncOpencode(cwd, raw, dryRun, configDir) {
|
|
|
1340
1358
|
// Making the exit code reflect failure would be a CLI-contract change
|
|
1341
1359
|
// affecting all three cmdEffortSync* branches (claude, codex, opencode) and
|
|
1342
1360
|
// is out of scope for this fix.
|
|
1361
|
+
if (!dryRun)
|
|
1362
|
+
(0, install_manifest_record_cjs_1.recordManifestFileHashes)(node_path_1.default.dirname(agentsDir), rewrittenFiles);
|
|
1343
1363
|
output({ synced, skipped, changes, dry_run: dryRun, agents_dir: agentsDir, write_failures: writeFailures, read_failures: readFailures }, raw, writeFailures.length > 0 || readFailures.length > 0 ? 'failed' : synced > 0 ? 'changed' : 'ok');
|
|
1344
1364
|
}
|
|
1345
1365
|
/**
|
|
@@ -1763,7 +1783,74 @@ function stageDeclaredRemovals(cwd, removedDeclared) {
|
|
|
1763
1783
|
}
|
|
1764
1784
|
return { removedEntries, removedPathspec, failures };
|
|
1765
1785
|
}
|
|
1766
|
-
|
|
1786
|
+
/**
|
|
1787
|
+
* #6043 (ADR-4908 §3): a committing verb commits in the checkout the process
|
|
1788
|
+
* runs in. Returns the typed refusal when it cannot: not a git repository, or
|
|
1789
|
+
* `cwd` / a path to commit resolves outside `ctx.worktreeRoot`.
|
|
1790
|
+
* `cwd` and the worktree root are compared by realpath; a relative path is
|
|
1791
|
+
* resolved lexically against the canonical `cwd`, so a `.planning` symlink
|
|
1792
|
+
* inside the worktree stays inside; an absolute path is canonicalized through
|
|
1793
|
+
* its nearest existing ancestor directory.
|
|
1794
|
+
* A degraded context (git timed out or could not run) makes no claim about the
|
|
1795
|
+
* worktree, so it never refuses.
|
|
1796
|
+
*/
|
|
1797
|
+
function commitRefusal(ctx, cwd, files, filesRemoved) {
|
|
1798
|
+
if (ctx.provenance.reason === 'not_git_repo') {
|
|
1799
|
+
return { reason: 'not_git_repo', message: `commit refused: ${cwd} is not inside a git repository` };
|
|
1800
|
+
}
|
|
1801
|
+
if (ctx.provenance.degraded)
|
|
1802
|
+
return null;
|
|
1803
|
+
const realOrSelf = (p) => {
|
|
1804
|
+
try {
|
|
1805
|
+
return node_fs_1.default.realpathSync.native(p);
|
|
1806
|
+
}
|
|
1807
|
+
catch {
|
|
1808
|
+
return p;
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
// Nearest existing ancestor directory of an absolute path, realpath'd, with
|
|
1812
|
+
// the not-yet-existing remainder re-appended (macOS /var <-> /private/var,
|
|
1813
|
+
// Windows 8.3 short names like RUNNER~1).
|
|
1814
|
+
const canonicalAbsolute = (abs) => {
|
|
1815
|
+
const rest = [node_path_1.default.basename(abs)];
|
|
1816
|
+
let ancestor = node_path_1.default.dirname(abs);
|
|
1817
|
+
for (;;) {
|
|
1818
|
+
try {
|
|
1819
|
+
return node_path_1.default.join(node_fs_1.default.realpathSync.native(ancestor), ...rest);
|
|
1820
|
+
}
|
|
1821
|
+
catch {
|
|
1822
|
+
const parent = node_path_1.default.dirname(ancestor);
|
|
1823
|
+
if (parent === ancestor)
|
|
1824
|
+
return abs;
|
|
1825
|
+
rest.unshift(node_path_1.default.basename(ancestor));
|
|
1826
|
+
ancestor = parent;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
const norm = (p) => {
|
|
1831
|
+
const slashed = p.replace(/\\/g, '/');
|
|
1832
|
+
return process.platform === 'win32' ? slashed.toLowerCase() : slashed;
|
|
1833
|
+
};
|
|
1834
|
+
const inside = (target, root) => (0, security_cjs_1.isContainedIn)(norm(target), norm(root), { sep: '/' });
|
|
1835
|
+
const worktreeRoot = realOrSelf(node_path_1.default.resolve(ctx.worktreeRoot));
|
|
1836
|
+
const realCwd = realOrSelf(node_path_1.default.resolve(cwd));
|
|
1837
|
+
const declared = [...(files ?? []), ...(filesRemoved ?? [])];
|
|
1838
|
+
const targets = declared.length > 0 ? declared : ['.planning/'];
|
|
1839
|
+
// Lexical against the canonical cwd: a `.planning` symlink component is not followed.
|
|
1840
|
+
const resolveTarget = (t) => node_path_1.default.isAbsolute(t) ? canonicalAbsolute(node_path_1.default.resolve(t)) : node_path_1.default.resolve(realCwd, t);
|
|
1841
|
+
const escaping = !inside(realCwd, worktreeRoot)
|
|
1842
|
+
? realCwd
|
|
1843
|
+
: targets.map(resolveTarget).find((abs) => !inside(abs, worktreeRoot));
|
|
1844
|
+
if (escaping === undefined)
|
|
1845
|
+
return null;
|
|
1846
|
+
return {
|
|
1847
|
+
reason: 'outside_worktree',
|
|
1848
|
+
skipped: false,
|
|
1849
|
+
message: `commit refused: ${escaping} is outside the worktree ${worktreeRoot}; ` +
|
|
1850
|
+
'pass the checkout that owns the path with --project-dir to commit there',
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
function cmdCommit(cwd, message, files, raw, amend, noVerify, filesRemoved, ctx) {
|
|
1767
1854
|
if (!message && !amend) {
|
|
1768
1855
|
error('commit message required');
|
|
1769
1856
|
}
|
|
@@ -1793,6 +1880,16 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify, filesRemoved) {
|
|
|
1793
1880
|
output(result, raw, 'skipped');
|
|
1794
1881
|
return;
|
|
1795
1882
|
}
|
|
1883
|
+
// #6043: refuse before branching or staging, and only AFTER the commit_docs
|
|
1884
|
+
// skip above, so a benign skip is never turned into an error.
|
|
1885
|
+
const refusal = ctx ? commitRefusal(ctx, cwd, files, filesRemoved) : null;
|
|
1886
|
+
if (refusal) {
|
|
1887
|
+
error(refusal.message, refusal.reason === 'not_git_repo' ? ERROR_REASON.COMMIT_NOT_GIT_REPO : ERROR_REASON.COMMIT_OUTSIDE_WORKTREE, {
|
|
1888
|
+
committed: false,
|
|
1889
|
+
...(refusal.skipped === false ? { skipped: false } : {}),
|
|
1890
|
+
hash: null,
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1796
1893
|
// Ensure branching strategy branch exists before first commit (#1278).
|
|
1797
1894
|
// Pre-execution workflows (discuss, plan, research) commit artifacts but the branch
|
|
1798
1895
|
// was previously only created during execute-phase — too late.
|
|
@@ -3518,7 +3615,7 @@ function cmdStats(cwd, format, raw) {
|
|
|
3518
3615
|
phasesByNumber.set(key, {
|
|
3519
3616
|
number: key,
|
|
3520
3617
|
...(displayId ? { display_id: displayId } : {}),
|
|
3521
|
-
name:
|
|
3618
|
+
name: (0, phase_entry_grammar_cjs_1.stripInsertedMarker)(phaseName),
|
|
3522
3619
|
plans: 0,
|
|
3523
3620
|
summaries: 0,
|
|
3524
3621
|
phaseStatus: phase_status_cjs_1.PHASE_STATUS.NOT_STARTED,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Config File Read — the single bounded reader for a project or workstream config.json (#5759)
|
|
4
|
+
*
|
|
5
|
+
* A config.json can be planted as a FIFO or device: a plain readFileSync opens it
|
|
6
|
+
* blocking and waits forever for a writer. Every read of a config.json in src/ goes through
|
|
7
|
+
* `readConfigFileBounded`, so the loader, the resolve-model omit check, and the many
|
|
8
|
+
* direct "read one key" helpers cannot diverge on how a non-regular or oversized file is treated.
|
|
9
|
+
*
|
|
10
|
+
* Leaf module: it imports only node:fs and ./capability-ledger.cjs (itself a node-builtins-only leaf), so
|
|
11
|
+
* planning-workspace, config-loader and every other reader can import it without a require cycle.
|
|
12
|
+
*
|
|
13
|
+
* Dependencies (leaf modules only):
|
|
14
|
+
* - ./capability-ledger.cjs (readSmallRegularFile — non-blocking open, fstat regular-file check, size cap)
|
|
15
|
+
*/
|
|
16
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
+
};
|
|
19
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
21
|
+
const capabilityLedgerMod = require("./capability-ledger.cjs");
|
|
22
|
+
const { readSmallRegularFile } = capabilityLedgerMod;
|
|
23
|
+
/**
|
|
24
|
+
* Largest config.json any reader will read (#5759). Real configs are a few KB; 1 MiB
|
|
25
|
+
* is generous enough that no legitimate config is refused. A larger file is reported
|
|
26
|
+
* unusable (EFBIG) rather than read whole.
|
|
27
|
+
*/
|
|
28
|
+
const CONFIG_MAX_BYTES = 1024 * 1024;
|
|
29
|
+
/**
|
|
30
|
+
* Read a config.json as a bounded regular file (#5759). Opens non-blocking and fstats the
|
|
31
|
+
* fd, so a FIFO or device with no writer is refused instead of blocking open(2) forever.
|
|
32
|
+
* Returns the UTF-8 text, or null when the file is absent (ENOENT); throws a LedgerIOError
|
|
33
|
+
* carrying `.code` (ENXIO FIFO/device, EISDIR directory, EFBIG over CONFIG_MAX_BYTES, or the
|
|
34
|
+
* errno) for every other condition.
|
|
35
|
+
*/
|
|
36
|
+
function readConfigFileBounded(filePath) {
|
|
37
|
+
return readSmallRegularFile(filePath, CONFIG_MAX_BYTES);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Why `absPath` — looked up on the REAL file system — must not be read blindly: it exists but is
|
|
41
|
+
* not a regular file (FIFO, device, socket, directory) or is larger than CONFIG_MAX_BYTES.
|
|
42
|
+
* Returns null when it is a regular file within the cap, and also when it cannot be stat'ed
|
|
43
|
+
* (absent, or not on the real file system at all, as with an installer fake-fs adapter), so the
|
|
44
|
+
* caller's own read decides. stat(2) follows links and never opens, so it cannot block. This is
|
|
45
|
+
* the guard for readers that must go through an injected fs seam and so cannot use
|
|
46
|
+
* `readConfigFileBounded`; it is a check-then-read, not the descriptor-bound read. Residual
|
|
47
|
+
* race: the install-fs adapter exposes `openSync`/`readSync` with string flags (no O_NONBLOCK)
|
|
48
|
+
* and no `fstatSync`, so the check cannot be bound to the descriptor that is read; a path
|
|
49
|
+
* swapped for a FIFO between this stat and the adapter's read can still block. Closing that
|
|
50
|
+
* needs `fstatSync` and numeric open flags on the adapter.
|
|
51
|
+
*/
|
|
52
|
+
function unboundedConfigFileReason(absPath) {
|
|
53
|
+
try {
|
|
54
|
+
const st = node_fs_1.default.statSync(absPath);
|
|
55
|
+
if (!st.isFile())
|
|
56
|
+
return 'not a regular file';
|
|
57
|
+
if (st.size > CONFIG_MAX_BYTES)
|
|
58
|
+
return `larger than ${CONFIG_MAX_BYTES} bytes`;
|
|
59
|
+
}
|
|
60
|
+
catch { /* absent or not on the real fs — the caller's read decides */ }
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A path as printed to a terminal: C0 controls, DEL and C1 controls (general category Cc) become
|
|
65
|
+
* `\xNN`; every other format (Cf), line/paragraph separator (Zl, Zp) and lone-surrogate (Cs)
|
|
66
|
+
* character — zero-width and bidirectional controls, soft hyphen, tag characters — becomes
|
|
67
|
+
* `\uNNNN` (`\u{N...}` above the BMP). A directory
|
|
68
|
+
* name can carry ESC (or a C1 CSI), and a path written to stderr verbatim is a terminal-escape
|
|
69
|
+
* injection through that name.
|
|
70
|
+
*/
|
|
71
|
+
function printablePath(p) {
|
|
72
|
+
return p.replace(new RegExp('[\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}\\p{Cs}]', 'gu'), (c) => {
|
|
73
|
+
const code = c.codePointAt(0);
|
|
74
|
+
const hex = code.toString(16).padStart(4, '0');
|
|
75
|
+
if (/\p{Cc}/u.test(c))
|
|
76
|
+
return `\\x${hex.slice(-2)}`; // C0, DEL, C1 (all <= 0xff)
|
|
77
|
+
return code > 0xffff ? `\\u{${hex}}` : `\\u${hex}`; // format, separator, lone surrogate
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
module.exports = {
|
|
81
|
+
CONFIG_MAX_BYTES,
|
|
82
|
+
readConfigFileBounded,
|
|
83
|
+
unboundedConfigFileReason,
|
|
84
|
+
printablePath,
|
|
85
|
+
};
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* - ./unusable-input.cjs (warnUnusableInput, UNUSABLE_REASON — #3760)
|
|
18
18
|
* - ./config-schema.cjs (VALID_CONFIG_KEYS, DYNAMIC_KEY_PATTERNS)
|
|
19
19
|
* - ./planning-workspace.cjs (planningDir, planningRoot)
|
|
20
|
-
* - ./shell-command-projection.cjs (execGit, platformWriteSync
|
|
20
|
+
* - ./shell-command-projection.cjs (execGit, platformWriteSync)
|
|
21
|
+
* - ./config-file-read.cjs (CONFIG_MAX_BYTES, readConfigFileBounded — bounded regular-file read, #5759)
|
|
21
22
|
* - ./core-utils.cjs (detectSubRepos)
|
|
22
23
|
* - ./model-catalog.cjs (KNOWN_RUNTIMES, KNOWN_PROVIDERS, ADAPTIVE_TIER_VALUES)
|
|
23
24
|
*/
|
|
@@ -27,6 +28,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
27
28
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
28
29
|
const node_path_1 = __importDefault(require("node:path"));
|
|
29
30
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
|
+
const configFileReadMod = require("./config-file-read.cjs");
|
|
33
|
+
const { CONFIG_MAX_BYTES, readConfigFileBounded, printablePath } = configFileReadMod;
|
|
30
34
|
// #4717: runtime-identity fill — env rung + per-install marker rung.
|
|
31
35
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
32
36
|
const runtime_homes_cjs_1 = require("./runtime-homes.cjs");
|
|
@@ -533,15 +537,19 @@ const CONFIG_REASON = Object.freeze({
|
|
|
533
537
|
/** The file exists but could not be read (EACCES/EIO/…) — NOT applied. */
|
|
534
538
|
CONFIG_UNREADABLE: 'config_unreadable',
|
|
535
539
|
});
|
|
540
|
+
// CONFIG_MAX_BYTES and readConfigFileBounded live in ./config-file-read.cjs (#5759), the leaf
|
|
541
|
+
// every config.json reader shares; they are re-exported below for existing importers.
|
|
536
542
|
/**
|
|
537
543
|
* Read + JSON-parse a config file, keeping *absent* distinguishable from
|
|
538
|
-
* *unusable*. `
|
|
539
|
-
*
|
|
544
|
+
* *unusable*. `readConfigFileBounded` returns null on ENOENT and throws an error
|
|
545
|
+
* carrying `.code` for every other condition — a FIFO/device (ENXIO), a directory
|
|
546
|
+
* (EISDIR), an oversized file (EFBIG) or an errno — which is the seam that makes
|
|
547
|
+
* this separable at all.
|
|
540
548
|
*/
|
|
541
549
|
function _readConfigFile(filePath) {
|
|
542
550
|
let raw;
|
|
543
551
|
try {
|
|
544
|
-
raw = (
|
|
552
|
+
raw = readConfigFileBounded(filePath);
|
|
545
553
|
}
|
|
546
554
|
catch (err) {
|
|
547
555
|
const code = err.code ?? 'EUNKNOWN';
|
|
@@ -573,14 +581,7 @@ function _readConfigFile(filePath) {
|
|
|
573
581
|
* genuine second failure in a different file.
|
|
574
582
|
*/
|
|
575
583
|
const _warnedUnusableConfig = new Set();
|
|
576
|
-
|
|
577
|
-
* The wiring clause (ADR-1411 amendment). `reason` lives on `ConfigResolution`,
|
|
578
|
-
* but `loadConfig` — the wrapper roughly fifty call sites use — returns
|
|
579
|
-
* `.config` alone and would never surface it. Without this diagnostic the field
|
|
580
|
-
* is unreachable to almost every consumer, and the user whose config was
|
|
581
|
-
* silently discarded still gets no signal. That was the whole defect in #1880.
|
|
582
|
-
*/
|
|
583
|
-
function _warnUnusableConfig(fault) {
|
|
584
|
+
function _warnUnusableConfig(fault, fallback = 'defaults') {
|
|
584
585
|
// The NUL separators are load-bearing: without them `path`+`reason`+`code` is bare
|
|
585
586
|
// concatenation and two distinct faults can key alike. They are written as escapes rather
|
|
586
587
|
// than literal 0x00 bytes because a literal NUL makes the whole file binary to file(1) and
|
|
@@ -594,7 +595,10 @@ function _warnUnusableConfig(fault) {
|
|
|
594
595
|
const what = fault.reason === CONFIG_REASON.CONFIG_UNPARSEABLE
|
|
595
596
|
? 'is not valid JSON'
|
|
596
597
|
: `could not be read (${fault.code})`;
|
|
597
|
-
|
|
598
|
+
const instead = fallback === 'root' ? '; using the root config instead'
|
|
599
|
+
: fallback === 'defaults' ? '; using defaults instead'
|
|
600
|
+
: '';
|
|
601
|
+
process.stderr.write(`gsd-tools: warning: ${printablePath(fault.path)} ${what} — its settings were NOT applied${instead}\n`);
|
|
598
602
|
}
|
|
599
603
|
/**
|
|
600
604
|
* loadConfigResolved — provenance-aware config loading (#1415, ADR-1411 P2).
|
|
@@ -677,7 +681,8 @@ function loadConfigResolvedInternal(cwd, options = {}) {
|
|
|
677
681
|
const rootRead = _readConfigFile(rootConfigPath);
|
|
678
682
|
if (rootRead.kind === 'fault') {
|
|
679
683
|
configFault = rootRead.fault;
|
|
680
|
-
|
|
684
|
+
// The workstream config below may still apply, so no replacement is claimed.
|
|
685
|
+
_warnUnusableConfig(rootRead.fault, null);
|
|
681
686
|
}
|
|
682
687
|
if (rootRead.kind !== 'ok')
|
|
683
688
|
throw new Error('root config absent or unusable');
|
|
@@ -730,7 +735,8 @@ function loadConfigResolvedInternal(cwd, options = {}) {
|
|
|
730
735
|
// The workstream/root config that ACTUALLY governs this resolution is
|
|
731
736
|
// unusable. This outranks any earlier root-config fault for reporting.
|
|
732
737
|
configFault = read.fault;
|
|
733
|
-
|
|
738
|
+
// With a workstream active and a usable root config, the root config is what applies.
|
|
739
|
+
_warnUnusableConfig(read.fault, ws && rootParsed ? 'root' : 'defaults');
|
|
734
740
|
}
|
|
735
741
|
if (read.kind !== 'ok')
|
|
736
742
|
throw new Error('config absent or unusable');
|
|
@@ -1135,6 +1141,8 @@ module.exports = {
|
|
|
1135
1141
|
loadConfig,
|
|
1136
1142
|
loadConfigResolved,
|
|
1137
1143
|
CONFIG_REASON,
|
|
1144
|
+
CONFIG_MAX_BYTES,
|
|
1145
|
+
readConfigFileBounded,
|
|
1138
1146
|
_warnedUnusableConfig,
|
|
1139
1147
|
isGitIgnored,
|
|
1140
1148
|
CONFIG_DEFAULTS,
|