@opengsd/gsd-core 1.13.0 → 1.14.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/agents/gsd-advisor-researcher.compact.md +85 -0
- package/agents/gsd-ai-researcher.compact.md +96 -0
- package/agents/gsd-assumptions-analyzer.compact.md +81 -0
- package/agents/gsd-code-fixer.compact.md +458 -0
- package/agents/gsd-code-fixer.md +5 -5
- package/agents/gsd-code-reviewer.compact.md +269 -0
- package/agents/gsd-code-reviewer.md +15 -3
- package/agents/gsd-codebase-mapper.compact.md +760 -0
- package/agents/gsd-debug-session-manager.compact.md +345 -0
- package/agents/gsd-doc-classifier.compact.md +192 -0
- package/agents/gsd-doc-synthesizer.compact.md +200 -0
- package/agents/gsd-doc-verifier.compact.md +143 -0
- package/agents/gsd-doc-writer.compact.md +440 -0
- package/agents/gsd-dom-verifier.compact.md +138 -0
- package/agents/gsd-domain-researcher.compact.md +141 -0
- package/agents/gsd-eval-auditor.compact.md +160 -0
- package/agents/gsd-eval-planner.compact.md +137 -0
- package/agents/gsd-framework-selector.compact.md +82 -0
- package/agents/gsd-integration-checker.compact.md +245 -0
- package/agents/gsd-intel-updater.compact.md +226 -0
- package/agents/gsd-mempalace-curator.compact.md +45 -0
- package/agents/gsd-nyquist-auditor.compact.md +179 -0
- package/agents/gsd-pattern-mapper.compact.md +275 -0
- package/agents/gsd-project-researcher.compact.md +587 -0
- package/agents/gsd-research-synthesizer.compact.md +212 -0
- package/agents/gsd-roadmapper.compact.md +454 -0
- package/agents/gsd-roadmapper.md +13 -0
- package/agents/gsd-security-auditor.compact.md +162 -0
- package/agents/gsd-ui-auditor.compact.md +404 -0
- package/agents/gsd-ui-checker.compact.md +277 -0
- package/agents/gsd-ui-researcher.compact.md +282 -0
- package/agents/gsd-user-profiler.compact.md +108 -0
- package/bin/install.js +206 -68
- package/commands/gsd/cleanup.md +1 -0
- package/commands/gsd/code-review.md +2 -1
- package/commands/gsd/complete-milestone.md +1 -0
- package/commands/gsd/config.md +1 -0
- package/commands/gsd/debug.md +1 -0
- package/commands/gsd/graphify.md +1 -0
- package/commands/gsd/health.md +1 -0
- package/commands/gsd/mempalace-capture.md +1 -0
- package/commands/gsd/mempalace-recall.md +1 -0
- package/commands/gsd/new-milestone.md +1 -0
- package/commands/gsd/new-project.md +1 -0
- package/commands/gsd/next.md +1 -0
- package/commands/gsd/pause-work.md +1 -0
- package/commands/gsd/phase.md +1 -0
- package/commands/gsd/pr-branch.md +1 -0
- package/commands/gsd/resume-work.md +1 -0
- package/commands/gsd/review-backlog.md +1 -0
- package/commands/gsd/settings.md +2 -1
- package/commands/gsd/stats.md +1 -0
- package/commands/gsd/thread.md +1 -0
- package/commands/gsd/workspace.md +1 -0
- package/commands/gsd/workstreams.md +1 -0
- package/gsd-core/bin/check-latest-version.cjs +8 -3
- package/gsd-core/bin/gsd-tools.cjs +338 -125
- package/gsd-core/bin/lib/adr-parser.cjs +1 -1
- package/gsd-core/bin/lib/artifacts.cjs +2 -1
- package/gsd-core/bin/lib/audit.cjs +39 -22
- package/gsd-core/bin/lib/broken-windows.cjs +168 -49
- package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
- package/gsd-core/bin/lib/capability-loader.cjs +135 -1
- package/gsd-core/bin/lib/capability-registry.cjs +79 -67
- package/gsd-core/bin/lib/capability-source.cjs +19 -2
- package/gsd-core/bin/lib/capability-validator.cjs +14 -1
- package/gsd-core/bin/lib/check-command-router.cjs +113 -36
- package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
- package/gsd-core/bin/lib/commands.cjs +650 -72
- package/gsd-core/bin/lib/config-loader.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +153 -38
- package/gsd-core/bin/lib/coverage.cjs +1 -1
- package/gsd-core/bin/lib/decisions.cjs +137 -34
- package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
- package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
- package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
- package/gsd-core/bin/lib/init.cjs +409 -47
- package/gsd-core/bin/lib/install-engine.cjs +16 -3
- package/gsd-core/bin/lib/install-profiles.cjs +14 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +33 -4
- package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
- package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
- package/gsd-core/bin/lib/milestone.cjs +19 -8
- package/gsd-core/bin/lib/model-resolver.cjs +101 -10
- package/gsd-core/bin/lib/phase-command-router.cjs +7 -1
- package/gsd-core/bin/lib/phase-id.cjs +161 -22
- package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
- package/gsd-core/bin/lib/phase.cjs +167 -63
- package/gsd-core/bin/lib/planning-inspect.cjs +34 -18
- package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
- package/gsd-core/bin/lib/planning-workspace.cjs +50 -1
- package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
- package/gsd-core/bin/lib/quick-batch.cjs +1 -1
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
- package/gsd-core/bin/lib/research-store.cjs +11 -12
- package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +56 -15
- package/gsd-core/bin/lib/roadmap.cjs +108 -14
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +27 -10
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +12 -3
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +193 -4
- package/gsd-core/bin/lib/security.cjs +126 -7
- package/gsd-core/bin/lib/state-document.cjs +130 -28
- package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
- package/gsd-core/bin/lib/state-transition.cjs +142 -28
- package/gsd-core/bin/lib/state.cjs +223 -27
- package/gsd-core/bin/lib/surface.cjs +60 -2
- package/gsd-core/bin/lib/task-command-router.cjs +12 -6
- package/gsd-core/bin/lib/uat.cjs +1 -1
- package/gsd-core/bin/lib/update-context.cjs +30 -24
- package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
- package/gsd-core/bin/lib/verification.cjs +47 -15
- package/gsd-core/bin/lib/verify-command-grounding.cjs +1 -1
- package/gsd-core/bin/lib/verify.cjs +188 -23
- package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
- package/gsd-core/bin/lib/worktree-safety.cjs +13 -7
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -0
- package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
- package/gsd-core/references/compact-content-gate.md +66 -0
- package/gsd-core/references/loop-hook-dispatch.md +18 -0
- package/gsd-core/references/model-profiles.md +12 -3
- package/gsd-core/references/planning-config.md +3 -0
- package/gsd-core/references/tdd.md +5 -2
- package/gsd-core/references/thinking-models-planning.md +18 -2
- package/gsd-core/references/verification-patterns.md +17 -4
- package/gsd-core/references/worktree-path-safety.md +112 -2
- package/gsd-core/templates/README.md +7 -1
- package/gsd-core/templates/state.md +6 -3
- package/gsd-core/templates/summary.compact.md +212 -0
- package/gsd-core/templates/user-setup.compact.md +199 -0
- package/gsd-core/templates/user-setup.md +0 -9
- package/gsd-core/workflows/add-todo.md +3 -2
- package/gsd-core/workflows/autonomous.md +13 -10
- package/gsd-core/workflows/check-todos.md +4 -2
- package/gsd-core/workflows/cleanup.md +3 -1
- package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +7 -0
- package/gsd-core/workflows/code-review-fix.md +3 -3
- package/gsd-core/workflows/code-review.md +156 -30
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
- package/gsd-core/workflows/complete-milestone.md +39 -262
- package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
- package/gsd-core/workflows/docs-update.md +14 -155
- package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +18 -3
- package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +7 -2
- package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
- package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
- package/gsd-core/workflows/execute-phase.md +53 -152
- package/gsd-core/workflows/execute-plan.md +20 -7
- package/gsd-core/workflows/help/modes/full.compact.md +398 -0
- package/gsd-core/workflows/help.md +1 -1
- package/gsd-core/workflows/map-codebase.md +50 -3
- package/gsd-core/workflows/new-milestone.md +54 -12
- package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
- package/gsd-core/workflows/new-project.md +32 -202
- package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
- package/gsd-core/workflows/plan-phase.md +22 -181
- package/gsd-core/workflows/pr-branch.md +19 -7
- package/gsd-core/workflows/quick.md +8 -1
- package/gsd-core/workflows/reapply-patches.md +77 -3
- package/gsd-core/workflows/settings.md +18 -5
- package/gsd-core/workflows/update.md +7 -5
- package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
- package/gsd-core/workflows/verify-work.md +20 -180
- package/hooks/dist/gsd-agent-isolation-guard.js +42 -16
- package/hooks/dist/gsd-context-monitor.js +88 -15
- package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
- package/hooks/dist/gsd-secret-read-guard.js +44 -18
- package/hooks/dist/gsd-statusline.js +11 -7
- package/hooks/dist/gsd-validate-commit.sh +34 -4
- package/hooks/dist/gsd-worktree-path-guard.js +25 -14
- package/hooks/dist/gsd-write-guard.js +46 -1
- package/hooks/dist/lib/dispatch-identity.js +187 -0
- package/hooks/dist/lib/filename-classification.js +64 -0
- package/hooks/dist/lib/isolation-deny-reason.js +53 -1
- package/hooks/dist/lib/isolation-sentinel.js +58 -19
- package/hooks/gsd-agent-isolation-guard.js +42 -16
- package/hooks/gsd-context-monitor.js +88 -15
- package/hooks/gsd-cursor-subagent-start.js +34 -14
- package/hooks/gsd-secret-read-guard.js +44 -18
- package/hooks/gsd-statusline.js +11 -7
- package/hooks/gsd-validate-commit.sh +34 -4
- package/hooks/gsd-worktree-path-guard.js +25 -14
- package/hooks/gsd-write-guard.js +46 -1
- package/hooks/lib/dispatch-identity.js +187 -0
- package/hooks/lib/filename-classification.js +64 -0
- package/hooks/lib/isolation-deny-reason.js +53 -1
- package/hooks/lib/isolation-sentinel.js +58 -19
- package/package.json +10 -6
- package/scripts/benchmark-compact-content-variants.cjs +298 -0
- package/scripts/benchmark-compact-content.cjs +368 -0
- package/scripts/check-contract-drift.cjs +4 -1
- package/scripts/check-env.cjs +36 -8
- package/scripts/check-glossary-refs.cjs +25 -21
- package/scripts/ci-next-health.cjs +271 -0
- package/scripts/ci-prepare-test-scope.cjs +7 -7
- package/scripts/ci-test-scope.cjs +126 -20
- package/scripts/ci-timeout-report.cjs +1 -1
- package/scripts/diff-touches-shipped-paths.cjs +1 -1
- package/scripts/docs-guard-registry.cjs +7 -2
- package/scripts/gen-adr-index.cjs +8 -2
- package/scripts/gen-inventory-manifest.cjs +12 -0
- package/scripts/gen-platform-conformance-tier.cjs +557 -0
- package/scripts/lib/drift-scan.cjs +1 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +210 -0
- package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +276 -0
- package/scripts/lib/suite-detection.cjs +32 -0
- package/scripts/lint-allowed-tools-parity.cjs +221 -0
- package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +19 -2
- package/scripts/lint-phase-id-drift.cjs +338 -13
- package/scripts/lint-response-language-coverage.cjs +9 -3
- package/scripts/lint-source-test-name-collision.cjs +1 -1
- package/scripts/lint-test-file-count.allowlist.json +1 -0
- package/scripts/lint-vendored-deps.cjs +128 -17
- package/scripts/lint-workflow-shellcheck-baseline.json +85 -0
- package/scripts/prompt-injection-scan.sh +14 -0
- package/scripts/workflow-size.cjs +139 -0
- package/skills/gsd-cleanup/SKILL.md +1 -0
- package/skills/gsd-code-review/SKILL.md +2 -1
- package/skills/gsd-complete-milestone/SKILL.md +1 -0
- package/skills/gsd-config/SKILL.md +1 -0
- package/skills/gsd-debug/SKILL.md +1 -0
- package/skills/gsd-graphify/SKILL.md +1 -0
- package/skills/gsd-health/SKILL.md +1 -0
- package/skills/gsd-mempalace-capture/SKILL.md +1 -0
- package/skills/gsd-mempalace-recall/SKILL.md +1 -0
- package/skills/gsd-new-milestone/SKILL.md +1 -0
- package/skills/gsd-new-project/SKILL.md +1 -0
- package/skills/gsd-next/SKILL.md +1 -0
- package/skills/gsd-pause-work/SKILL.md +1 -0
- package/skills/gsd-phase/SKILL.md +1 -0
- package/skills/gsd-pr-branch/SKILL.md +1 -0
- package/skills/gsd-resume-work/SKILL.md +1 -0
- package/skills/gsd-review-backlog/SKILL.md +1 -0
- package/skills/gsd-settings/SKILL.md +2 -1
- package/skills/gsd-stats/SKILL.md +1 -0
- package/skills/gsd-thread/SKILL.md +1 -0
- package/skills/gsd-workspace/SKILL.md +1 -0
- package/skills/gsd-workstreams/SKILL.md +1 -0
- package/vscode/package.json +1 -1
- package/gsd-core/templates/claude-md.md +0 -145
- package/gsd-core/templates/codebase/concerns.md +0 -310
- package/gsd-core/templates/codebase/conventions.md +0 -307
- package/gsd-core/templates/codebase/integrations.md +0 -280
- package/gsd-core/templates/codebase/structure.md +0 -285
- package/gsd-core/templates/codebase/testing.md +0 -480
- package/gsd-core/templates/debug-subagent-prompt.md +0 -91
- package/gsd-core/templates/discovery.md +0 -146
|
@@ -25,7 +25,7 @@ const coreUtils = require("./core-utils.cjs");
|
|
|
25
25
|
const { normalizeLineEndings } = coreUtils;
|
|
26
26
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
27
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
28
|
-
const { planningDir, quickDirFrom } = planningWorkspace;
|
|
28
|
+
const { planningDir, quickDirFrom, todosDir } = planningWorkspace;
|
|
29
29
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
30
30
|
const frontmatter = require("./frontmatter.cjs");
|
|
31
31
|
const { extractFrontmatter, spliceFrontmatter } = frontmatter;
|
|
@@ -277,7 +277,7 @@ function scanDebugSessions(planDir) {
|
|
|
277
277
|
const filePath = node_path_1.default.join(debugDir, entry.name);
|
|
278
278
|
let safeFilePath;
|
|
279
279
|
try {
|
|
280
|
-
safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'debug session file',
|
|
280
|
+
safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'debug session file', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
281
281
|
}
|
|
282
282
|
catch {
|
|
283
283
|
continue;
|
|
@@ -387,7 +387,7 @@ function scanQuickTasks(planDir) {
|
|
|
387
387
|
const taskDir = node_path_1.default.join(quickDir, dirName);
|
|
388
388
|
let safeTaskDir;
|
|
389
389
|
try {
|
|
390
|
-
safeTaskDir = (0, security_cjs_1.requireSafePath)(taskDir, planDir, 'quick task dir',
|
|
390
|
+
safeTaskDir = (0, security_cjs_1.requireSafePath)(taskDir, planDir, 'quick task dir', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
391
391
|
}
|
|
392
392
|
catch {
|
|
393
393
|
continue;
|
|
@@ -399,7 +399,7 @@ function scanQuickTasks(planDir) {
|
|
|
399
399
|
if (summaryPath && node_fs_1.default.existsSync(summaryPath)) {
|
|
400
400
|
let safeSum;
|
|
401
401
|
try {
|
|
402
|
-
safeSum = (0, security_cjs_1.requireSafePath)(summaryPath, planDir, 'quick task summary',
|
|
402
|
+
safeSum = (0, security_cjs_1.requireSafePath)(summaryPath, planDir, 'quick task summary', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
403
403
|
}
|
|
404
404
|
catch {
|
|
405
405
|
continue;
|
|
@@ -475,7 +475,7 @@ function scanThreads(planDir) {
|
|
|
475
475
|
const filePath = node_path_1.default.join(threadsDir, entry.name);
|
|
476
476
|
let safeFilePath;
|
|
477
477
|
try {
|
|
478
|
-
safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'thread file',
|
|
478
|
+
safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'thread file', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
479
479
|
}
|
|
480
480
|
catch {
|
|
481
481
|
continue;
|
|
@@ -522,9 +522,16 @@ function scanThreads(planDir) {
|
|
|
522
522
|
* Scan .planning/todos/pending/ for pending todos.
|
|
523
523
|
* Returns array of { filename, priority, area, summary }.
|
|
524
524
|
* Display limited to first 5 + count of remainder.
|
|
525
|
+
*
|
|
526
|
+
* #4256: takes the ROOT-scoped todos base (`todosDir(cwd)`), NOT the
|
|
527
|
+
* workstream-scoped planning dir the other scans use — todos are shared
|
|
528
|
+
* project state (the migrateToWorkstreams contract keeps them at
|
|
529
|
+
* .planning/todos/), so the close gate must read the root or it clears
|
|
530
|
+
* vacuously under a workstream. The requireSafePath boundary below moves
|
|
531
|
+
* with the base.
|
|
525
532
|
*/
|
|
526
|
-
function scanTodos(
|
|
527
|
-
const pendingDir = node_path_1.default.join(
|
|
533
|
+
function scanTodos(todosBase) {
|
|
534
|
+
const pendingDir = node_path_1.default.join(todosBase, 'pending');
|
|
528
535
|
if (!node_fs_1.default.existsSync(pendingDir))
|
|
529
536
|
return { items: [], acknowledged: 0 };
|
|
530
537
|
let files;
|
|
@@ -550,7 +557,7 @@ function scanTodos(planDir) {
|
|
|
550
557
|
const filePath = node_path_1.default.join(pendingDir, entry.name);
|
|
551
558
|
let safeFilePath;
|
|
552
559
|
try {
|
|
553
|
-
safeFilePath = (0, security_cjs_1.requireSafePath)(filePath,
|
|
560
|
+
safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, todosBase, 'todo file', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
554
561
|
}
|
|
555
562
|
catch {
|
|
556
563
|
continue;
|
|
@@ -622,7 +629,7 @@ function scanSeeds(planDir) {
|
|
|
622
629
|
const filePath = node_path_1.default.join(seedsDir, entry.name);
|
|
623
630
|
let safeFilePath;
|
|
624
631
|
try {
|
|
625
|
-
safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'seed file',
|
|
632
|
+
safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'seed file', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
626
633
|
}
|
|
627
634
|
catch {
|
|
628
635
|
continue;
|
|
@@ -785,7 +792,7 @@ function scanUatGaps(planDir, cwd) {
|
|
|
785
792
|
const filePath = node_path_1.default.join(target.fullPath, file);
|
|
786
793
|
let safeFilePath;
|
|
787
794
|
try {
|
|
788
|
-
safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'UAT file',
|
|
795
|
+
safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'UAT file', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
789
796
|
}
|
|
790
797
|
catch {
|
|
791
798
|
continue;
|
|
@@ -858,7 +865,7 @@ function scanVerificationGaps(planDir, cwd) {
|
|
|
858
865
|
const filePath = node_path_1.default.join(target.fullPath, file);
|
|
859
866
|
let safeFilePath;
|
|
860
867
|
try {
|
|
861
|
-
safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'VERIFICATION file',
|
|
868
|
+
safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'VERIFICATION file', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
862
869
|
}
|
|
863
870
|
catch {
|
|
864
871
|
continue;
|
|
@@ -920,7 +927,7 @@ function scanContextQuestions(planDir, cwd) {
|
|
|
920
927
|
const filePath = node_path_1.default.join(target.fullPath, file);
|
|
921
928
|
let safeFilePath;
|
|
922
929
|
try {
|
|
923
|
-
safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'CONTEXT file',
|
|
930
|
+
safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'CONTEXT file', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
924
931
|
}
|
|
925
932
|
catch {
|
|
926
933
|
continue;
|
|
@@ -1008,7 +1015,7 @@ function scanDeferredItems(planDir, cwd) {
|
|
|
1008
1015
|
continue;
|
|
1009
1016
|
let safeFilePath;
|
|
1010
1017
|
try {
|
|
1011
|
-
safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'deferred items file',
|
|
1018
|
+
safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'deferred items file', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
1012
1019
|
}
|
|
1013
1020
|
catch {
|
|
1014
1021
|
continue;
|
|
@@ -1076,8 +1083,13 @@ function auditOpenArtifacts(cwd) {
|
|
|
1076
1083
|
}
|
|
1077
1084
|
})();
|
|
1078
1085
|
const todos = (() => {
|
|
1086
|
+
// #4256: the ONE root-scoped category — todos are shared project state,
|
|
1087
|
+
// so the close gate reads todosDir(cwd) (the root), not the workstream-
|
|
1088
|
+
// scoped planDir every other scan below receives. Reading planDir here
|
|
1089
|
+
// made audit-open print "All artifact types clear. Safe to proceed."
|
|
1090
|
+
// with pending todos on disk under a workstream.
|
|
1079
1091
|
try {
|
|
1080
|
-
return scanTodos(
|
|
1092
|
+
return scanTodos(todosDir(cwd));
|
|
1081
1093
|
}
|
|
1082
1094
|
catch {
|
|
1083
1095
|
return { items: [{ scan_error: true, filename: '', priority: '', area: '', summary: '' }], acknowledged: 0 };
|
|
@@ -1421,7 +1433,7 @@ function cmdAuditAcknowledge(cwd, args, raw) {
|
|
|
1421
1433
|
ioError(`no phase directory found for phase "${phase}"${archivedMilestone ? ` (archived-milestone "${archivedMilestone}")` : ''}`);
|
|
1422
1434
|
}
|
|
1423
1435
|
const filePath = node_path_1.default.join(targetDir, file);
|
|
1424
|
-
const safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'audit acknowledge target',
|
|
1436
|
+
const safeFilePath = (0, security_cjs_1.requireSafePath)(filePath, planDir, 'audit acknowledge target', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
1425
1437
|
if (!node_fs_1.default.existsSync(safeFilePath))
|
|
1426
1438
|
ioError(`file not found: ${file}`);
|
|
1427
1439
|
if (category === 'deferred_items') {
|
|
@@ -1503,7 +1515,7 @@ function cmdAuditAcknowledge(cwd, args, raw) {
|
|
|
1503
1515
|
if (category === 'debug_sessions') {
|
|
1504
1516
|
if (!slug)
|
|
1505
1517
|
ioError('--slug is required for --category debug_sessions');
|
|
1506
|
-
safeFilePath = (0, security_cjs_1.requireSafePath)(node_path_1.default.join(planDir, 'debug', `${slug}.md`), planDir, 'audit acknowledge target',
|
|
1518
|
+
safeFilePath = (0, security_cjs_1.requireSafePath)(node_path_1.default.join(planDir, 'debug', `${slug}.md`), planDir, 'audit acknowledge target', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
1507
1519
|
if (!node_fs_1.default.existsSync(safeFilePath))
|
|
1508
1520
|
ioError(`file not found: debug/${slug}.md`);
|
|
1509
1521
|
const content = node_fs_1.default.readFileSync(safeFilePath, 'utf-8');
|
|
@@ -1512,7 +1524,7 @@ function cmdAuditAcknowledge(cwd, args, raw) {
|
|
|
1512
1524
|
else if (category === 'threads') {
|
|
1513
1525
|
if (!slug)
|
|
1514
1526
|
ioError('--slug is required for --category threads');
|
|
1515
|
-
safeFilePath = (0, security_cjs_1.requireSafePath)(node_path_1.default.join(planDir, 'threads', `${slug}.md`), planDir, 'audit acknowledge target',
|
|
1527
|
+
safeFilePath = (0, security_cjs_1.requireSafePath)(node_path_1.default.join(planDir, 'threads', `${slug}.md`), planDir, 'audit acknowledge target', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
1516
1528
|
if (!node_fs_1.default.existsSync(safeFilePath))
|
|
1517
1529
|
ioError(`file not found: threads/${slug}.md`);
|
|
1518
1530
|
const content = node_fs_1.default.readFileSync(safeFilePath, 'utf-8');
|
|
@@ -1521,7 +1533,7 @@ function cmdAuditAcknowledge(cwd, args, raw) {
|
|
|
1521
1533
|
else if (category === 'seeds') {
|
|
1522
1534
|
if (!seedId)
|
|
1523
1535
|
ioError('--seed-id is required for --category seeds');
|
|
1524
|
-
safeFilePath = (0, security_cjs_1.requireSafePath)(node_path_1.default.join(planDir, 'seeds', `${seedId}.md`), planDir, 'audit acknowledge target',
|
|
1536
|
+
safeFilePath = (0, security_cjs_1.requireSafePath)(node_path_1.default.join(planDir, 'seeds', `${seedId}.md`), planDir, 'audit acknowledge target', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
1525
1537
|
if (!node_fs_1.default.existsSync(safeFilePath))
|
|
1526
1538
|
ioError(`file not found: seeds/${seedId}.md`);
|
|
1527
1539
|
const content = node_fs_1.default.readFileSync(safeFilePath, 'utf-8');
|
|
@@ -1530,7 +1542,12 @@ function cmdAuditAcknowledge(cwd, args, raw) {
|
|
|
1530
1542
|
else if (category === 'todos') {
|
|
1531
1543
|
if (!filename)
|
|
1532
1544
|
ioError('--filename is required for --category todos');
|
|
1533
|
-
|
|
1545
|
+
// #4256: todos are root-scoped shared state — derive the todos base and
|
|
1546
|
+
// pass it as BOTH the path base and the requireSafePath boundary. The
|
|
1547
|
+
// old workstream-scoped planDir boundary would refuse a root todos file
|
|
1548
|
+
// outright, and even a path fix alone would have thrown here.
|
|
1549
|
+
const rootTodos = todosDir(cwd);
|
|
1550
|
+
safeFilePath = (0, security_cjs_1.requireSafePath)(node_path_1.default.join(rootTodos, 'pending', filename), rootTodos, 'audit acknowledge target', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
1534
1551
|
if (!node_fs_1.default.existsSync(safeFilePath))
|
|
1535
1552
|
ioError(`file not found: todos/pending/${filename}`);
|
|
1536
1553
|
currentValue = ''; // presence-only — see scanTodos
|
|
@@ -1538,14 +1555,14 @@ function cmdAuditAcknowledge(cwd, args, raw) {
|
|
|
1538
1555
|
else if (category === 'quick_tasks') {
|
|
1539
1556
|
if (!quickDir)
|
|
1540
1557
|
ioError('--dir is required for --category quick_tasks');
|
|
1541
|
-
const taskDir = (0, security_cjs_1.requireSafePath)(node_path_1.default.join(planDir, 'quick', quickDir), planDir, 'audit acknowledge target dir',
|
|
1558
|
+
const taskDir = (0, security_cjs_1.requireSafePath)(node_path_1.default.join(planDir, 'quick', quickDir), planDir, 'audit acknowledge target dir', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
1542
1559
|
if (!node_fs_1.default.existsSync(taskDir))
|
|
1543
1560
|
ioError(`directory not found: quick/${quickDir}`);
|
|
1544
1561
|
// Shared with scanQuickTasks (#3458 follow-up) so the reader and this
|
|
1545
1562
|
// writer can never disagree about which file is the task's record.
|
|
1546
1563
|
const resolvedSummaryPath = resolveQuickTaskSummaryFile(taskDir, quickDir);
|
|
1547
1564
|
if (resolvedSummaryPath) {
|
|
1548
|
-
safeFilePath = (0, security_cjs_1.requireSafePath)(resolvedSummaryPath, planDir, 'audit acknowledge target',
|
|
1565
|
+
safeFilePath = (0, security_cjs_1.requireSafePath)(resolvedSummaryPath, planDir, 'audit acknowledge target', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
1549
1566
|
const content = node_fs_1.default.readFileSync(safeFilePath, 'utf-8');
|
|
1550
1567
|
currentValue = (extractFrontmatter(content, safeFilePath).status || 'unknown').toLowerCase();
|
|
1551
1568
|
}
|
|
@@ -1556,7 +1573,7 @@ function cmdAuditAcknowledge(cwd, args, raw) {
|
|
|
1556
1573
|
// acknowledgment's own snapshot of "no summary exists yet", which
|
|
1557
1574
|
// self-invalidates the moment a real SUMMARY.md is written (the
|
|
1558
1575
|
// scanner then reads THAT file's own status instead).
|
|
1559
|
-
safeFilePath = (0, security_cjs_1.requireSafePath)(node_path_1.default.join(taskDir, `${quickDir}-SUMMARY.md`), planDir, 'audit acknowledge target',
|
|
1576
|
+
safeFilePath = (0, security_cjs_1.requireSafePath)(node_path_1.default.join(taskDir, `${quickDir}-SUMMARY.md`), planDir, 'audit acknowledge target', security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
1560
1577
|
currentValue = 'missing';
|
|
1561
1578
|
createIfMissing = true;
|
|
1562
1579
|
fmForCreate = { status: 'missing' };
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* When `workflow.windows_enforce` is true, `/gsd-ship` blocks while any entry is
|
|
8
8
|
* `open`; an entry can be `waived` only with a recorded reason or `fixed`.
|
|
9
9
|
*
|
|
10
|
-
* LEAF MODULE — imports
|
|
10
|
+
* LEAF MODULE — imports node:fs + node:path, plus two compiled sibling lib
|
|
11
|
+
* modules require()d at runtime: workstream-inventory.cjs (the #4487
|
|
12
|
+
* milestone stamp) and capability-lock.cjs (the #3780 cross-process ledger
|
|
13
|
+
* lock). No other src/ imports.
|
|
11
14
|
*
|
|
12
15
|
* Storage format (`.planning/WINDOWS.md`):
|
|
13
16
|
* ---
|
|
@@ -58,12 +61,15 @@ exports.parseLedger = parseLedger;
|
|
|
58
61
|
exports.renderLedger = renderLedger;
|
|
59
62
|
exports.renderTable = renderTable;
|
|
60
63
|
exports.extractTableRegion = extractTableRegion;
|
|
64
|
+
exports.withLedgerLock = withLedgerLock;
|
|
61
65
|
exports.cmdWindowsStatus = cmdWindowsStatus;
|
|
62
66
|
exports.cmdWindowsAppend = cmdWindowsAppend;
|
|
63
67
|
exports.cmdWindowsWaive = cmdWindowsWaive;
|
|
64
68
|
exports.cmdWindowsMarkFixed = cmdWindowsMarkFixed;
|
|
65
69
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
66
70
|
const node_path_1 = __importDefault(require("node:path"));
|
|
71
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
72
|
+
const workstreamInventory = require("./workstream-inventory.cjs");
|
|
67
73
|
// ─── Constants ─────────────────────────────────────────────────────────────
|
|
68
74
|
exports.LEDGER_FILE_NAME = 'WINDOWS.md';
|
|
69
75
|
exports.SCHEMA_VERSION = 1;
|
|
@@ -90,6 +96,11 @@ exports.REASON = Object.freeze({
|
|
|
90
96
|
// sole source of truth) at the pre-write seam — refuse rather than silently
|
|
91
97
|
// reconcile by overwriting the operator's hand-edit or dropping a row.
|
|
92
98
|
WINDOWS_LEDGER_TABLE_DRIFT: 'windows_ledger_table_drift',
|
|
99
|
+
// #3780: the read-compute-write cycle is serialized on a cross-process
|
|
100
|
+
// ledger lock; this fires only when another writer held the lock past the
|
|
101
|
+
// whole bounded retry budget — a typed, actionable refusal instead of a
|
|
102
|
+
// silently-lost mutation reported as success.
|
|
103
|
+
WINDOWS_LEDGER_LOCK: 'windows_ledger_lock',
|
|
93
104
|
});
|
|
94
105
|
/** Allowed window kinds. Aligned with the issue's enumerated sources. */
|
|
95
106
|
exports.KINDS = Object.freeze([
|
|
@@ -219,12 +230,14 @@ function nextId(entries) {
|
|
|
219
230
|
* Append a window to the ledger. Assigns the next dense id (max+1), sets
|
|
220
231
|
* status=open, timestamps via opts.now.
|
|
221
232
|
*
|
|
222
|
-
* Concurrency (issue #1950 review L2
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
233
|
+
* Concurrency (issue #1950 review L2, superseded by #3780): as a PURE
|
|
234
|
+
* function this operates on whatever ledger snapshot it is passed and cannot
|
|
235
|
+
* see concurrent writers — serialization is the CALLER's job. The I/O entry
|
|
236
|
+
* points below (cmdWindowsAppend/Waive/MarkFixed) now discharge that duty by
|
|
237
|
+
* holding the cross-process ledger lock across their whole
|
|
238
|
+
* read-compute-write cycle, so the previously-documented loss (two parallel
|
|
239
|
+
* writers, second rename wins, first entry silently gone) can no longer
|
|
240
|
+
* occur through the CLI.
|
|
228
241
|
*/
|
|
229
242
|
function appendWindow(ledger, input, opts = { now: new Date().toISOString() }) {
|
|
230
243
|
validateKind(input.kind);
|
|
@@ -243,6 +256,7 @@ function appendWindow(ledger, input, opts = { now: new Date().toISOString() }) {
|
|
|
243
256
|
reason: '',
|
|
244
257
|
recorded_at: opts.now,
|
|
245
258
|
resolved_at: null,
|
|
259
|
+
milestone: input.milestone ?? null,
|
|
246
260
|
};
|
|
247
261
|
const entries = [...ledger.entries, entry];
|
|
248
262
|
const result = recomputeCounts({ ...ledger, entries, last_updated: opts.now });
|
|
@@ -475,6 +489,19 @@ function validateEntryShape(e, i) {
|
|
|
475
489
|
reason: o.reason,
|
|
476
490
|
recorded_at: recordedStr,
|
|
477
491
|
resolved_at: resolvedStr,
|
|
492
|
+
// #4487: NOT in `required` above -- an entry recorded before this field
|
|
493
|
+
// existed has no `milestone` key at all, and that must parse cleanly.
|
|
494
|
+
// Preserve the absence itself -- by not materializing the property at
|
|
495
|
+
// all, via the conditional spread below, rather than assigning it
|
|
496
|
+
// `milestone: undefined` (an object literal property set to `undefined`
|
|
497
|
+
// is still an OWN property; `'milestone' in entry` reads true either
|
|
498
|
+
// way) -- so a genuinely absent key stays absent both to `in` and to
|
|
499
|
+
// JSON.stringify on re-render. Collapsing absence to an explicit null
|
|
500
|
+
// instead would stamp every legacy entry with permanent
|
|
501
|
+
// `"milestone": null` noise the moment the ledger is next touched. A
|
|
502
|
+
// genuinely-recorded-but-unresolvable milestone (set by appendWindow)
|
|
503
|
+
// is still an explicit null and round-trips as one.
|
|
504
|
+
...('milestone' in o ? { milestone: typeof o.milestone === 'string' ? o.milestone : null } : {}),
|
|
478
505
|
};
|
|
479
506
|
}
|
|
480
507
|
function parseLedger(raw) {
|
|
@@ -685,6 +712,78 @@ function diffTableRowIds(expectedTable, actualTable) {
|
|
|
685
712
|
function ledgerPath(cwd) {
|
|
686
713
|
return node_path_1.default.join(cwd, '.planning', exports.LEDGER_FILE_NAME);
|
|
687
714
|
}
|
|
715
|
+
/**
|
|
716
|
+
* The SHARED hardened cross-process lock primitive (single source of truth
|
|
717
|
+
* for capability-lifecycle + capability-consent, extracted so locks cannot
|
|
718
|
+
* diverge). Required LAZILY: capability-lock captures the process start time
|
|
719
|
+
* at module load — a `ps` subprocess on macOS, PowerShell on win32 — and
|
|
720
|
+
* this module is loaded by lock-free readers (`windows status`, the
|
|
721
|
+
* /gsd-ship gate) that must not pay that per-invocation cost (#3780
|
|
722
|
+
* review). `require` is cached, so writers pay it once per process.
|
|
723
|
+
*/
|
|
724
|
+
let _lockMod = null;
|
|
725
|
+
function ledgerLock() {
|
|
726
|
+
if (_lockMod === null) {
|
|
727
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
728
|
+
_lockMod = require('./capability-lock.cjs');
|
|
729
|
+
/* eslint-enable @typescript-eslint/no-require-imports */
|
|
730
|
+
}
|
|
731
|
+
return _lockMod;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Budget mirrors capability-consent's CONSENT_LOCK_MAX_ATTEMPTS: two
|
|
735
|
+
* genuinely-racing writers must SERIALIZE, not fail. The ledger's critical
|
|
736
|
+
* section is sub-millisecond and the primitive backs off ~25-50ms per
|
|
737
|
+
* attempt, so 50 attempts is orders of magnitude beyond any real contention
|
|
738
|
+
* while keeping the worst case (a holder that never releases until the
|
|
739
|
+
* primitive's own liveness/deadman protocol reclaims it) bounded at ~2s
|
|
740
|
+
* before the typed refusal below.
|
|
741
|
+
*/
|
|
742
|
+
const LEDGER_LOCK_MAX_ATTEMPTS = 50;
|
|
743
|
+
function ledgerLockPath(cwd) {
|
|
744
|
+
return node_path_1.default.join(cwd, '.planning', '.WINDOWS.lock');
|
|
745
|
+
}
|
|
746
|
+
function acquireLedgerLock(cwd) {
|
|
747
|
+
return ledgerLock().acquireLock(ledgerLockPath(cwd), {
|
|
748
|
+
maxAttempts: LEDGER_LOCK_MAX_ATTEMPTS,
|
|
749
|
+
// A contended fresh/live holder is WAITED FOR (back off + retry), not
|
|
750
|
+
// failed-fast — racing wave-level executors serialize (issue #3780).
|
|
751
|
+
waitForFresh: true,
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
function releaseLedgerLock(handle) {
|
|
755
|
+
ledgerLock().releaseLock(handle);
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Run `fn` (a full ledger read-compute-write cycle) while holding the
|
|
759
|
+
* cross-process ledger lock. Throws a typed WindowsError — never falls back
|
|
760
|
+
* to an unlocked mutation — when the lock cannot be acquired within the
|
|
761
|
+
* budget, mirroring capability-consent finding 3: a locked store must refuse
|
|
762
|
+
* the write rather than silently race for it. Readers (cmdWindowsStatus, the
|
|
763
|
+
* ship gate) deliberately do NOT take this lock: the atomic rename already
|
|
764
|
+
* gives them a whole-file snapshot.
|
|
765
|
+
*
|
|
766
|
+
* EXPORTED (#3780) because `withLedgerLock` is the ONE serialization seam
|
|
767
|
+
* for WINDOWS.md: every writer of the ledger — the cmd* entry points here
|
|
768
|
+
* and any sibling module with its own read-compute-write cycle on the same
|
|
769
|
+
* file (refactor-trigger-command-router's strict-window record/resolve) —
|
|
770
|
+
* must hold this lock, or the lost-update race #3780 fixed survives on that
|
|
771
|
+
* path.
|
|
772
|
+
*/
|
|
773
|
+
function withLedgerLock(cwd, fn) {
|
|
774
|
+
const handle = acquireLedgerLock(cwd);
|
|
775
|
+
if (!handle) {
|
|
776
|
+
throw new WindowsError(exports.REASON.WINDOWS_LEDGER_LOCK, `Another writer holds the ledger lock at ${ledgerLockPath(cwd)}; WINDOWS.md ` +
|
|
777
|
+
'mutations are serialized per project. Re-run the command once the other ' +
|
|
778
|
+
'writer finishes — the lock is reclaimed automatically if its holder died.');
|
|
779
|
+
}
|
|
780
|
+
try {
|
|
781
|
+
return fn();
|
|
782
|
+
}
|
|
783
|
+
finally {
|
|
784
|
+
releaseLedgerLock(handle);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
688
787
|
function readLedgerOrNull(cwd) {
|
|
689
788
|
const p = ledgerPath(cwd);
|
|
690
789
|
let raw;
|
|
@@ -910,24 +1009,36 @@ function cmdWindowsAppend(cwd, args, opts = {}) {
|
|
|
910
1009
|
flags: ['--kind', '--phase', '--file', '--line', '--description'],
|
|
911
1010
|
required: ['--kind', '--phase', '--description'],
|
|
912
1011
|
});
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1012
|
+
// #3780: the whole read-compute-write cycle — snapshot, milestone stamp,
|
|
1013
|
+
// id allocation, atomic rename — holds the ledger lock, so two parallel
|
|
1014
|
+
// invocations can no longer compute the same nextId from the same snapshot
|
|
1015
|
+
// and silently lose the first append to the second rename.
|
|
1016
|
+
withLedgerLock(cwd, () => {
|
|
1017
|
+
let ledger;
|
|
1018
|
+
try {
|
|
1019
|
+
ledger = readLedgerOrNull(cwd) ?? emptyLedger(nowIso());
|
|
1020
|
+
}
|
|
1021
|
+
catch (e) {
|
|
1022
|
+
if (e instanceof WindowsError)
|
|
1023
|
+
throw e;
|
|
1024
|
+
throw new WindowsError(exports.REASON.WINDOWS_LEDGER_MALFORMED, e.message);
|
|
1025
|
+
}
|
|
1026
|
+
// #4487: stamp the workstream's resolved milestone at record time -- the
|
|
1027
|
+
// same STATE.md-first, ROADMAP-fallback resolution workstream-inventory.cts
|
|
1028
|
+
// already uses. Best-effort: an unreadable/missing STATE.md or ROADMAP.md
|
|
1029
|
+
// resolves to null, same as an entry recorded before this field existed.
|
|
1030
|
+
const milestone = workstreamInventory.readCurrentMilestoneVersion(node_path_1.default.join(cwd, '.planning', 'STATE.md'), node_path_1.default.join(cwd, '.planning', 'ROADMAP.md'));
|
|
1031
|
+
const result = appendWindow(ledger, {
|
|
1032
|
+
kind: parsed.values['--kind'],
|
|
1033
|
+
phase: parsed.values['--phase'] ?? '',
|
|
1034
|
+
file: parsed.values['--file'] ?? '',
|
|
1035
|
+
line: parsed.values['--line'] == null ? null : Number(parsed.values['--line']),
|
|
1036
|
+
description: parsed.values['--description'] ?? '',
|
|
1037
|
+
milestone,
|
|
1038
|
+
}, { now: nowIso() });
|
|
1039
|
+
writeLedgerAtomic(cwd, result.ledger);
|
|
1040
|
+
emit({ ok: true, ledger: result.ledger, entry: result.entry });
|
|
1041
|
+
});
|
|
931
1042
|
}
|
|
932
1043
|
/** `gsd-tools windows waive <id> "<reason>"`. */
|
|
933
1044
|
function cmdWindowsWaive(cwd, args, opts = {}) {
|
|
@@ -936,36 +1047,44 @@ function cmdWindowsWaive(cwd, args, opts = {}) {
|
|
|
936
1047
|
const idStr = positionals[0];
|
|
937
1048
|
const reason = positionals[1];
|
|
938
1049
|
const id = parseIdOrThrow(idStr);
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1050
|
+
// #3780: same serialization as append — a concurrent append holding a stale
|
|
1051
|
+
// snapshot would otherwise overwrite the waive and resurrect the entry.
|
|
1052
|
+
withLedgerLock(cwd, () => {
|
|
1053
|
+
let ledger;
|
|
1054
|
+
try {
|
|
1055
|
+
ledger = readLedgerOrNull(cwd) ?? emptyLedger(nowIso());
|
|
1056
|
+
}
|
|
1057
|
+
catch (e) {
|
|
1058
|
+
if (e instanceof WindowsError)
|
|
1059
|
+
throw e;
|
|
1060
|
+
throw new WindowsError(exports.REASON.WINDOWS_LEDGER_MALFORMED, e.message);
|
|
1061
|
+
}
|
|
1062
|
+
const updated = markWaived(ledger, id, reason ?? '', { now: nowIso() });
|
|
1063
|
+
writeLedgerAtomic(cwd, updated);
|
|
1064
|
+
emit({ ok: true, ledger: updated });
|
|
1065
|
+
});
|
|
951
1066
|
}
|
|
952
1067
|
/** `gsd-tools windows fixed <id>`. */
|
|
953
1068
|
function cmdWindowsMarkFixed(cwd, args, opts = {}) {
|
|
954
1069
|
void opts;
|
|
955
1070
|
const { positionals } = parseArgs(args, { flags: [], required: [], positionals: 1 });
|
|
956
1071
|
const id = parseIdOrThrow(positionals[0]);
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1072
|
+
// #3780: same serialization as append — a concurrent writer holding a
|
|
1073
|
+
// stale snapshot would otherwise overwrite the resolved status.
|
|
1074
|
+
withLedgerLock(cwd, () => {
|
|
1075
|
+
let ledger;
|
|
1076
|
+
try {
|
|
1077
|
+
ledger = readLedgerOrNull(cwd) ?? emptyLedger(nowIso());
|
|
1078
|
+
}
|
|
1079
|
+
catch (e) {
|
|
1080
|
+
if (e instanceof WindowsError)
|
|
1081
|
+
throw e;
|
|
1082
|
+
throw new WindowsError(exports.REASON.WINDOWS_LEDGER_MALFORMED, e.message);
|
|
1083
|
+
}
|
|
1084
|
+
const updated = markFixed(ledger, id, { now: nowIso() });
|
|
1085
|
+
writeLedgerAtomic(cwd, updated);
|
|
1086
|
+
emit({ ok: true, ledger: updated });
|
|
1087
|
+
});
|
|
969
1088
|
}
|
|
970
1089
|
function parseIdOrThrow(raw) {
|
|
971
1090
|
if (raw == null || raw === '') {
|
|
@@ -25,6 +25,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
25
25
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
26
26
|
const node_path_1 = __importDefault(require("node:path"));
|
|
27
27
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
28
|
+
const security_cjs_1 = require("./security.cjs");
|
|
28
29
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
29
30
|
const sourceMod = require('./capability-source.cjs');
|
|
30
31
|
const ledgerMod = require('./capability-ledger.cjs');
|
|
@@ -202,7 +203,10 @@ function safeRmUnder(runtimeDir, rel) {
|
|
|
202
203
|
catch {
|
|
203
204
|
return false;
|
|
204
205
|
}
|
|
205
|
-
|
|
206
|
+
// Containment decision is the canonical LEXICAL predicate (ADR-4650 decision 6); lexical because
|
|
207
|
+
// both operands are already realpath-resolved here and the final component is deliberately
|
|
208
|
+
// handled as a link (see above).
|
|
209
|
+
if ((0, security_cjs_1.tryWithinRootLexical)(realParent, realRoot) === null)
|
|
206
210
|
return false;
|
|
207
211
|
const realTarget = node_path_1.default.join(realParent, node_path_1.default.basename(target));
|
|
208
212
|
let st;
|
|
@@ -250,11 +254,11 @@ function confinedSharedFile(runtimeDir, relFile) {
|
|
|
250
254
|
catch {
|
|
251
255
|
// Parent does not exist yet (created inside the scope on write): a non-existent path cannot be a
|
|
252
256
|
// symlink escaping the root, so a lexical containment check is sufficient.
|
|
253
|
-
if (
|
|
257
|
+
if ((0, security_cjs_1.tryWithinRootLexical)(parentDir, realRoot) === null)
|
|
254
258
|
return null;
|
|
255
259
|
return target;
|
|
256
260
|
}
|
|
257
|
-
if (
|
|
261
|
+
if ((0, security_cjs_1.tryWithinRootLexical)(realParent, realRoot) === null)
|
|
258
262
|
return null;
|
|
259
263
|
return node_path_1.default.join(realParent, node_path_1.default.basename(target));
|
|
260
264
|
}
|
|
@@ -363,7 +367,7 @@ function confinedBundleScript(capDirPath, script) {
|
|
|
363
367
|
// disk): a non-existent root cannot be a symlink escaping itself, so confine lexically.
|
|
364
368
|
realCapRoot = node_path_1.default.resolve(capDirPath);
|
|
365
369
|
const targetLex = node_path_1.default.resolve(realCapRoot, script);
|
|
366
|
-
if (
|
|
370
|
+
if ((0, security_cjs_1.tryWithinRootLexical)(targetLex, realCapRoot) === null)
|
|
367
371
|
return null;
|
|
368
372
|
return targetLex;
|
|
369
373
|
}
|
|
@@ -376,13 +380,13 @@ function confinedBundleScript(capDirPath, script) {
|
|
|
376
380
|
catch {
|
|
377
381
|
// Parent does not exist yet (created inside the bundle): lexical containment is sufficient
|
|
378
382
|
// because a non-existent path cannot be a symlink escaping the root.
|
|
379
|
-
if (
|
|
383
|
+
if ((0, security_cjs_1.tryWithinRootLexical)(parentDir, realCapRoot) === null)
|
|
380
384
|
return null;
|
|
381
385
|
return target;
|
|
382
386
|
}
|
|
383
387
|
// The realpath'd parent chain must remain inside the bundle — an ancestor symlink escaping the
|
|
384
388
|
// bundle is refused here (the symlink is followed by realpathSync, so its real location is checked).
|
|
385
|
-
if (
|
|
389
|
+
if ((0, security_cjs_1.tryWithinRootLexical)(realParent, realCapRoot) === null)
|
|
386
390
|
return null;
|
|
387
391
|
return node_path_1.default.join(realParent, node_path_1.default.basename(target));
|
|
388
392
|
}
|