@opengsd/gsd-core 1.7.0-rc.1 → 1.7.0-rc.3
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 +20 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +711 -0
- package/GEMINI.md +25 -23
- package/README.md +3 -3
- package/agents/gsd-advisor-researcher.md +3 -1
- package/agents/gsd-ai-researcher.md +2 -2
- package/agents/gsd-assumptions-analyzer.md +2 -0
- package/agents/gsd-code-fixer.md +3 -1
- package/agents/gsd-code-reviewer.md +2 -0
- package/agents/gsd-codebase-mapper.md +2 -0
- package/agents/gsd-debug-session-manager.md +1 -1
- package/agents/gsd-debugger.md +3 -1
- package/agents/gsd-doc-writer.md +2 -0
- package/agents/gsd-domain-researcher.md +1 -1
- package/agents/gsd-eval-auditor.md +3 -1
- package/agents/gsd-executor.md +12 -9
- package/agents/gsd-integration-checker.md +2 -0
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +1 -1
- package/agents/gsd-nyquist-auditor.md +2 -0
- package/agents/gsd-phase-researcher.md +4 -2
- package/agents/gsd-plan-checker.md +3 -1
- package/agents/gsd-planner.md +4 -2
- package/agents/gsd-project-researcher.md +4 -2
- package/agents/gsd-research-synthesizer.md +3 -1
- package/agents/gsd-roadmapper.md +2 -0
- package/agents/gsd-security-auditor.md +2 -0
- package/agents/gsd-ui-auditor.md +2 -0
- package/agents/gsd-ui-checker.md +2 -0
- package/agents/gsd-ui-researcher.md +4 -2
- package/agents/gsd-verifier.md +6 -4
- package/bin/install.js +226 -459
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/mempalace-recall.md +9 -5
- package/commands/gsd/next.md +29 -0
- package/commands/gsd/ns-workflow.md +5 -2
- package/gsd-core/bin/ensure-runtime-build.cjs +246 -0
- package/gsd-core/bin/gsd-tools.cjs +43 -8
- package/gsd-core/bin/lib/capability-loader.cjs +27 -9
- package/gsd-core/bin/lib/capability-registry.cjs +209 -205
- package/gsd-core/bin/lib/capability-source.cjs +22 -7
- package/gsd-core/bin/lib/capability-validator.cjs +24 -2
- package/gsd-core/bin/lib/check-command-router.cjs +111 -1
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- package/gsd-core/bin/lib/commands.cjs +2 -1
- package/gsd-core/bin/lib/config.cjs +19 -1
- package/gsd-core/bin/lib/core-utils.cjs +41 -0
- package/gsd-core/bin/lib/external-job.cjs +287 -0
- package/gsd-core/bin/lib/frontmatter.cjs +53 -6
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +151 -0
- package/gsd-core/bin/lib/graphify.cjs +41 -9
- package/gsd-core/bin/lib/handshake-serialized.cjs +70 -0
- package/gsd-core/bin/lib/host-integration-sdk.cjs +53 -0
- package/gsd-core/bin/lib/host-integration.cjs +61 -0
- package/gsd-core/bin/lib/init.cjs +34 -6
- package/gsd-core/bin/lib/milestone.cjs +56 -11
- package/gsd-core/bin/lib/phase-id.cjs +18 -0
- package/gsd-core/bin/lib/phase.cjs +51 -29
- package/gsd-core/bin/lib/phases-command-router.cjs +4 -3
- package/gsd-core/bin/lib/plan-scan.cjs +9 -1
- package/gsd-core/bin/lib/probe-core.cjs +44 -4
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +21 -19
- package/gsd-core/bin/lib/roadmap-command-router.cjs +3 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +21 -11
- package/gsd-core/bin/lib/roadmap.cjs +45 -22
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +18 -13
- package/gsd-core/bin/lib/runtime-name-policy.cjs +8 -10
- package/gsd-core/bin/lib/shell-command-projection.cjs +12 -8
- package/gsd-core/bin/lib/smart-entry.cjs +521 -0
- package/gsd-core/bin/lib/state-transition.cjs +15 -0
- package/gsd-core/bin/lib/state.cjs +27 -8
- package/gsd-core/bin/lib/surface.cjs +18 -9
- package/gsd-core/bin/lib/update-context.cjs +0 -5
- package/gsd-core/bin/lib/validate.cjs +2 -1
- package/gsd-core/bin/lib/verify.cjs +6 -4
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +12 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +28 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -13
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +0 -5
- package/gsd-core/references/agent-skills-bootstrap.md +60 -0
- package/gsd-core/references/gates.md +1 -1
- package/gsd-core/references/model-profiles.md +27 -0
- package/gsd-core/references/planning-config.md +1 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous.md +23 -25
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +1 -1
- package/gsd-core/workflows/code-review.md +1 -1
- package/gsd-core/workflows/complete-milestone.md +7 -11
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +1 -1
- package/gsd-core/workflows/docs-update.md +1 -1
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
- package/gsd-core/workflows/execute-phase.md +5 -6
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/explore.md +1 -1
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/forensics.md +4 -4
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +15 -1
- package/gsd-core/workflows/help/modes/topic.md +1 -0
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +1 -1
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +4 -4
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +7 -1
- package/gsd-core/workflows/new-project.md +1 -1
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +42 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +102 -0
- package/gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md +23 -0
- package/gsd-core/workflows/plan-phase.md +12 -160
- package/gsd-core/workflows/plan-review-convergence.md +1 -1
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/pr-branch.md +1 -1
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +1 -1
- package/gsd-core/workflows/reapply-patches.md +1 -1
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +50 -9
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +13 -11
- package/gsd-core/workflows/settings-integrations.md +1 -1
- package/gsd-core/workflows/settings.md +1 -1
- package/gsd-core/workflows/ship.md +1 -1
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +120 -0
- package/gsd-core/workflows/spec-phase.md +8 -6
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +1 -1
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/update.md +3 -6
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +63 -6
- package/package.json +5 -5
- package/scripts/ci-test-scope.cjs +19 -1
- package/scripts/generate-package-identity.cjs +27 -2
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -148
- package/scripts/lint-regression-test-names.allowlist.json +1 -273
- package/scripts/lint-test-file-count.allowlist.json +7 -91
- package/scripts/research-profiles.cjs +6 -6
- package/scripts/run-tests.cjs +67 -4
- package/scripts/slurm-adapter.cjs +269 -0
- package/scripts/sync-manifest-versions.cjs +67 -14
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-recall/SKILL.md +9 -5
- package/skills/gsd-next/SKILL.md +30 -0
- package/skills/gsd-ns-workflow/SKILL.md +4 -1
- package/skills/gsd-review/SKILL.md +6 -0
- package/gemini-extension.json +0 -6
|
@@ -24,7 +24,7 @@ const PROFILES = [
|
|
|
24
24
|
'Researches domain ecosystem before roadmap creation. Produces files in .planning/research/ consumed during roadmap creation. Spawned by /gsd:new-project or /gsd:new-milestone orchestrators.',
|
|
25
25
|
color: 'cyan',
|
|
26
26
|
tools:
|
|
27
|
-
'Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*',
|
|
27
|
+
'Read, Write, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*',
|
|
28
28
|
requiredIncludes: [
|
|
29
29
|
'@~/.claude/gsd-core/references/research-documentation-lookup.md',
|
|
30
30
|
'@~/.claude/gsd-core/references/research-philosophy.md',
|
|
@@ -46,7 +46,7 @@ const PROFILES = [
|
|
|
46
46
|
'Researches how to implement a phase before planning. Produces RESEARCH.md consumed by gsd-planner. Spawned by /gsd:plan-phase orchestrator.',
|
|
47
47
|
color: 'cyan',
|
|
48
48
|
tools:
|
|
49
|
-
'Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*',
|
|
49
|
+
'Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*, mcp__perplexity__*',
|
|
50
50
|
requiredIncludes: [
|
|
51
51
|
'@~/.claude/gsd-core/references/research-documentation-lookup.md',
|
|
52
52
|
'@~/.claude/gsd-core/references/research-philosophy.md',
|
|
@@ -68,7 +68,7 @@ const PROFILES = [
|
|
|
68
68
|
description:
|
|
69
69
|
'Researches a single gray area decision and returns a structured comparison table with rationale. Spawned by discuss-phase advisor mode.',
|
|
70
70
|
color: 'cyan',
|
|
71
|
-
tools: 'Read, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*',
|
|
71
|
+
tools: 'Read, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*',
|
|
72
72
|
requiredIncludes: [
|
|
73
73
|
'@~/.claude/gsd-core/references/research-documentation-lookup.md',
|
|
74
74
|
],
|
|
@@ -84,7 +84,7 @@ const PROFILES = [
|
|
|
84
84
|
'Researches a chosen AI framework\'s official docs to produce implementation-ready guidance — best practices, syntax, core patterns, and pitfalls distilled for the specific use case. Writes the Framework Quick Reference and Implementation Guidance sections of AI-SPEC.md. Spawned by /gsd:ai-integration-phase orchestrator.',
|
|
85
85
|
color: 'green',
|
|
86
86
|
tools:
|
|
87
|
-
'Read, Write, Edit, Bash, Grep, Glob, WebFetch, WebSearch, mcp__context7__*',
|
|
87
|
+
'Read, Write, Edit, Bash, Grep, Glob, WebFetch, WebSearch, mcp__context7__*, mcp__plugin_context7_context7__*',
|
|
88
88
|
requiredIncludes: [
|
|
89
89
|
'@~/.claude/gsd-core/references/research-documentation-lookup.md',
|
|
90
90
|
],
|
|
@@ -101,7 +101,7 @@ const PROFILES = [
|
|
|
101
101
|
'Researches the business domain and real-world application context of the AI system being built. Surfaces domain expert evaluation criteria, industry-specific failure modes, regulatory context, and what "good" looks like for practitioners in this field — before the eval-planner turns it into measurable rubrics. Spawned by /gsd:ai-integration-phase orchestrator.',
|
|
102
102
|
color: 'purple',
|
|
103
103
|
tools:
|
|
104
|
-
'Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*',
|
|
104
|
+
'Read, Write, Edit, Bash, Grep, Glob, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*',
|
|
105
105
|
requiredIncludes: [
|
|
106
106
|
'@~/.claude/gsd-core/references/research-documentation-lookup.md',
|
|
107
107
|
],
|
|
@@ -117,7 +117,7 @@ const PROFILES = [
|
|
|
117
117
|
'Produces UI-SPEC.md design contract for frontend phases. Reads upstream artifacts, detects design system state, asks only unanswered questions. Spawned by /gsd:ui-phase orchestrator.',
|
|
118
118
|
color: 'purple',
|
|
119
119
|
tools:
|
|
120
|
-
'Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
|
|
120
|
+
'Read, Write, Edit, Bash, Grep, Glob, Skill, WebSearch, WebFetch, mcp__context7__*, mcp__plugin_context7_context7__*, mcp__firecrawl__*, mcp__exa__*, mcp__tavily__*, mcp__ref__*, mcp__jina__*',
|
|
121
121
|
requiredIncludes: [
|
|
122
122
|
'@~/.claude/gsd-core/references/research-documentation-lookup.md',
|
|
123
123
|
],
|
package/scripts/run-tests.cjs
CHANGED
|
@@ -119,6 +119,51 @@ function ensureBuiltArtifacts(overrides = {}) {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
+
|
|
123
|
+
// hooks/dist/ is gitignored (.gitignore) and NOT built by `prepare`
|
|
124
|
+
// (npm run build:lib only) — only the full `build`/`prepublishOnly` scripts run
|
|
125
|
+
// build:hooks. So on a clean checkout + `npm ci` (fresh CI, incl. the scoped
|
|
126
|
+
// test lane) hooks/dist starts absent. Install tests (e.g.
|
|
127
|
+
// bug-3683-workflow-colon-namespace-leak) spawn `install.js --<runtime> --local`
|
|
128
|
+
// which copies hooks from hooks/dist/ and then verifyInstalled() hard-fails if
|
|
129
|
+
// the target hooks dir is empty. build-hooks.js `build()` creates DIST_DIR
|
|
130
|
+
// empty and fills it file-by-file, so the FIRST on-demand build (triggered by
|
|
131
|
+
// whichever concurrent install test's before() hook runs first) exposes a
|
|
132
|
+
// window where hooks/dist exists but is empty/partial. A concurrently-spawned
|
|
133
|
+
// install reader observes zero hooks -> "Failed to install hooks: directory is
|
|
134
|
+
// empty" -> intermittent scoped-lane failure (full lanes dodge it only by luck
|
|
135
|
+
// of a hooks-builder finishing early). Building hooks/dist ONCE here — the same
|
|
136
|
+
// upfront chokepoint as ensureBuiltArtifacts, single-process with no concurrent
|
|
137
|
+
// readers — fully populates dist before any test runs, closing the first-build
|
|
138
|
+
// empty window everywhere (CI scoped/unit shards + local). Subsequent on-demand
|
|
139
|
+
// rebuilds only atomically replace individual files (per-file rename in
|
|
140
|
+
// build-hooks.js) and never re-empty the dir, so they stay safe.
|
|
141
|
+
function ensureBuiltHooks(overrides = {}) {
|
|
142
|
+
const { existsSync, statSync } = require('fs');
|
|
143
|
+
const root = overrides.root || join(__dirname, '..');
|
|
144
|
+
const distDir = overrides.distDir || join(root, 'hooks', 'dist');
|
|
145
|
+
const hookNames = overrides.hookNames || require('./build-hooks.js').HOOKS_TO_COPY;
|
|
146
|
+
const runBuild = overrides.runBuild || (() => {
|
|
147
|
+
execFileSync(process.execPath, [join(root, 'scripts', 'build-hooks.js')], {
|
|
148
|
+
cwd: root,
|
|
149
|
+
stdio: 'inherit',
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// dist is "complete" only if every expected hook exists as a non-empty file.
|
|
154
|
+
// Absent dir, empty dir, or a missing/zero-byte hook all trigger a rebuild.
|
|
155
|
+
const complete = existsSync(distDir) && hookNames.every((hook) => {
|
|
156
|
+
const p = join(distDir, hook);
|
|
157
|
+
try {
|
|
158
|
+
return existsSync(p) && statSync(p).size > 0;
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
if (!complete) {
|
|
164
|
+
runBuild();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
122
167
|
const MARKED_SUITES = ['integration', 'install', 'security', 'slow'];
|
|
123
168
|
|
|
124
169
|
// Recursively collect *.test.cjs files under dir, returning paths relative to dir.
|
|
@@ -454,6 +499,11 @@ function main() {
|
|
|
454
499
|
// Build the gitignored bin/lib artifact if absent, before any test requires it.
|
|
455
500
|
ensureBuiltArtifacts();
|
|
456
501
|
|
|
502
|
+
// Build the gitignored hooks/dist artifact once, before any concurrent install
|
|
503
|
+
// test spawns install.js and reads it — closes the first-build empty-dir race
|
|
504
|
+
// that intermittently failed the scoped CI lane (see ensureBuiltHooks above).
|
|
505
|
+
ensureBuiltHooks();
|
|
506
|
+
|
|
457
507
|
// Hermeticity: in-process tests resolve `.planning` via planningDir(cwd), which
|
|
458
508
|
// honours GSD_PROJECT/GSD_WORKSTREAM. A developer shell inside a GSD workstream
|
|
459
509
|
// exports GSD_WORKSTREAM, which would redirect fixture STATE.md reads away from
|
|
@@ -512,9 +562,19 @@ function main() {
|
|
|
512
562
|
const MAX_CMDLINE_CHARS = process.env.RUN_TESTS_MAX_CMDLINE_CHARS
|
|
513
563
|
? Number(process.env.RUN_TESTS_MAX_CMDLINE_CHARS)
|
|
514
564
|
: 28000; // headroom below the 32,767 Windows ceiling
|
|
565
|
+
// A full-lane shard (~171 files) fit in ONE chunk at the old cap of 180, so the
|
|
566
|
+
// entire shard's wall-clock ran against a single per-chunk timeout. On the slow
|
|
567
|
+
// Windows runner the install-heavy files in a shard (e.g. install-minimal-hooks
|
|
568
|
+
// .test.cjs alone runs ~250 cases doing dozens of real installs) push that single
|
|
569
|
+
// chunk past the 600s per-chunk backstop — killed mid-run while still making slow
|
|
570
|
+
// progress (verified: no leaked handle / hang; --test-force-exit exits leaks
|
|
571
|
+
// cleanly, so the timeout was pure slowness, NOT the leak the kill message guesses).
|
|
572
|
+
// The per-chunk timeout is sized for a "healthy chunk (~4-5 min)"; keep chunks at
|
|
573
|
+
// roughly half a shard so each gets its own fresh 600s budget and a fresh node
|
|
574
|
+
// process (also relieving per-process memory pressure from 170+ files at once).
|
|
515
575
|
const MAX_FILES_PER_CHUNK = process.env.RUN_TESTS_MAX_FILES_PER_CHUNK
|
|
516
576
|
? Number(process.env.RUN_TESTS_MAX_FILES_PER_CHUNK)
|
|
517
|
-
:
|
|
577
|
+
: 90;
|
|
518
578
|
|
|
519
579
|
// node:test does not exit until the event loop drains. A unit test that leaks
|
|
520
580
|
// an open handle (un-terminated Worker, un-killed child_process, ref'd timer)
|
|
@@ -580,9 +640,12 @@ function main() {
|
|
|
580
640
|
if (timedOut) {
|
|
581
641
|
console.error(
|
|
582
642
|
`run-tests: chunk ${i + 1}/${chunks.length} exceeded the per-chunk timeout ` +
|
|
583
|
-
`of ${chunkTimeoutMs}ms and was killed
|
|
643
|
+
`of ${chunkTimeoutMs}ms and was killed. Two possible causes: (1) a test leaks ` +
|
|
584
644
|
`an open handle (un-terminated Worker, un-killed child process, or ref'd timer) ` +
|
|
585
|
-
`so node --test never exits
|
|
645
|
+
`so node --test never exits — but --test-force-exit already guards that, so if it ` +
|
|
646
|
+
`is enabled suspect (2) the chunk is legitimately too slow for the budget (too ` +
|
|
647
|
+
`many/too-heavy files packed together). Check whether output kept flowing until ` +
|
|
648
|
+
`the kill (slow) vs stopped early (hang) before assuming a leak. Files: ${chunks[i]
|
|
586
649
|
.map(f => f.split(/[\\/]/).pop())
|
|
587
650
|
.join(' ')}`,
|
|
588
651
|
);
|
|
@@ -600,4 +663,4 @@ if (require.main === module) {
|
|
|
600
663
|
runMain(main);
|
|
601
664
|
}
|
|
602
665
|
|
|
603
|
-
module.exports = { suiteOf, ensureBuiltArtifacts, parseShardArg, selectShard };
|
|
666
|
+
module.exports = { suiteOf, ensureBuiltArtifacts, ensureBuiltHooks, parseShardArg, selectShard };
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* slurm-adapter.cjs — SLURM scheduler-adapter helper for the external-job
|
|
6
|
+
* capability (#1164 / #1105).
|
|
7
|
+
*
|
|
8
|
+
* Thin CLI: runs bounded sbatch / squeue / sacct subprocesses and delegates
|
|
9
|
+
* all parsing, manifest build/validate, and fail-closed writing to the pure
|
|
10
|
+
* module (gsd-core/bin/lib/external-job.cjs). The pure module is fully unit-
|
|
11
|
+
* tested; this script is the operator surface that needs a real cluster.
|
|
12
|
+
*
|
|
13
|
+
* Subcommands:
|
|
14
|
+
* submit --plan <plan_id> --phase <phase> --expected <path>[,<path>] \
|
|
15
|
+
* --verify <cmd> --resume <cmd> -- <sbatch...>
|
|
16
|
+
* poll --job <job_id> [--plan <plan_id>]
|
|
17
|
+
* show --job <job_id> (surface manifest status + commands; no auto-run)
|
|
18
|
+
*
|
|
19
|
+
* Trust boundary: this script never auto-runs verification_command or
|
|
20
|
+
* resume_command from a manifest (planning-artifacts.md). `show` prints them
|
|
21
|
+
* for explicit operator confirmation.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const { execFileSync } = require('node:child_process');
|
|
25
|
+
const fs = require('node:fs');
|
|
26
|
+
const path = require('node:path');
|
|
27
|
+
|
|
28
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
29
|
+
const m = require('../gsd-core/bin/lib/external-job.cjs');
|
|
30
|
+
|
|
31
|
+
// #1164 refinements B + C: the adapter now resolves external_job.* settings
|
|
32
|
+
// through the canonical capability-config seam (resolveConfigKey in
|
|
33
|
+
// capability-activation.cjs), which walks loadConfig -> workstream config.json
|
|
34
|
+
// -> root config.json -> registry configSchema default. Env vars remain the
|
|
35
|
+
// top-precedence override so cluster operators can tune without editing config.
|
|
36
|
+
const { resolveConfigKey } = require('../gsd-core/bin/lib/capability-activation.cjs');
|
|
37
|
+
const FALLBACK_SUBMIT_TIMEOUT_MS = 30000;
|
|
38
|
+
const FALLBACK_POLL_TIMEOUT_MS = 15000;
|
|
39
|
+
const FALLBACK_ARTIFACT_DIR = 'Artifacts/jobs';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the external_job.* runtime settings. Pure given {config, env,
|
|
43
|
+
* registry}; degrades to hardcoded fallbacks if the registry/config are absent
|
|
44
|
+
* so the adapter never unbounds a subprocess (CLAUDE.md bounded-subprocess).
|
|
45
|
+
*
|
|
46
|
+
* Precedence: env override > nested config value > registry default > fallback.
|
|
47
|
+
*/
|
|
48
|
+
function resolveExternalJobSettings({ cwd, env, config, registry } = {}) {
|
|
49
|
+
const reg = registry || {};
|
|
50
|
+
let cfg = config;
|
|
51
|
+
if (cfg === undefined) {
|
|
52
|
+
try {
|
|
53
|
+
cfg = require('../gsd-core/bin/lib/config-loader.cjs').loadConfig(cwd);
|
|
54
|
+
} catch {
|
|
55
|
+
cfg = {}; // resolveConfigKey still falls back to the registry default
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const e = env || {};
|
|
59
|
+
const submitEnv = e.GSD_SLURM_SUBMIT_TIMEOUT_MS;
|
|
60
|
+
const pollEnv = e.GSD_SLURM_POLL_TIMEOUT_MS;
|
|
61
|
+
const artifactEnv = e.GSD_EXTERNAL_JOB_ARTIFACT_DIR;
|
|
62
|
+
const submit = submitEnv ? Number(submitEnv)
|
|
63
|
+
: _num(resolveConfigKey('external_job.submit_timeout_ms', { config: cfg, cwd, registry: reg }), FALLBACK_SUBMIT_TIMEOUT_MS);
|
|
64
|
+
const poll = pollEnv ? Number(pollEnv)
|
|
65
|
+
: _num(resolveConfigKey('external_job.poll_timeout_ms', { config: cfg, cwd, registry: reg }), FALLBACK_POLL_TIMEOUT_MS);
|
|
66
|
+
const artifactDir = artifactEnv
|
|
67
|
+
|| _str(resolveConfigKey('external_job.artifact_dir', { config: cfg, cwd, registry: reg }), FALLBACK_ARTIFACT_DIR);
|
|
68
|
+
return { submitTimeoutMs: submit, pollTimeoutMs: poll, artifactDir };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function _num(res, fallback) {
|
|
72
|
+
const v = res && res.found ? res.value : undefined;
|
|
73
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : fallback;
|
|
74
|
+
}
|
|
75
|
+
function _str(res, fallback) {
|
|
76
|
+
const v = res && res.found ? res.value : undefined;
|
|
77
|
+
return typeof v === 'string' && v.length > 0 ? v : fallback;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function usage() {
|
|
81
|
+
return [
|
|
82
|
+
'usage: slurm-adapter.cjs <submit|poll|show> ...',
|
|
83
|
+
' submit --plan <id> --phase <n> --expected <p1,p2> --verify <cmd> --resume <cmd> -- sbatch --parsable ...',
|
|
84
|
+
' poll --job <job_id> [--plan <plan_id>]',
|
|
85
|
+
' show --job <job_id>',
|
|
86
|
+
].join('\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseFlags(argv) {
|
|
90
|
+
const out = {};
|
|
91
|
+
const rest = [];
|
|
92
|
+
for (let i = 0; i < argv.length; i++) {
|
|
93
|
+
const a = argv[i];
|
|
94
|
+
if (a === '--') { out['--'] = argv.slice(i + 1); break; }
|
|
95
|
+
if (a.startsWith('--')) {
|
|
96
|
+
const v = argv[i + 1];
|
|
97
|
+
out[a.slice(2)] = v;
|
|
98
|
+
i++;
|
|
99
|
+
} else {
|
|
100
|
+
rest.push(a);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
out._ = rest;
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function findPlanningDir(start) {
|
|
108
|
+
let dir = path.resolve(start || process.cwd());
|
|
109
|
+
for (let i = 0; i < 10; i++) {
|
|
110
|
+
if (fs.existsSync(path.join(dir, '.planning'))) return path.join(dir, '.planning');
|
|
111
|
+
const parent = path.dirname(dir);
|
|
112
|
+
if (parent === dir) break;
|
|
113
|
+
dir = parent;
|
|
114
|
+
}
|
|
115
|
+
throw new ExitError(1, 'could not locate a .planning directory (walked up 10 levels)');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function cmdSubmit(flags) {
|
|
119
|
+
const plan = flags.plan;
|
|
120
|
+
const phase = flags.phase;
|
|
121
|
+
const sbatchCmd = flags['--'];
|
|
122
|
+
if (!plan || phase === undefined || !Array.isArray(sbatchCmd) || sbatchCmd.length === 0) {
|
|
123
|
+
throw new ExitError(1, 'submit requires --plan, --phase, and an sbatch command after --\n' + usage());
|
|
124
|
+
}
|
|
125
|
+
const expected = (flags.expected || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
126
|
+
if (expected.length === 0) throw new ExitError(1, 'submit requires --expected (comma-separated artifact paths)');
|
|
127
|
+
const verify = flags.verify;
|
|
128
|
+
const resume = flags.resume || ('/gsd:execute-phase ' + phase);
|
|
129
|
+
if (!verify) throw new ExitError(1, 'submit requires --verify (the command that verifies job output)');
|
|
130
|
+
|
|
131
|
+
// Resolve settings from config/env before any subprocess (env > config > default).
|
|
132
|
+
const planningDir = findPlanningDir();
|
|
133
|
+
const settings = resolveExternalJobSettings({
|
|
134
|
+
cwd: path.dirname(planningDir),
|
|
135
|
+
env: process.env,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
let stdout;
|
|
139
|
+
try {
|
|
140
|
+
stdout = execFileSync(sbatchCmd[0], sbatchCmd.slice(1), {
|
|
141
|
+
encoding: 'utf8',
|
|
142
|
+
timeout: settings.submitTimeoutMs,
|
|
143
|
+
maxBuffer: 1024 * 1024,
|
|
144
|
+
});
|
|
145
|
+
} catch (e) {
|
|
146
|
+
throw new ExitError(1, 'sbatch failed: ' + (e.message || String(e)));
|
|
147
|
+
}
|
|
148
|
+
const parsed = m.parseSbatchParsable(stdout);
|
|
149
|
+
if (!parsed.ok) {
|
|
150
|
+
throw new ExitError(1, 'could not parse sbatch --parsable output (kind=' + parsed.kind + '): ' + parsed.raw);
|
|
151
|
+
}
|
|
152
|
+
const manifest = m.buildManifest({
|
|
153
|
+
plan_id: plan,
|
|
154
|
+
phase,
|
|
155
|
+
job_id: parsed.job_id,
|
|
156
|
+
backend: 'slurm',
|
|
157
|
+
submit_command: sbatchCmd.join(' '),
|
|
158
|
+
status: 'submitted',
|
|
159
|
+
expected_artifacts: expected,
|
|
160
|
+
verification_command: verify,
|
|
161
|
+
resume_command: resume,
|
|
162
|
+
});
|
|
163
|
+
const res = m.writeManifest(manifest, planningDir);
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
throw new ExitError(1, 'writeManifest refused (' + res.kind + '): ' + res.message);
|
|
166
|
+
}
|
|
167
|
+
process.stdout.write('submitted job ' + parsed.job_id + ' for plan ' + plan + '\n');
|
|
168
|
+
process.stdout.write('manifest: ' + res.path + '\n');
|
|
169
|
+
process.stdout.write('artifact_dir: ' + settings.artifactDir + '\n');
|
|
170
|
+
process.stdout.write('state: external_job_waiting (SUMMARY deferred)\n');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function cmdPoll(flags) {
|
|
174
|
+
const jobId = flags.job;
|
|
175
|
+
if (!jobId) throw new ExitError(1, 'poll requires --job <job_id>\n' + usage());
|
|
176
|
+
const planningDir = findPlanningDir();
|
|
177
|
+
const settings = resolveExternalJobSettings({
|
|
178
|
+
cwd: path.dirname(planningDir),
|
|
179
|
+
env: process.env,
|
|
180
|
+
});
|
|
181
|
+
const manifestFile = m.manifestPath(planningDir, jobId);
|
|
182
|
+
if (!fs.existsSync(manifestFile)) {
|
|
183
|
+
throw new ExitError(1, 'no manifest for job ' + jobId + ' at ' + manifestFile);
|
|
184
|
+
}
|
|
185
|
+
const existing = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
186
|
+
|
|
187
|
+
let rawState = null;
|
|
188
|
+
try {
|
|
189
|
+
const out = execFileSync('squeue', ['-h', '-j', jobId, '-o', '%i %T'], {
|
|
190
|
+
encoding: 'utf8', timeout: settings.pollTimeoutMs, maxBuffer: 1024 * 1024,
|
|
191
|
+
}).trim();
|
|
192
|
+
const line = out.split('\n')[0];
|
|
193
|
+
const parsed = m.parseSqueueLine(line || '');
|
|
194
|
+
if (parsed) rawState = parsed.state;
|
|
195
|
+
} catch (_e) { /* squeue empty/failed — fall back to sacct */ }
|
|
196
|
+
if (!rawState) {
|
|
197
|
+
try {
|
|
198
|
+
const out = execFileSync('sacct', ['-X', '-P', '-j', jobId, '-o', 'JobID,State'], {
|
|
199
|
+
encoding: 'utf8', timeout: settings.pollTimeoutMs, maxBuffer: 1024 * 1024,
|
|
200
|
+
}).trim();
|
|
201
|
+
for (const line of out.split('\n').slice(1)) {
|
|
202
|
+
const parsed = m.parseSacctRow(line.split('|'));
|
|
203
|
+
if (parsed) { rawState = parsed.state; break; }
|
|
204
|
+
}
|
|
205
|
+
} catch (e) {
|
|
206
|
+
throw new ExitError(1, 'both squeue and sacct failed: ' + (e.message || String(e)));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const mapped = m.mapSlurmState(rawState || '');
|
|
210
|
+
if (!mapped) throw new ExitError(1, 'unmapped SLURM state "' + rawState + '" — not guessing; inspect manually');
|
|
211
|
+
|
|
212
|
+
const updated = m.buildManifest(
|
|
213
|
+
Object.assign({}, existing, { status: mapped, terminal_details: existing.terminal_details || null }),
|
|
214
|
+
{ clock: { nowIso: () => existing.submitted_at } },
|
|
215
|
+
);
|
|
216
|
+
const res = m.writeManifest(updated, planningDir);
|
|
217
|
+
if (!res.ok) throw new ExitError(1, 'writeManifest refused (' + res.kind + '): ' + res.message);
|
|
218
|
+
process.stdout.write(JSON.stringify({ job_id: jobId, slurm_state: rawState, manifest_status: mapped, path: res.path }) + '\n');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function formatShowReport(manifest) {
|
|
222
|
+
const lines = [];
|
|
223
|
+
lines.push('job ' + manifest.job_id + ' (plan ' + manifest.plan_id + ', backend ' + manifest.backend + ')');
|
|
224
|
+
lines.push('status: ' + manifest.status);
|
|
225
|
+
if (manifest.terminal_details) {
|
|
226
|
+
lines.push('terminal_details: ' + JSON.stringify(manifest.terminal_details));
|
|
227
|
+
}
|
|
228
|
+
// Trust boundary: surface commands for confirmation, never auto-run.
|
|
229
|
+
lines.push('');
|
|
230
|
+
lines.push('Manifest commands (UNTRUSTED — confirm before running):');
|
|
231
|
+
lines.push(' submit_command: ' + manifest.submit_command);
|
|
232
|
+
lines.push(' verification_command: ' + manifest.verification_command);
|
|
233
|
+
lines.push(' resume_command: ' + manifest.resume_command);
|
|
234
|
+
return lines.join('\n') + '\n';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function cmdShow(flags) {
|
|
238
|
+
const jobId = flags.job;
|
|
239
|
+
if (!jobId) throw new ExitError(1, 'show requires --job <job_id>\n' + usage());
|
|
240
|
+
const planningDir = findPlanningDir();
|
|
241
|
+
const manifestFile = m.manifestPath(planningDir, jobId);
|
|
242
|
+
if (!fs.existsSync(manifestFile)) {
|
|
243
|
+
throw new ExitError(1, 'no manifest for job ' + jobId + ' at ' + manifestFile);
|
|
244
|
+
}
|
|
245
|
+
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
246
|
+
process.stdout.write(formatShowReport(manifest));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function main() {
|
|
250
|
+
const [, , sub, ...rest] = process.argv;
|
|
251
|
+
const flags = parseFlags(rest);
|
|
252
|
+
if (sub === 'submit') cmdSubmit(flags);
|
|
253
|
+
else if (sub === 'poll') cmdPoll(flags);
|
|
254
|
+
else if (sub === 'show') cmdShow(flags);
|
|
255
|
+
else throw new ExitError(1, usage());
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (require.main === module) {
|
|
260
|
+
runMain(main);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = {
|
|
264
|
+
parseFlags,
|
|
265
|
+
findPlanningDir,
|
|
266
|
+
resolveExternalJobSettings,
|
|
267
|
+
formatShowReport,
|
|
268
|
+
ExitError,
|
|
269
|
+
};
|
|
@@ -21,18 +21,67 @@ const { execFileSync } = require('child_process');
|
|
|
21
21
|
|
|
22
22
|
const ROOT = path.resolve(__dirname, '..');
|
|
23
23
|
|
|
24
|
-
// Single source of truth: runtime-integration manifests whose
|
|
25
|
-
// MUST track package.json.
|
|
26
|
-
//
|
|
24
|
+
// Single source of truth: runtime-integration manifests whose `version` field
|
|
25
|
+
// MUST track package.json. Each entry names the manifest path plus the dotted
|
|
26
|
+
// path to the version field inside that JSON document. Top-level `version` is
|
|
27
|
+
// the default (plugin.json); the Claude plugin
|
|
28
|
+
// marketplace manifest carries its canonical version at plugins[0].version
|
|
29
|
+
// (the schema-canonical location runtimes read — issue #1855).
|
|
30
|
+
//
|
|
31
|
+
// Add a new manifest here so `npm version` keeps it in sync — the regression
|
|
32
|
+
// guard test (issue 844) fails if you forget.
|
|
33
|
+
// #1928: gemini-extension.json was removed with the gemini runtime (Google
|
|
34
|
+
// sunset Gemini CLI 2026-06-18); it is no longer a registered manifest.
|
|
27
35
|
const VERSIONED_MANIFESTS = [
|
|
28
|
-
'.claude-plugin/plugin.json',
|
|
29
|
-
'
|
|
36
|
+
{ path: '.claude-plugin/plugin.json', versionKey: 'version' },
|
|
37
|
+
{ path: '.claude-plugin/marketplace.json', versionKey: 'plugins.0.version' },
|
|
38
|
+
{ path: 'vscode/package.json', versionKey: 'version' },
|
|
30
39
|
];
|
|
31
40
|
|
|
41
|
+
// Convenience: just the registered paths, for consumers that only need to
|
|
42
|
+
// iterate files (e.g. the issue-844 regression-guard ALLOWED set).
|
|
43
|
+
const VERSIONED_MANIFEST_PATHS = VERSIONED_MANIFESTS.map((e) => e.path);
|
|
44
|
+
|
|
32
45
|
function readJson(p) {
|
|
33
46
|
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
34
47
|
}
|
|
35
48
|
|
|
49
|
+
// Property names that must never be traversed/assigned through a dotted path —
|
|
50
|
+
// the prototype-pollution triple. Mirrors the _isSafePropKey CodeQL barrier used
|
|
51
|
+
// elsewhere in the repo. Versions are hand-authored descriptors, but the helpers
|
|
52
|
+
// are exported, so guard them before reuse across a trust boundary.
|
|
53
|
+
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
54
|
+
function assertSafePath(dotPath) {
|
|
55
|
+
for (const seg of String(dotPath).split('.')) {
|
|
56
|
+
if (UNSAFE_KEYS.has(seg)) {
|
|
57
|
+
throw new Error(`refusing to traverse reserved property "${seg}" in version path "${dotPath}"`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Read a dotted path ('plugins.0.version') from a parsed JSON document.
|
|
63
|
+
// Returns undefined if any intermediate is missing. Array indices are plain
|
|
64
|
+
// numeric keys, so 'plugins.0.version' resolves obj.plugins[0].version.
|
|
65
|
+
// Reserved properties (__proto__/constructor/prototype) are rejected.
|
|
66
|
+
function getByPath(obj, dotPath) {
|
|
67
|
+
assertSafePath(dotPath);
|
|
68
|
+
return String(dotPath).split('.').reduce((cur, key) => (cur == null ? cur : cur[key]), obj);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Write a value at a dotted path. Intermediate nodes must already exist (every
|
|
72
|
+
// registered manifest is hand-authored with its version slot present, so this
|
|
73
|
+
// never needs to materialize a path). Reserved properties are rejected. Kept
|
|
74
|
+
// trivial — no eval, no creation.
|
|
75
|
+
function setByPath(obj, dotPath, value) {
|
|
76
|
+
assertSafePath(dotPath);
|
|
77
|
+
const parts = String(dotPath).split('.');
|
|
78
|
+
let cur = obj;
|
|
79
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
80
|
+
cur = cur[parts[i]];
|
|
81
|
+
}
|
|
82
|
+
cur[parts[parts.length - 1]] = value;
|
|
83
|
+
}
|
|
84
|
+
|
|
36
85
|
function getPackageVersion(root) {
|
|
37
86
|
const r = root || ROOT;
|
|
38
87
|
return readJson(path.join(r, 'package.json')).version;
|
|
@@ -44,13 +93,13 @@ function syncManifestVersions(opts) {
|
|
|
44
93
|
const root = (opts && opts.root) || ROOT;
|
|
45
94
|
const v = (opts && opts.version) != null ? opts.version : getPackageVersion(root);
|
|
46
95
|
const changed = [];
|
|
47
|
-
for (const
|
|
48
|
-
const abs = path.join(root,
|
|
96
|
+
for (const entry of VERSIONED_MANIFESTS) {
|
|
97
|
+
const abs = path.join(root, entry.path);
|
|
49
98
|
const manifest = readJson(abs);
|
|
50
|
-
if (manifest.
|
|
51
|
-
manifest.
|
|
99
|
+
if (getByPath(manifest, entry.versionKey) !== v) {
|
|
100
|
+
setByPath(manifest, entry.versionKey, v);
|
|
52
101
|
fs.writeFileSync(abs, JSON.stringify(manifest, null, 2) + '\n');
|
|
53
|
-
changed.push(
|
|
102
|
+
changed.push(entry.path);
|
|
54
103
|
}
|
|
55
104
|
}
|
|
56
105
|
return changed;
|
|
@@ -61,9 +110,10 @@ function findDrift(opts) {
|
|
|
61
110
|
const root = (opts && opts.root) || ROOT;
|
|
62
111
|
const v = (opts && opts.version) != null ? opts.version : getPackageVersion(root);
|
|
63
112
|
const drift = [];
|
|
64
|
-
for (const
|
|
65
|
-
const
|
|
66
|
-
|
|
113
|
+
for (const entry of VERSIONED_MANIFESTS) {
|
|
114
|
+
const manifest = readJson(path.join(root, entry.path));
|
|
115
|
+
const found = getByPath(manifest, entry.versionKey);
|
|
116
|
+
if (found !== v) drift.push({ manifest: entry.path, found, expected: v });
|
|
67
117
|
}
|
|
68
118
|
return drift;
|
|
69
119
|
}
|
|
@@ -143,7 +193,7 @@ function stageManifests(opts) {
|
|
|
143
193
|
console.warn('sync-manifest-versions: not a git work tree; skipping staging.');
|
|
144
194
|
return;
|
|
145
195
|
}
|
|
146
|
-
const toStage = [...
|
|
196
|
+
const toStage = [...VERSIONED_MANIFEST_PATHS, ...listCapabilityManifests({ root })];
|
|
147
197
|
try {
|
|
148
198
|
execFileSync('git', ['add', '--', ...toStage], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
149
199
|
} catch (err) {
|
|
@@ -154,6 +204,9 @@ function stageManifests(opts) {
|
|
|
154
204
|
|
|
155
205
|
module.exports = {
|
|
156
206
|
VERSIONED_MANIFESTS,
|
|
207
|
+
VERSIONED_MANIFEST_PATHS,
|
|
208
|
+
getByPath,
|
|
209
|
+
setByPath,
|
|
157
210
|
syncManifestVersions,
|
|
158
211
|
findDrift,
|
|
159
212
|
getPackageVersion,
|
|
@@ -56,7 +56,7 @@ On any error or timeout, stop and let the phase continue -- capture is best-effo
|
|
|
56
56
|
|
|
57
57
|
1. **Dedup first.** Interactive: `mempalace_check_duplicate` on the artifact's deterministic drawer id. Headless: rely on `mempalace mine`'s content-hash idempotency.
|
|
58
58
|
2. **Add the drawer (verbatim).** File the exact artifact text into `room: <room>` of `wing: <wing>` with provenance (`source_file`, phase id). Interactive: `mempalace_add_drawer`. Headless: `mempalace mine <path> --wing <wing> --room <room>`.
|
|
59
|
-
3. **Mirror KG facts** when `config.mempalace.mirror_kg` is true: extract decision/delivery facts and `mempalace_kg_add` them with `valid_from` = the phase date (e.g. `(<project>, decided, <decision>)` from CONTEXT; `(<phase>, delivered, <capability>)` from SUMMARY).
|
|
59
|
+
3. **Mirror KG facts** when `config.mempalace.mirror_kg` is true: extract decision/delivery facts and `mempalace_kg_add` them with `valid_from` = the phase date (e.g. `(<project>, decided, <decision>)` from CONTEXT; `(<phase>, delivered, <capability>)` from SUMMARY). Under `augment` these are an *additive* mirror of GSD's native `.planning/graphs/`. Under `kg_backend`/`replace` the palace KG is the *authoritative* fact store — GSD still produces `.planning/graphs/` through its normal graphify, so an unreachable palace never loses a fact.
|
|
60
60
|
4. Re-running a phase MUST NOT create duplicate drawers (deterministic ids + `check_duplicate`).
|
|
61
61
|
|
|
62
62
|
## Step 4 -- Report
|
|
@@ -48,13 +48,17 @@ This step is `onError: skip` at `plan:pre` -- recall never blocks planning.
|
|
|
48
48
|
## Step 2 -- Resolve wing, mode, and transport
|
|
49
49
|
|
|
50
50
|
1. **Wing.** Use `config.mempalace.wing` if non-empty; otherwise derive from `config.project_code`; otherwise fall back to the repository directory name.
|
|
51
|
-
2. **Mode.** Read `config.mempalace.memory_mode` (`augment` | `kg_backend` | `replace`, default `augment`).
|
|
51
|
+
2. **Mode.** Read `config.mempalace.memory_mode` (`augment` | `kg_backend` | `replace`, default `augment`). It sets how authoritative the palace is during recall:
|
|
52
|
+
- `augment` — the palace is an *additional* layer; read native memory (`.planning/graphs/`, STATE) too and treat the palace as supplementary.
|
|
53
|
+
- `kg_backend` — for knowledge-graph facts, query the palace's temporal KG *first, as the primary source*; fall back to `.planning/graphs/` when the palace is unreachable. Non-KG drawer recall stays additive.
|
|
54
|
+
- `replace` — resolve recall *through the palace as the source of truth*; consult native artifacts only as a fallback when the palace is unreachable.
|
|
55
|
+
In every mode an unreachable palace degrades to native memory — recall never blocks (`onError: skip`).
|
|
52
56
|
3. **Transport.** Prefer the **MCP tools** (`mempalace_*`) in interactive runs *when your MemPalace MCP server is registered and your runtime permits those tools*. Otherwise — headless/cron/autonomous runs, or runtimes that don't grant the MemPalace MCP tools — use the **CLI** (`mempalace wake-up`, `mempalace search`), which this skill's `Bash` allow-tool always covers. If neither is reachable, go to Step 4.
|
|
53
57
|
4. **Topic.** Read the phase `CONTEXT.md` (the consumed artifact). Derive a short search query from its title, goal, and key decisions.
|
|
54
58
|
|
|
55
59
|
## Step 3 -- Retrieve (read-only)
|
|
56
60
|
|
|
57
|
-
All calls in this step are side-effect-free. On any error or timeout, stop retrieving and write whatever was gathered (or the stub) -- never raise.
|
|
61
|
+
All calls in this step are side-effect-free. On any error or timeout, stop retrieving and write whatever was gathered (or the stub) -- never raise. This skill reads only the palace; GSD's planner reads native memory (`.planning/graphs/`, STATE) regardless. So under `kg_backend`/`replace` an unreachable palace falls back to that native memory automatically — reflect that in the stub (Step 4) rather than implying memory is gone.
|
|
58
62
|
|
|
59
63
|
1. **Wake up** (cheap, ~600--900 tokens):
|
|
60
64
|
- Interactive: read the wing identity/summary, then `mempalace_search`.
|
|
@@ -62,7 +66,7 @@ All calls in this step are side-effect-free. On any error or timeout, stop retri
|
|
|
62
66
|
2. **Targeted search:**
|
|
63
67
|
- Interactive: `mempalace_search(query=<topic>, wing=<wing>)`.
|
|
64
68
|
- Headless: `mempalace search "<topic>" --wing <wing>`.
|
|
65
|
-
3. **Knowledge-graph facts** (when `config.mempalace.mirror_kg` is true): `mempalace_kg_query` / `mempalace_kg_timeline` for decisions relevant to the topic and their validity windows.
|
|
69
|
+
3. **Knowledge-graph facts** (when `config.mempalace.mirror_kg` is true): `mempalace_kg_query` / `mempalace_kg_timeline` for decisions relevant to the topic and their validity windows. Under `augment` the palace KG *supplements* GSD's native `.planning/graphs/` — combine both, do not treat the palace as the sole source. Under `kg_backend` or `replace` the palace KG is the *primary* graph source — query it first and use `.planning/graphs/` only as a fallback when the palace is unreachable.
|
|
66
70
|
4. **Dedup** the returned drawers/facts; keep the top results.
|
|
67
71
|
|
|
68
72
|
## Step 4 -- Write MEMORY-RECALL.md
|
|
@@ -86,12 +90,12 @@ _Wing: <wing> · Mode: <mode> · Transport: <mcp|cli>_
|
|
|
86
90
|
- <surprise> — <provenance>
|
|
87
91
|
```
|
|
88
92
|
|
|
89
|
-
When MemPalace is unreachable, write the stub and continue:
|
|
93
|
+
When MemPalace is unreachable, write the stub and continue. Under `kg_backend`/`replace`, name the native fallback so the planner knows memory is not gone — only un-augmented by the palace this run:
|
|
90
94
|
|
|
91
95
|
```markdown
|
|
92
96
|
# Memory Recall (MemPalace)
|
|
93
97
|
|
|
94
|
-
_MemPalace unavailable at recall time —
|
|
98
|
+
_MemPalace unavailable at recall time — falling back to GSD native memory (`.planning/graphs/`, STATE). No palace recall this run._
|
|
95
99
|
```
|
|
96
100
|
|
|
97
101
|
## Anti-Patterns
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gsd-next
|
|
3
|
+
description: "Smart entry — detect project state and route to the right next GSD action."
|
|
4
|
+
effort: low
|
|
5
|
+
allowed-tools:
|
|
6
|
+
- Read
|
|
7
|
+
- Bash
|
|
8
|
+
- Glob
|
|
9
|
+
- SlashCommand
|
|
10
|
+
- AskUserQuestion
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<objective>
|
|
14
|
+
GSD smart entry — the state-aware front door. Detect what's going on in this project, then present a short menu of the right next actions and dispatch to one.
|
|
15
|
+
|
|
16
|
+
This is a launcher/router only. It never does the work itself. It reads project + workflow state via `gsd-tools smart-entry --json`, shows a situation-appropriate menu, and hands off to an existing GSD command.
|
|
17
|
+
</objective>
|
|
18
|
+
|
|
19
|
+
<execution_context>
|
|
20
|
+
@~/.claude/gsd-core/workflows/smart-entry.md
|
|
21
|
+
@~/.claude/gsd-core/references/ui-brand.md
|
|
22
|
+
</execution_context>
|
|
23
|
+
|
|
24
|
+
<context>
|
|
25
|
+
Arguments: $ARGUMENTS
|
|
26
|
+
</context>
|
|
27
|
+
|
|
28
|
+
<process>
|
|
29
|
+
Follow ~/.claude/gsd-core/workflows/smart-entry.md. Detect the situation, present the menu, and dispatch exactly one command. Then stop.
|
|
30
|
+
</process>
|
|
@@ -10,7 +10,9 @@ allowed-tools:
|
|
|
10
10
|
Route to the appropriate phase-pipeline skill based on the user's intent.
|
|
11
11
|
Sub-skill names below are post-#2790 consolidated targets — `gsd-phase`
|
|
12
12
|
absorbs the former add/insert/remove/edit-phase commands and `gsd-progress`
|
|
13
|
-
absorbs the former next/do commands.
|
|
13
|
+
absorbs the former next/do workflow-advance commands. The reclaimed
|
|
14
|
+
`gsd-next` target is the state-aware smart-entry launcher, not the retired
|
|
15
|
+
workflow-advance command.
|
|
14
16
|
|
|
15
17
|
| User wants | Invoke |
|
|
16
18
|
|---|---|
|
|
@@ -21,6 +23,7 @@ absorbs the former next/do commands.
|
|
|
21
23
|
| Verify built features through UAT | gsd-verify-work |
|
|
22
24
|
| Add / insert / remove / edit a phase | gsd-phase |
|
|
23
25
|
| Advance to the next logical step | gsd-progress |
|
|
26
|
+
| Open the state-aware smart-entry launcher | gsd-next |
|
|
24
27
|
| Offload planning to the ultraplan cloud | gsd-ultraplan-phase |
|
|
25
28
|
| Cross-AI plan review convergence loop | gsd-plan-review-convergence |
|
|
26
29
|
| Generate tests for a completed phase | gsd-add-tests |
|