@opengsd/gsd-core 1.7.0-rc.2 → 1.7.0-rc.4
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/GEMINI.md +25 -23
- package/README.ja-JP.md +4 -3
- package/README.ko-KR.md +4 -3
- package/README.md +7 -6
- package/README.pt-BR.md +4 -3
- package/README.zh-CN.md +4 -3
- package/agents/gsd-advisor-researcher.md +1 -1
- package/agents/gsd-ai-researcher.md +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-debug-session-manager.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-domain-researcher.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +4 -4
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +1 -1
- package/agents/gsd-phase-researcher.md +2 -2
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +2 -2
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-ui-researcher.md +2 -2
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +132 -464
- package/commands/gsd/map-codebase.md +4 -4
- 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-project.md +2 -1
- package/commands/gsd/ns-workflow.md +5 -2
- package/commands/gsd/onboard.md +46 -0
- package/gsd-core/bin/ensure-runtime-build.cjs +246 -0
- package/gsd-core/bin/gsd-tools.cjs +72 -3
- package/gsd-core/bin/lib/capability-registry.cjs +684 -264
- package/gsd-core/bin/lib/capability-state.cjs +117 -18
- package/gsd-core/bin/lib/capability-writer.cjs +11 -1
- package/gsd-core/bin/lib/check-command-router.cjs +111 -1
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +136 -0
- package/gsd-core/bin/lib/claude-orchestration.cjs +404 -0
- package/gsd-core/bin/lib/clusters.cjs +2 -0
- package/gsd-core/bin/lib/command-aliases.cjs +8 -0
- 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/gate-predicate-evaluator.cjs +151 -0
- package/gsd-core/bin/lib/graphify.cjs +41 -9
- package/gsd-core/bin/lib/init-command-router.cjs +4 -0
- package/gsd-core/bin/lib/init.cjs +25 -69
- package/gsd-core/bin/lib/install-engine.cjs +8 -5
- package/gsd-core/bin/lib/install-profiles.cjs +2 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +7 -1
- package/gsd-core/bin/lib/milestone.cjs +7 -1
- package/gsd-core/bin/lib/model-resolver.cjs +69 -4
- package/gsd-core/bin/lib/onboard-projection.cjs +309 -0
- package/gsd-core/bin/lib/phase.cjs +14 -2
- package/gsd-core/bin/lib/plan-scan.cjs +9 -1
- 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.cjs +17 -2
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +18 -13
- package/gsd-core/bin/lib/runtime-name-policy.cjs +11 -11
- 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/surface.cjs +63 -33
- package/gsd-core/bin/lib/update-context.cjs +0 -5
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/bin/shared/model-catalog.json +5 -5
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +0 -5
- package/gsd-core/references/gates.md +1 -1
- package/gsd-core/references/gsd-run-resolver.md +8 -0
- package/gsd-core/references/planning-config.md +1 -1
- package/gsd-core/templates/project.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 +1 -1
- 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 +1 -1
- 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 +5 -4
- 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 +4 -5
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/explore.md +1 -1
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- package/gsd-core/workflows/help/modes/brief.md +2 -1
- package/gsd-core/workflows/help/modes/default.md +2 -1
- package/gsd-core/workflows/help/modes/full.md +25 -1
- package/gsd-core/workflows/help/modes/topic.md +2 -1
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-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 +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +1 -1
- package/gsd-core/workflows/new-project.md +1 -1
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/onboard.md +277 -0
- 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/prd-express-path.md +1 -1
- package/gsd-core/workflows/plan-phase.md +9 -2
- 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 +21 -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 +43 -7
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +3 -1
- 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 +62 -4
- package/package.json +3 -5
- package/scripts/ci-test-scope.cjs +19 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +77 -0
- 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 +18 -3
- package/scripts/slurm-adapter.cjs +269 -0
- package/scripts/sync-manifest-versions.cjs +3 -2
- package/scripts/sync-runtime-launcher.cjs +20 -2
- package/skills/gsd-map-codebase/SKILL.md +3 -3
- 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-project/SKILL.md +1 -0
- package/skills/gsd-ns-workflow/SKILL.md +4 -1
- package/skills/gsd-onboard/SKILL.md +46 -0
- package/gemini-extension.json +0 -6
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* external-job.cts — scheduler-adapter producer module for the async
|
|
4
|
+
* external-job contract (#1164 / #1105).
|
|
5
|
+
*
|
|
6
|
+
* The CORE loop CONSUMES manifests at `.planning/async-jobs/<job>.json`
|
|
7
|
+
* (#1165 — external_job_waiting half-state); this module is the Capability
|
|
8
|
+
* half that PRODUCES them. SLURM is the first backend; the design stays
|
|
9
|
+
* scheduler-pluggable via the `backend` field (planning-artifacts.md).
|
|
10
|
+
*
|
|
11
|
+
* Pure helpers (state map, build, validate, parsers) take no I/O; the writer
|
|
12
|
+
* takes injected `fs` and `clock` seams so tests drive it without touching disk
|
|
13
|
+
* or wall-clock time (CLAUDE.md clock-seam + injectable-deps conventions).
|
|
14
|
+
*
|
|
15
|
+
* Build: `src/*.cts` -> `gsd-core/bin/lib/*.cjs` (ADR-457 build-at-publish).
|
|
16
|
+
*/
|
|
17
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
20
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
21
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
22
|
+
// ─── Closed status enum (stability contract — Hyrum's Law) ────────────────────
|
|
23
|
+
const MANIFEST_VERSION = '1.0';
|
|
24
|
+
const MANIFEST_STATUS = [
|
|
25
|
+
'submitted',
|
|
26
|
+
'running',
|
|
27
|
+
'completed-unverified',
|
|
28
|
+
'failed',
|
|
29
|
+
'cancelled',
|
|
30
|
+
'timeout',
|
|
31
|
+
];
|
|
32
|
+
const NON_TERMINAL_STATUSES = ['submitted', 'running'];
|
|
33
|
+
const TERMINAL_FAILURE_STATUSES = ['failed', 'cancelled', 'timeout'];
|
|
34
|
+
// ─── SLURM state -> manifest status ───────────────────────────────────────────
|
|
35
|
+
//
|
|
36
|
+
// Source: SLURM job state codes (squeue/sacct State column). Producers for
|
|
37
|
+
// other backends map their own states onto the closed enum above; this table
|
|
38
|
+
// is SLURM-specific and lives behind the `backend: 'slurm'` field.
|
|
39
|
+
const SLURM_STATE_MAP = {
|
|
40
|
+
PENDING: 'submitted',
|
|
41
|
+
CONFIGURING: 'submitted',
|
|
42
|
+
RUNNING: 'running',
|
|
43
|
+
COMPLETING: 'running',
|
|
44
|
+
COMPLETED: 'completed-unverified',
|
|
45
|
+
FAILED: 'failed',
|
|
46
|
+
CANCELLED: 'cancelled',
|
|
47
|
+
TIMEOUT: 'timeout',
|
|
48
|
+
OUT_OF_MEMORY: 'failed',
|
|
49
|
+
BOOT_FAIL: 'failed',
|
|
50
|
+
NODE_FAIL: 'failed',
|
|
51
|
+
PREEMPTED: 'failed',
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Map a raw SLURM state string to the closed, scheduler-agnostic manifest
|
|
55
|
+
* status. Case-insensitive; trims whitespace; strips a trailing by-part
|
|
56
|
+
* ("CANCELLED by 1001" -> "CANCELLED"). Returns `null` for any unknown
|
|
57
|
+
* state so the caller can decide whether to surface or fail — never guesses
|
|
58
|
+
* (CLAUDE.md anti-guessing).
|
|
59
|
+
*/
|
|
60
|
+
function mapSlurmState(raw) {
|
|
61
|
+
if (typeof raw !== 'string')
|
|
62
|
+
return null;
|
|
63
|
+
const key = raw.trim().toUpperCase();
|
|
64
|
+
const head = key.split(/\s+/)[0];
|
|
65
|
+
if (head && Object.prototype.hasOwnProperty.call(SLURM_STATE_MAP, head)) {
|
|
66
|
+
return SLURM_STATE_MAP[head];
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const REQUIRED_FIELDS = [
|
|
71
|
+
'plan_id',
|
|
72
|
+
'phase',
|
|
73
|
+
'job_id',
|
|
74
|
+
'backend',
|
|
75
|
+
'submit_command',
|
|
76
|
+
'status',
|
|
77
|
+
'expected_artifacts',
|
|
78
|
+
'verification_command',
|
|
79
|
+
'resume_command',
|
|
80
|
+
];
|
|
81
|
+
function assertString(v, key) {
|
|
82
|
+
if (typeof v !== 'string' || v.length === 0) {
|
|
83
|
+
throw new Error(`buildManifest: field "${key}" must be a non-empty string`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const STATUS_LIST = MANIFEST_STATUS;
|
|
87
|
+
/**
|
|
88
|
+
* Build a versioned, frozen manifest. Stamps `version` and `submitted_at`
|
|
89
|
+
* (via the injected clock seam) and normalises `terminal_details`:
|
|
90
|
+
* `null` unless the status is a terminal failure AND details were supplied.
|
|
91
|
+
* Throws on missing required fields or an out-of-enum status.
|
|
92
|
+
*/
|
|
93
|
+
function buildManifest(input, opts = {}) {
|
|
94
|
+
const inputRecord = input;
|
|
95
|
+
for (const key of REQUIRED_FIELDS) {
|
|
96
|
+
const v = inputRecord[key];
|
|
97
|
+
if (key === 'expected_artifacts') {
|
|
98
|
+
if (!Array.isArray(v) || v.length === 0 || !v.every((x) => typeof x === 'string')) {
|
|
99
|
+
throw new Error('buildManifest: field "expected_artifacts" must be a non-empty string[]');
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
assertString(v, key);
|
|
104
|
+
}
|
|
105
|
+
if (!STATUS_LIST.includes(input.status)) {
|
|
106
|
+
throw new Error(`buildManifest: field "status" must be one of ${MANIFEST_STATUS.join(', ')}`);
|
|
107
|
+
}
|
|
108
|
+
const clock = opts.clock ?? { nowIso: () => new Date().toISOString() };
|
|
109
|
+
const isTerminalFailure = TERMINAL_FAILURE_STATUSES.includes(input.status);
|
|
110
|
+
const terminal_details = isTerminalFailure && input.terminal_details ? input.terminal_details : null;
|
|
111
|
+
return Object.freeze({
|
|
112
|
+
version: MANIFEST_VERSION,
|
|
113
|
+
job_id: input.job_id,
|
|
114
|
+
plan_id: input.plan_id,
|
|
115
|
+
phase: input.phase,
|
|
116
|
+
backend: input.backend,
|
|
117
|
+
submit_command: input.submit_command,
|
|
118
|
+
status: input.status,
|
|
119
|
+
expected_artifacts: [...input.expected_artifacts],
|
|
120
|
+
verification_command: input.verification_command,
|
|
121
|
+
resume_command: input.resume_command,
|
|
122
|
+
submitted_at: clock.nowIso(),
|
|
123
|
+
terminal_details,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Producer-side schema validator — the mirror of the consumer trust boundary
|
|
128
|
+
* (planning-artifacts.md). Producers MUST emit a manifest this accepts; the
|
|
129
|
+
* core loop re-validates defensively on read.
|
|
130
|
+
*/
|
|
131
|
+
function validateManifest(value) {
|
|
132
|
+
if (!value || typeof value !== 'object') {
|
|
133
|
+
return { ok: false, errors: ['manifest must be an object'] };
|
|
134
|
+
}
|
|
135
|
+
const m = value;
|
|
136
|
+
const errors = [];
|
|
137
|
+
if (m.version !== MANIFEST_VERSION)
|
|
138
|
+
errors.push(`version must be "${MANIFEST_VERSION}"`);
|
|
139
|
+
for (const f of ['job_id', 'plan_id', 'phase', 'backend', 'submit_command', 'verification_command', 'resume_command', 'submitted_at']) {
|
|
140
|
+
if (typeof m[f] !== 'string' || m[f].length === 0) {
|
|
141
|
+
errors.push(`field "${f}" must be a non-empty string`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (typeof m.status !== 'string' || !STATUS_LIST.includes(m.status)) {
|
|
145
|
+
errors.push(`status must be one of ${MANIFEST_STATUS.join(', ')}`);
|
|
146
|
+
}
|
|
147
|
+
if (!Array.isArray(m.expected_artifacts) || !m.expected_artifacts.every((x) => typeof x === 'string')) {
|
|
148
|
+
errors.push('expected_artifacts must be a string[]');
|
|
149
|
+
}
|
|
150
|
+
if (m.terminal_details !== null && typeof m.terminal_details !== 'object') {
|
|
151
|
+
errors.push('terminal_details must be null or an object');
|
|
152
|
+
}
|
|
153
|
+
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Parse `sbatch --parsable` output. Accepts either a bare job id
|
|
157
|
+
* (`"12345"`) or the `"12345;clustername"` form. Rejects prose like
|
|
158
|
+
* `"Submitted batch job 12345"` (that is the non-parsable default format).
|
|
159
|
+
*/
|
|
160
|
+
function parseSbatchParsable(stdout) {
|
|
161
|
+
if (typeof stdout !== 'string')
|
|
162
|
+
return { ok: false, kind: 'non_string', raw: String(stdout) };
|
|
163
|
+
const trimmed = stdout.trim();
|
|
164
|
+
if (!trimmed)
|
|
165
|
+
return { ok: false, kind: 'empty', raw: stdout };
|
|
166
|
+
const head = trimmed.split(';')[0].split(/\s+/)[0];
|
|
167
|
+
if (!/^\d+$/.test(head))
|
|
168
|
+
return { ok: false, kind: 'not_numeric', raw: trimmed };
|
|
169
|
+
return { ok: true, job_id: head };
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Parse a single `squeue` line of the form `"<jobid> <state>"`. Returns `null`
|
|
173
|
+
* for a header or any row that does not have at least two tokens.
|
|
174
|
+
*/
|
|
175
|
+
function parseSqueueLine(line) {
|
|
176
|
+
if (typeof line !== 'string')
|
|
177
|
+
return null;
|
|
178
|
+
const parts = line.trim().split(/\s+/);
|
|
179
|
+
if (parts.length < 2)
|
|
180
|
+
return null;
|
|
181
|
+
const [job_id, state] = parts;
|
|
182
|
+
if (!/^\d+$/.test(job_id))
|
|
183
|
+
return null;
|
|
184
|
+
return { job_id, state };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Parse a `sacct -P` row given as pre-split columns where index 0 is the job
|
|
188
|
+
* id and index 1 is the state. Returns `null` for malformed rows.
|
|
189
|
+
*/
|
|
190
|
+
function parseSacctRow(cols) {
|
|
191
|
+
if (cols.length < 2)
|
|
192
|
+
return null;
|
|
193
|
+
const job_id = cols[0];
|
|
194
|
+
const state = cols[1];
|
|
195
|
+
if (typeof job_id !== 'string' || typeof state !== 'string')
|
|
196
|
+
return null;
|
|
197
|
+
if (!/^\d+$/.test(job_id))
|
|
198
|
+
return null;
|
|
199
|
+
return { job_id, state };
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Pure path projection: `.planning/async-jobs/<job_id>.json`.
|
|
203
|
+
*/
|
|
204
|
+
function manifestPath(planningDir, jobId) {
|
|
205
|
+
return node_path_1.default.join(planningDir, 'async-jobs', `${jobId}.json`);
|
|
206
|
+
}
|
|
207
|
+
function _isNonTerminal(status) {
|
|
208
|
+
return typeof status === 'string' && NON_TERMINAL_STATUSES.includes(status);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Write a manifest to `.planning/async-jobs/<job_id>.json`.
|
|
212
|
+
*
|
|
213
|
+
* Fail-closed rules (mirror of the consumer contract, planning-artifacts.md):
|
|
214
|
+
* - If any existing manifest in the dir shares `plan_id` but has a different
|
|
215
|
+
* `job_id` AND is non-terminal -> refuse (`duplicate_plan_id`); dispatching
|
|
216
|
+
* again would duplicate the external job.
|
|
217
|
+
* - If the target file exists but is not valid JSON -> refuse
|
|
218
|
+
* (`malformed_existing`); never silently clobber.
|
|
219
|
+
* - Same `job_id` for the same `plan_id` -> allowed (status progression).
|
|
220
|
+
* - A prior job for the same `plan_id` that is already terminal -> allowed
|
|
221
|
+
* (the duplicate guard only protects against re-dispatching live work).
|
|
222
|
+
*/
|
|
223
|
+
function writeManifest(manifest, planningDir, opts = {}) {
|
|
224
|
+
const fs = opts.fs ?? node_fs_1.default;
|
|
225
|
+
const dir = node_path_1.default.join(planningDir, 'async-jobs');
|
|
226
|
+
const target = manifestPath(planningDir, manifest.job_id);
|
|
227
|
+
let names;
|
|
228
|
+
try {
|
|
229
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
230
|
+
names = fs.readdirSync(dir);
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
return { ok: false, kind: 'io_error', message: e.message };
|
|
234
|
+
}
|
|
235
|
+
for (const name of names) {
|
|
236
|
+
if (!name.endsWith('.json'))
|
|
237
|
+
continue;
|
|
238
|
+
const p = node_path_1.default.join(dir, name);
|
|
239
|
+
let raw;
|
|
240
|
+
try {
|
|
241
|
+
raw = String(fs.readFileSync(p));
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
let existing;
|
|
247
|
+
try {
|
|
248
|
+
existing = JSON.parse(raw);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
if (p === target) {
|
|
252
|
+
return { ok: false, kind: 'malformed_existing', message: `target manifest ${p} is not valid JSON` };
|
|
253
|
+
}
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const samePlan = existing.plan_id === manifest.plan_id;
|
|
257
|
+
const sameJob = existing.job_id === manifest.job_id;
|
|
258
|
+
if (samePlan && !sameJob && _isNonTerminal(existing.status)) {
|
|
259
|
+
return {
|
|
260
|
+
ok: false,
|
|
261
|
+
kind: 'duplicate_plan_id',
|
|
262
|
+
message: `plan_id "${manifest.plan_id}" already has non-terminal job "${String(existing.job_id)}" at ${p}; dispatching again would duplicate the external job`,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
fs.writeFileSync(target, JSON.stringify(manifest, null, 2) + '\n');
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
return { ok: false, kind: 'io_error', message: e.message };
|
|
271
|
+
}
|
|
272
|
+
return { ok: true, path: target };
|
|
273
|
+
}
|
|
274
|
+
module.exports = {
|
|
275
|
+
MANIFEST_VERSION,
|
|
276
|
+
MANIFEST_STATUS,
|
|
277
|
+
NON_TERMINAL_STATUSES,
|
|
278
|
+
TERMINAL_FAILURE_STATUSES,
|
|
279
|
+
mapSlurmState,
|
|
280
|
+
buildManifest,
|
|
281
|
+
validateManifest,
|
|
282
|
+
parseSbatchParsable,
|
|
283
|
+
parseSqueueLine,
|
|
284
|
+
parseSacctRow,
|
|
285
|
+
writeManifest,
|
|
286
|
+
manifestPath,
|
|
287
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Gate Predicate Evaluator — issue #2008 / ADR-2008
|
|
4
|
+
*
|
|
5
|
+
* Pure, deps-injected evaluator for capability gate `check.predicate` blocks.
|
|
6
|
+
*
|
|
7
|
+
* Background: the loop-resolver renders active gate hooks (carrying their
|
|
8
|
+
* `check.predicate` declarations) but, prior to #2008, nothing EVALUATED a
|
|
9
|
+
* declared predicate for non-built-in capabilities — `check.query` was the only
|
|
10
|
+
* enforced shape (dispatched via `gsd_run check <query>`), and `check.predicate`
|
|
11
|
+
* was declaration-only. This module is the generic evaluation path.
|
|
12
|
+
*
|
|
13
|
+
* The workflow gate-dispatch calls this evaluator (via the `gsd_run check predicate`
|
|
14
|
+
* subcommand in check-command-router) for any gate whose `check` carries a
|
|
15
|
+
* `predicate` instead of a `query`. The evaluator dispatches by `predicate.kind`
|
|
16
|
+
* and returns the existing `{ block, message }` gate contract. A THROWN error
|
|
17
|
+
* (malformed predicate / unknown kind) is mapped by the CLI wrapper to a
|
|
18
|
+
* non-zero check-command exit, which the workflow's two-step gate contract treats
|
|
19
|
+
* as a step-1 command failure (routed per the gate's `onError`).
|
|
20
|
+
*
|
|
21
|
+
* Built-in kind (v1): `command-exit-zero` — run a declared command in a bounded
|
|
22
|
+
* `sh -c` subprocess at the project root, inheriting the process env; exit 0 =>
|
|
23
|
+
* pass, non-zero => block, timeout => block. The production runBoundedShell
|
|
24
|
+
* binding is shell-command-projection.execTool (bounded spawnSync). See ADR-2008
|
|
25
|
+
* for the full sandbox contract.
|
|
26
|
+
*
|
|
27
|
+
* This is a leaf pure module: no fs, no child_process, no config — the subprocess
|
|
28
|
+
* seam is injected so the evaluator is trivially testable without spawning.
|
|
29
|
+
*/
|
|
30
|
+
// ─── Public constants ─────────────────────────────────────────────────────────
|
|
31
|
+
/** Default command timeout for `command-exit-zero` (30s). Matches execTool default. */
|
|
32
|
+
const COMMAND_EXIT_ZERO_DEFAULT_TIMEOUT_MS = 30_000;
|
|
33
|
+
/** Hard cap on the stderr/stdout tail embedded in the gate `message`. */
|
|
34
|
+
const COMMAND_MAX_OUTPUT_CHARS = 2000;
|
|
35
|
+
/** Hard cap on the declared command length (defense-in-depth against ARGV overflow / abuse). */
|
|
36
|
+
const COMMAND_MAX_LENGTH = 4096;
|
|
37
|
+
/** Predicate kinds this evaluator recognises (extensible — add to KIND_TABLE). */
|
|
38
|
+
const EVALUATOR_KINDS = Object.freeze(['command-exit-zero']);
|
|
39
|
+
/** Placeholders interpolated into a declared command, in addition to sh's own vars. */
|
|
40
|
+
const INTERPOLATION_VAR_NAMES = Object.freeze(['PHASE_NUMBER', 'PHASE_DIR', 'PHASE_REQ_IDS']);
|
|
41
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
42
|
+
const INTERPOLATION_RE = /\$\{(PHASE_NUMBER|PHASE_DIR|PHASE_REQ_IDS)\}/g;
|
|
43
|
+
/** Replace the three known ${PHASE_*} placeholders with context values (undefined => ''). */
|
|
44
|
+
function interpolate(command, ctx) {
|
|
45
|
+
return command.replace(INTERPOLATION_RE, (_whole, name) => {
|
|
46
|
+
if (name === 'PHASE_NUMBER')
|
|
47
|
+
return ctx.phaseNumber ?? '';
|
|
48
|
+
if (name === 'PHASE_DIR')
|
|
49
|
+
return ctx.phaseDir ?? '';
|
|
50
|
+
if (name === 'PHASE_REQ_IDS')
|
|
51
|
+
return ctx.phaseReqIds ?? '';
|
|
52
|
+
return '';
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/** Cap a string at COMMAND_MAX_OUTPUT_CHARS so gate messages stay context-bounded. */
|
|
56
|
+
function trimToMax(s) {
|
|
57
|
+
return s.length > COMMAND_MAX_OUTPUT_CHARS ? s.slice(0, COMMAND_MAX_OUTPUT_CHARS) : s;
|
|
58
|
+
}
|
|
59
|
+
function isNonEmptyString(v) {
|
|
60
|
+
return typeof v === 'string' && v.trim().length > 0;
|
|
61
|
+
}
|
|
62
|
+
// ─── Kind: command-exit-zero ──────────────────────────────────────────────────
|
|
63
|
+
function evaluateCommandExitZero(predicate, ctx, deps) {
|
|
64
|
+
const command = predicate['command'];
|
|
65
|
+
if (!isNonEmptyString(command)) {
|
|
66
|
+
throw new Error('command-exit-zero predicate requires a non-empty string "command"');
|
|
67
|
+
}
|
|
68
|
+
if (command.length > COMMAND_MAX_LENGTH) {
|
|
69
|
+
throw new Error(`command-exit-zero predicate "command" exceeds max length ${COMMAND_MAX_LENGTH}`);
|
|
70
|
+
}
|
|
71
|
+
let timeoutMs = COMMAND_EXIT_ZERO_DEFAULT_TIMEOUT_MS;
|
|
72
|
+
const rawTimeout = predicate['timeout'];
|
|
73
|
+
if (rawTimeout !== undefined) {
|
|
74
|
+
if (typeof rawTimeout !== 'number' || !Number.isFinite(rawTimeout) || rawTimeout <= 0) {
|
|
75
|
+
throw new Error('command-exit-zero predicate "timeout" must be a positive finite number (seconds)');
|
|
76
|
+
}
|
|
77
|
+
timeoutMs = Math.floor(rawTimeout * 1000);
|
|
78
|
+
}
|
|
79
|
+
const interpolated = interpolate(command, ctx);
|
|
80
|
+
const res = deps.runBoundedShell({ command: interpolated, cwd: ctx.cwd, timeoutMs });
|
|
81
|
+
if (res.timedOut) {
|
|
82
|
+
return {
|
|
83
|
+
block: true,
|
|
84
|
+
message: trimToMax(`command timed out after ${Math.round(timeoutMs / 1000)}s: ${res.stderr || interpolated}`),
|
|
85
|
+
details: { kind: 'command-exit-zero', timedOut: true, signal: res.signal },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (res.exitCode === 0) {
|
|
89
|
+
return {
|
|
90
|
+
block: false,
|
|
91
|
+
message: 'command exited 0',
|
|
92
|
+
details: { kind: 'command-exit-zero', exitCode: 0 },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// Non-zero (incl. null exit from a signal kill) => block. Surface code + stderr/stdout tail.
|
|
96
|
+
const code = res.exitCode === null ? '<none>' : String(res.exitCode);
|
|
97
|
+
const tail = trimToMax(res.stderr || res.stdout || '');
|
|
98
|
+
const message = tail ? `command exited ${code}: ${tail}` : `command exited ${code}`;
|
|
99
|
+
return {
|
|
100
|
+
block: true,
|
|
101
|
+
message: trimToMax(message),
|
|
102
|
+
details: { kind: 'command-exit-zero', exitCode: res.exitCode, signal: res.signal },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// ─── Kind dispatch table ──────────────────────────────────────────────────────
|
|
106
|
+
const KIND_TABLE = {
|
|
107
|
+
'command-exit-zero': evaluateCommandExitZero,
|
|
108
|
+
};
|
|
109
|
+
// ─── Public entry point ───────────────────────────────────────────────────────
|
|
110
|
+
/**
|
|
111
|
+
* Evaluate a capability gate `check.predicate`.
|
|
112
|
+
*
|
|
113
|
+
* Returns `{ block, message }` for any successfully-recognised predicate.
|
|
114
|
+
* THROWS for a malformed predicate, missing context/deps, or unknown `kind` —
|
|
115
|
+
* the CLI wrapper converts a throw into a non-zero check-command exit so the
|
|
116
|
+
* workflow's `onError` (step-1) contract applies. This keeps the gate fail-closed
|
|
117
|
+
* without conflating an evaluator bug with a legitimate gate-block decision.
|
|
118
|
+
*/
|
|
119
|
+
function evaluatePredicate(predicate, context, deps) {
|
|
120
|
+
if (!predicate || typeof predicate !== 'object' || Array.isArray(predicate)) {
|
|
121
|
+
throw new Error('predicate must be an object');
|
|
122
|
+
}
|
|
123
|
+
const ctx = context;
|
|
124
|
+
if (!ctx || typeof ctx.cwd !== 'string' || ctx.cwd.length === 0) {
|
|
125
|
+
throw new Error('predicate context requires a non-empty "cwd"');
|
|
126
|
+
}
|
|
127
|
+
const d = deps;
|
|
128
|
+
if (!d || typeof d.runBoundedShell !== 'function') {
|
|
129
|
+
throw new Error('predicate deps require a "runBoundedShell" function');
|
|
130
|
+
}
|
|
131
|
+
const pred = predicate;
|
|
132
|
+
const kind = pred['kind'];
|
|
133
|
+
if (typeof kind !== 'string' || kind.length === 0) {
|
|
134
|
+
throw new Error('predicate.kind must be a non-empty string');
|
|
135
|
+
}
|
|
136
|
+
const handler = KIND_TABLE[kind];
|
|
137
|
+
if (!handler) {
|
|
138
|
+
throw new Error(`Unknown predicate kind: "${kind}". Known kinds: ${EVALUATOR_KINDS.join(', ')}`);
|
|
139
|
+
}
|
|
140
|
+
return handler(pred, ctx, d);
|
|
141
|
+
}
|
|
142
|
+
module.exports = {
|
|
143
|
+
evaluatePredicate,
|
|
144
|
+
evaluateCommandExitZero,
|
|
145
|
+
interpolate,
|
|
146
|
+
COMMAND_EXIT_ZERO_DEFAULT_TIMEOUT_MS,
|
|
147
|
+
COMMAND_MAX_OUTPUT_CHARS,
|
|
148
|
+
COMMAND_MAX_LENGTH,
|
|
149
|
+
EVALUATOR_KINDS,
|
|
150
|
+
INTERPOLATION_VAR_NAMES,
|
|
151
|
+
};
|
|
@@ -261,6 +261,29 @@ function countCommitsBetween(cwd, from, to) {
|
|
|
261
261
|
const n = parseInt(r.stdout.trim(), 10);
|
|
262
262
|
return Number.isFinite(n) ? n : null;
|
|
263
263
|
}
|
|
264
|
+
// ─── Graph location resolution (#1825) ───────────────────────────────────────
|
|
265
|
+
//
|
|
266
|
+
// `graphify.graph_path` (in .planning/config.json) lets one umbrella-level graph
|
|
267
|
+
// serve multiple sibling projects without a per-project mirror. When set, query /
|
|
268
|
+
// status / diff read the configured graph, and the diff snapshot travels with it
|
|
269
|
+
// (alongside graph.json). When unset/blank/non-string, behaviour is byte-identical
|
|
270
|
+
// to the historical `<planningDir>/graphs/graph.json`.
|
|
271
|
+
const GRAPH_FILENAME = 'graph.json';
|
|
272
|
+
const SNAPSHOT_FILENAME = '.last-build-snapshot.json';
|
|
273
|
+
/**
|
|
274
|
+
* Resolve the absolute graph.json location. Honors `graphify.graph_path` in
|
|
275
|
+
* config.json (resolved relative to the project root, `cwd`); falls back to the
|
|
276
|
+
* default `<planningDir>/graphs/graph.json` when unset/blank/non-string.
|
|
277
|
+
*/
|
|
278
|
+
function resolveGraphLocation(cwd, planningDir) {
|
|
279
|
+
const config = safeReadJson(node_path_1.default.join(planningDir, 'config.json'));
|
|
280
|
+
const graphify = (config && config['graphify']);
|
|
281
|
+
const configuredValue = graphify && graphify['graph_path'];
|
|
282
|
+
if (typeof configuredValue === 'string' && configuredValue.trim().length > 0) {
|
|
283
|
+
return { graphPath: node_path_1.default.resolve(cwd, configuredValue), configured: true };
|
|
284
|
+
}
|
|
285
|
+
return { graphPath: node_path_1.default.join(planningDir, 'graphs', GRAPH_FILENAME), configured: false };
|
|
286
|
+
}
|
|
264
287
|
/**
|
|
265
288
|
* Query the knowledge graph for nodes matching a term, with optional budget cap.
|
|
266
289
|
* Uses seed-then-expand BFS traversal (D-01).
|
|
@@ -269,9 +292,11 @@ function graphifyQuery(cwd, term, options = {}) {
|
|
|
269
292
|
const planningDir = node_path_1.default.join(cwd, '.planning');
|
|
270
293
|
if (!isCapabilityActive('graphify', cwd))
|
|
271
294
|
return disabledResponse();
|
|
272
|
-
const graphPath =
|
|
295
|
+
const { graphPath, configured } = resolveGraphLocation(cwd, planningDir);
|
|
273
296
|
if (!node_fs_1.default.existsSync(graphPath)) {
|
|
274
|
-
return { error:
|
|
297
|
+
return { error: configured
|
|
298
|
+
? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run /gsd:graphify build.`
|
|
299
|
+
: 'No graph built yet. Run graphify build first.' };
|
|
275
300
|
}
|
|
276
301
|
const graph = safeReadJson(graphPath);
|
|
277
302
|
if (!graph) {
|
|
@@ -303,9 +328,11 @@ function graphifyStatus(cwd) {
|
|
|
303
328
|
const planningDir = node_path_1.default.join(cwd, '.planning');
|
|
304
329
|
if (!isCapabilityActive('graphify', cwd))
|
|
305
330
|
return disabledResponse();
|
|
306
|
-
const graphPath =
|
|
331
|
+
const { graphPath, configured } = resolveGraphLocation(cwd, planningDir);
|
|
307
332
|
if (!node_fs_1.default.existsSync(graphPath)) {
|
|
308
|
-
return { exists: false, message:
|
|
333
|
+
return { exists: false, message: configured
|
|
334
|
+
? `Configured graph not found at ${graphPath}. Set graphify.graph_path or run /gsd:graphify build.`
|
|
335
|
+
: 'No graph built yet. Run graphify build to create one.' };
|
|
309
336
|
}
|
|
310
337
|
const stat = node_fs_1.default.statSync(graphPath);
|
|
311
338
|
const graph = safeReadJson(graphPath);
|
|
@@ -360,8 +387,8 @@ function graphifyDiff(cwd) {
|
|
|
360
387
|
const planningDir = node_path_1.default.join(cwd, '.planning');
|
|
361
388
|
if (!isCapabilityActive('graphify', cwd))
|
|
362
389
|
return disabledResponse();
|
|
363
|
-
const
|
|
364
|
-
const
|
|
390
|
+
const { graphPath } = resolveGraphLocation(cwd, planningDir);
|
|
391
|
+
const snapshotPath = node_path_1.default.join(node_path_1.default.dirname(graphPath), SNAPSHOT_FILENAME);
|
|
365
392
|
if (!node_fs_1.default.existsSync(snapshotPath)) {
|
|
366
393
|
return { no_baseline: true, message: 'No previous snapshot. Run graphify build first, then build again to generate a diff baseline.' };
|
|
367
394
|
}
|
|
@@ -405,7 +432,11 @@ function graphifyBuild(cwd) {
|
|
|
405
432
|
if (!installed.installed)
|
|
406
433
|
return { error: installed.message };
|
|
407
434
|
const version = checkGraphifyVersion();
|
|
408
|
-
// Ensure output directory exists (D-05)
|
|
435
|
+
// Ensure output directory exists (D-05). Build stays project-scoped: the build
|
|
436
|
+
// skill cp's artifacts into `<planningDir>/graphs/` regardless of graph_path, so
|
|
437
|
+
// graphs_dir reflects that real destination (not the configured read location).
|
|
438
|
+
// A shared umbrella graph is built in the umbrella project; sub-projects only
|
|
439
|
+
// READ it via graphify.graph_path (#1825).
|
|
409
440
|
const graphsDir = node_path_1.default.join(planningDir, 'graphs');
|
|
410
441
|
node_fs_1.default.mkdirSync(graphsDir, { recursive: true });
|
|
411
442
|
// Read build timeout from config -- default 300s per D-02
|
|
@@ -428,7 +459,8 @@ function graphifyBuild(cwd) {
|
|
|
428
459
|
* using platformWriteSync for crash safety.
|
|
429
460
|
*/
|
|
430
461
|
function writeSnapshot(cwd) {
|
|
431
|
-
const
|
|
462
|
+
const planningDir = node_path_1.default.join(cwd, '.planning');
|
|
463
|
+
const { graphPath } = resolveGraphLocation(cwd, planningDir);
|
|
432
464
|
const graph = safeReadJson(graphPath);
|
|
433
465
|
if (!graph)
|
|
434
466
|
return { error: 'Cannot write snapshot: graph.json not parseable' };
|
|
@@ -438,7 +470,7 @@ function writeSnapshot(cwd) {
|
|
|
438
470
|
nodes: graph.nodes || [],
|
|
439
471
|
edges: graph.edges || graph.links || [],
|
|
440
472
|
};
|
|
441
|
-
const snapshotPath = node_path_1.default.join(
|
|
473
|
+
const snapshotPath = node_path_1.default.join(node_path_1.default.dirname(graphPath), SNAPSHOT_FILENAME);
|
|
442
474
|
(0, shell_command_projection_cjs_1.platformWriteSync)(snapshotPath, JSON.stringify(snapshot, null, 2));
|
|
443
475
|
return {
|
|
444
476
|
saved: true,
|
|
@@ -39,6 +39,10 @@ function routeInitCommand({ init, args, cwd, raw, error }) {
|
|
|
39
39
|
},
|
|
40
40
|
'new-project': () => init.cmdInitNewProject(cwd, raw),
|
|
41
41
|
'new-milestone': () => init.cmdInitNewMilestone(cwd, raw),
|
|
42
|
+
onboard: () => {
|
|
43
|
+
const namedArgs = (0, command_arg_projection_cjs_1.parseNamedArgs)(args, [], ['fast', 'text']);
|
|
44
|
+
init.cmdInitOnboard(cwd, raw, { fast: namedArgs['fast'], text: namedArgs['text'] });
|
|
45
|
+
},
|
|
42
46
|
quick: () => init.cmdInitQuick(cwd, args.slice(2).join(' '), raw),
|
|
43
47
|
'ingest-docs': () => init.cmdInitIngestDocs(cwd, raw),
|
|
44
48
|
resume: () => init.cmdInitResume(cwd, raw),
|