@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
|
@@ -126,6 +126,49 @@ function extractFrontmatter(content) {
|
|
|
126
126
|
}
|
|
127
127
|
return frontmatter;
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Escape a string for emission inside a YAML double-quoted scalar (#1779).
|
|
131
|
+
* Backslash must be escaped first so the backslashes added for embedded quotes
|
|
132
|
+
* (and control chars) are not themselves doubled. Without this, a value
|
|
133
|
+
* carrying an indicator (`:`/`#`) that also contains a literal `"` serializes
|
|
134
|
+
* to invalid YAML, e.g. `upstream: "https://x (Tom; "Git. Ship. Done")"`. A
|
|
135
|
+
* literal newline/tab/control char inside the quotes likewise breaks (or
|
|
136
|
+
* silently alters) the scalar, so those are escaped to their YAML forms too.
|
|
137
|
+
*/
|
|
138
|
+
function escapeDoubleQuoted(s) {
|
|
139
|
+
return s
|
|
140
|
+
.replace(/\\/g, '\\\\')
|
|
141
|
+
.replace(/"/g, '\\"')
|
|
142
|
+
.replace(/\n/g, '\\n')
|
|
143
|
+
.replace(/\t/g, '\\t')
|
|
144
|
+
.replace(/\r/g, '\\r')
|
|
145
|
+
// Remaining C0 controls + DEL → \xHH (a valid YAML double-quoted escape).
|
|
146
|
+
.replace(/[\u0000-\u001f\u007f]/g, (c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* A plain (unquoted) scalar that would mis-parse or round-trip lossily when
|
|
150
|
+
* emitted bare must instead go through the double-quoted + escaped form
|
|
151
|
+
* (#1779): the empty string (bare `k:` reloads as null), an embedded `"`/`\`
|
|
152
|
+
* or control char, a leading YAML indicator (quote, `&`/`*`/`!` anchor/alias/
|
|
153
|
+
* tag, `|`/`>` block scalar, flow `[]{},`, `#`, reserved `%`/`@`/backtick, or
|
|
154
|
+
* `-`/`?`/`:` before a space), or leading/trailing whitespace. This helper is
|
|
155
|
+
* the correctness complement of `escapeDoubleQuoted`: it broadens the *trigger*
|
|
156
|
+
* for quoting without broadening the lossy object-list handling deferred to
|
|
157
|
+
* #1572/#1660.
|
|
158
|
+
*/
|
|
159
|
+
function scalarNeedsDoubleQuoting(s) {
|
|
160
|
+
if (s === '')
|
|
161
|
+
return true;
|
|
162
|
+
if (/["\\\u0000-\u001f\u007f]/.test(s))
|
|
163
|
+
return true;
|
|
164
|
+
// Always-unsafe leading indicators, or leading/trailing whitespace.
|
|
165
|
+
if (/^[,[\]{}#&*!|>'"%@`]/.test(s) || /^\s|\s$/.test(s))
|
|
166
|
+
return true;
|
|
167
|
+
// `-` `?` `:` only start a plain scalar safely when NOT followed by a space.
|
|
168
|
+
if (/^[-?:](\s|$)/.test(s))
|
|
169
|
+
return true;
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
129
172
|
function reconstructFrontmatter(obj) {
|
|
130
173
|
const lines = [];
|
|
131
174
|
for (const [key, value] of Object.entries(obj)) {
|
|
@@ -141,7 +184,7 @@ function reconstructFrontmatter(obj) {
|
|
|
141
184
|
else {
|
|
142
185
|
lines.push(`${key}:`);
|
|
143
186
|
for (const item of value) {
|
|
144
|
-
lines.push(` - ${typeof item === 'string' && (item.includes(':') || item.includes('#')) ? `"${item}"` : item}`);
|
|
187
|
+
lines.push(` - ${typeof item === 'string' && (item.includes(':') || item.includes('#') || scalarNeedsDoubleQuoting(item)) ? `"${escapeDoubleQuoted(item)}"` : item}`);
|
|
145
188
|
}
|
|
146
189
|
}
|
|
147
190
|
}
|
|
@@ -160,7 +203,7 @@ function reconstructFrontmatter(obj) {
|
|
|
160
203
|
else {
|
|
161
204
|
lines.push(` ${subkey}:`);
|
|
162
205
|
for (const item of subval) {
|
|
163
|
-
lines.push(` - ${typeof item === 'string' && (item.includes(':') || item.includes('#')) ? `"${item}"` : item}`);
|
|
206
|
+
lines.push(` - ${typeof item === 'string' && (item.includes(':') || item.includes('#') || scalarNeedsDoubleQuoting(item)) ? `"${escapeDoubleQuoted(item)}"` : item}`);
|
|
164
207
|
}
|
|
165
208
|
}
|
|
166
209
|
}
|
|
@@ -189,14 +232,14 @@ function reconstructFrontmatter(obj) {
|
|
|
189
232
|
else {
|
|
190
233
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
191
234
|
const sv = String(subval);
|
|
192
|
-
lines.push(` ${subkey}: ${sv.includes(':') || sv.includes('#') ? `"${sv}"` : sv}`);
|
|
235
|
+
lines.push(` ${subkey}: ${sv.includes(':') || sv.includes('#') || scalarNeedsDoubleQuoting(sv) ? `"${escapeDoubleQuoted(sv)}"` : sv}`);
|
|
193
236
|
}
|
|
194
237
|
}
|
|
195
238
|
}
|
|
196
239
|
else {
|
|
197
240
|
const sv = String(value);
|
|
198
|
-
if (sv.includes(':') || sv.includes('#') || sv.startsWith('[') || sv.startsWith('{')) {
|
|
199
|
-
lines.push(`${key}: "${sv}"`);
|
|
241
|
+
if (sv.includes(':') || sv.includes('#') || sv.startsWith('[') || sv.startsWith('{') || scalarNeedsDoubleQuoting(sv)) {
|
|
242
|
+
lines.push(`${key}: "${escapeDoubleQuoted(sv)}"`);
|
|
200
243
|
}
|
|
201
244
|
else {
|
|
202
245
|
lines.push(`${key}: ${sv}`);
|
|
@@ -448,7 +491,11 @@ function parseMustHavesBlock(content, blockName) {
|
|
|
448
491
|
else {
|
|
449
492
|
const kvMatch = trimmed.match(/^(\w+):\s*"?([^"]*)"?\s*$/);
|
|
450
493
|
if (kvMatch) {
|
|
451
|
-
|
|
494
|
+
// Trim: a quoted value like `"backstop "` captures the inner trailing space in group 2.
|
|
495
|
+
// Left untrimmed, a hand-authored `must_haves` marker degrades (a `backstop` truth silently
|
|
496
|
+
// grades green instead of abstaining — #1905, the #1154 false-pass; also the sibling
|
|
497
|
+
// check_target/violationFixture path). Whitespace is never semantic in a scalar KV value.
|
|
498
|
+
const val = kvMatch[2].trim();
|
|
452
499
|
// Try to parse as number
|
|
453
500
|
(current)[kvMatch[1]] = /^\d+$/.test(val) ? parseInt(val, 10) : val;
|
|
454
501
|
}
|
|
@@ -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,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialized (out-of-process) capability-exchange handshake (ADR-1239 Phase E / #1683).
|
|
3
|
+
*
|
|
4
|
+
* Phase 1's `negotiateHostCapabilities` is IN-PROCESS (a host descriptor merged
|
|
5
|
+
* directly into the engine). Out-of-process SDK hosts (pi, VS Code) cannot share
|
|
6
|
+
* object references with the engine — they exchange a SERIALIZED capability set
|
|
7
|
+
* over a wire boundary (an MCP-style `initialize`). This module is the wire form
|
|
8
|
+
* of that handshake, kept CONSISTENT with the in-process negotiation: a request
|
|
9
|
+
* built + serialized here MUST yield the same NegotiationResult the in-process
|
|
10
|
+
* call produces for the same axes (asserted in tests/handshake-serialized.test.cjs).
|
|
11
|
+
*
|
|
12
|
+
* Wire shape (JSON — no object refs, safe across a process/IPC boundary):
|
|
13
|
+
*
|
|
14
|
+
* request = { protocolVersion: number, axes: Partial<HostIntegrationAxes> }
|
|
15
|
+
* response = NegotiationResult = { protocolVersion, effective, points, warnings }
|
|
16
|
+
*
|
|
17
|
+
* The engine side delegates to negotiateHostCapabilities; the host side builds
|
|
18
|
+
* the request from a descriptor. Both round-trip through JSON so the exchange is
|
|
19
|
+
* strictly serializable (a non-JSON-safe value would break the wire contract).
|
|
20
|
+
*
|
|
21
|
+
* Pure + additive: no I/O, no global state. The companion MCP server (Phase 4)
|
|
22
|
+
* or an SDK host binds this to a real transport.
|
|
23
|
+
*/
|
|
24
|
+
'use strict';
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
26
|
+
const hostIntegration = require("./host-integration.cjs");
|
|
27
|
+
/** The wire method name for the initialize-style exchange. */
|
|
28
|
+
const HANDSHAKE_METHOD = 'gsd/host-initialize';
|
|
29
|
+
/**
|
|
30
|
+
* Host side: build a JSON-serializable handshake request from a host descriptor.
|
|
31
|
+
* Accepts either `{ protocolVersion?, axes }` or a bare axes object.
|
|
32
|
+
* Defaults protocolVersion to the engine's current PROTOCOL_VERSION.
|
|
33
|
+
*/
|
|
34
|
+
function buildHandshakeRequest(hostDescriptor) {
|
|
35
|
+
if (!hostDescriptor || typeof hostDescriptor !== 'object') {
|
|
36
|
+
throw new TypeError('buildHandshakeRequest: host descriptor (object) is required');
|
|
37
|
+
}
|
|
38
|
+
const desc = hostDescriptor;
|
|
39
|
+
const hasAxes = Object.prototype.hasOwnProperty.call(desc, 'axes');
|
|
40
|
+
const axes = (hasAxes ? desc.axes : desc);
|
|
41
|
+
if (!axes || typeof axes !== 'object') {
|
|
42
|
+
throw new TypeError('buildHandshakeRequest: descriptor.axes (object) is required');
|
|
43
|
+
}
|
|
44
|
+
const protocolVersion = typeof desc.protocolVersion === 'number' && Number.isFinite(desc.protocolVersion)
|
|
45
|
+
? desc.protocolVersion
|
|
46
|
+
: hostIntegration.PROTOCOL_VERSION;
|
|
47
|
+
// Force a wire round-trip so a non-JSON-safe descriptor fails HERE, not later.
|
|
48
|
+
return JSON.parse(JSON.stringify({ protocolVersion, axes }));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Engine side: handle a serialized handshake request → a JSON-serializable
|
|
52
|
+
* NegotiationResult. Delegates to negotiateHostCapabilities, so the result is
|
|
53
|
+
* identical to the in-process negotiation for the same axes.
|
|
54
|
+
*/
|
|
55
|
+
function handleHandshakeRequest(request, engine = hostIntegration.DEFAULT_ENGINE) {
|
|
56
|
+
if (!request || typeof request !== 'object') {
|
|
57
|
+
throw new TypeError('handleHandshakeRequest: request (object) is required');
|
|
58
|
+
}
|
|
59
|
+
const req = request;
|
|
60
|
+
const axes = (req.axes && typeof req.axes === 'object' ? req.axes : {});
|
|
61
|
+
const protocolVersion = req.protocolVersion;
|
|
62
|
+
const result = hostIntegration.negotiateHostCapabilities({ ...axes, ...(typeof protocolVersion === 'number' && Number.isFinite(protocolVersion) ? { protocolVersion } : {}) }, engine);
|
|
63
|
+
// Force a wire round-trip: the response must be strictly JSON-serializable.
|
|
64
|
+
return JSON.parse(JSON.stringify(result));
|
|
65
|
+
}
|
|
66
|
+
module.exports = {
|
|
67
|
+
HANDSHAKE_METHOD,
|
|
68
|
+
buildHandshakeRequest,
|
|
69
|
+
handleHandshakeRequest,
|
|
70
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-Integration SDK — the published public surface (ADR-1239 Phase E / #1683).
|
|
3
|
+
*
|
|
4
|
+
* External host-plugin authors import ONLY from this entry. It IS the contract:
|
|
5
|
+
* everything it re-exports is public + versioned (PROTOCOL_VERSION governs the
|
|
6
|
+
* set); everything else in gsd-core is internal. An SDK smoke test
|
|
7
|
+
* (tests/sdk-smoke.test.cjs) builds a new host-plugin against this surface only
|
|
8
|
+
* — proving an external author can wire a host without reading gsd-core internals.
|
|
9
|
+
*
|
|
10
|
+
* Surface: the negotiated schema + classification, the five adapters
|
|
11
|
+
* (declarative/imperative/model/hook/state), and the serialized handshake.
|
|
12
|
+
* Frozen so the public shape cannot be mutated by consumers.
|
|
13
|
+
*/
|
|
14
|
+
'use strict';
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
16
|
+
const hostIntegration = require("./host-integration.cjs");
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
18
|
+
const adapterDeclarative = require("./adapter-declarative.cjs");
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
20
|
+
const adapterImperative = require("./adapter-imperative.cjs");
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
22
|
+
const modelAdapter = require("./model-adapter.cjs");
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
24
|
+
const hookBus = require("./hook-bus.cjs");
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
26
|
+
const stateIo = require("./state-io.cjs");
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
|
+
const handshake = require("./handshake-serialized.cjs");
|
|
29
|
+
const SDK = Object.freeze({
|
|
30
|
+
// ── Schema + protocol version ────────────────────────────────────────────
|
|
31
|
+
PROTOCOL_VERSION: hostIntegration.PROTOCOL_VERSION,
|
|
32
|
+
HOST_INTEGRATION_AXES: hostIntegration.HOST_INTEGRATION_AXES,
|
|
33
|
+
INTERFACE_POINTS: hostIntegration.INTERFACE_POINTS,
|
|
34
|
+
PROFILE_BASELINES: hostIntegration.PROFILE_BASELINES,
|
|
35
|
+
// ── Negotiation + classification ─────────────────────────────────────────
|
|
36
|
+
negotiateHostCapabilities: hostIntegration.negotiateHostCapabilities,
|
|
37
|
+
profileOf: hostIntegration.profileOf,
|
|
38
|
+
degradationFor: hostIntegration.degradationFor,
|
|
39
|
+
hookEventSurfaceFor: hostIntegration.hookEventSurfaceFor,
|
|
40
|
+
extensionEventSurfaceFor: hostIntegration.extensionEventSurfaceFor,
|
|
41
|
+
shouldFlattenDispatch: hostIntegration.shouldFlattenDispatch,
|
|
42
|
+
// ── Embedding + engine adapters ──────────────────────────────────────────
|
|
43
|
+
createDeclarativeAdapter: adapterDeclarative.createDeclarativeAdapter,
|
|
44
|
+
createImperativeAdapter: adapterImperative.createImperativeAdapter,
|
|
45
|
+
createModelAdapter: modelAdapter.createModelAdapter,
|
|
46
|
+
createHookBus: hookBus.createHookBus,
|
|
47
|
+
createStateIO: stateIo.createStateIO,
|
|
48
|
+
// ── Serialized handshake (out-of-process SDK hosts: pi / VS Code) ─────────
|
|
49
|
+
HANDSHAKE_METHOD: handshake.HANDSHAKE_METHOD,
|
|
50
|
+
buildHandshakeRequest: handshake.buildHandshakeRequest,
|
|
51
|
+
handleHandshakeRequest: handshake.handleHandshakeRequest,
|
|
52
|
+
});
|
|
53
|
+
module.exports = SDK;
|
|
@@ -394,6 +394,63 @@ function shouldFlattenDispatch(dispatch) {
|
|
|
394
394
|
const canBackground = dispatch.background === true && dispatch.backgroundDispatch === true;
|
|
395
395
|
return !canBackground;
|
|
396
396
|
}
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
// Managed-hook event surface per hookEvents dialect (ADR-1239 / ADR-1016)
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
// Host-fireable MANAGED-hook events per `hookEvents` dialect. `hookEvents` is the
|
|
401
|
+
// managed-hook dialect — the event names GSD writes into a DECLARATIVE host's
|
|
402
|
+
// settings.json (claude = SessionStart/PreToolUse/…; gemini = BeforeTool/AfterTool).
|
|
403
|
+
// This is DISTINCT from the extension-system event surface (below): a host's
|
|
404
|
+
// plugin/extension API fires a different, plugin-owned event set. The two must
|
|
405
|
+
// not be conflated (ADR-1239 amendment / #1943 — the former 'opencode-subset'
|
|
406
|
+
// `hookEvents` value was this conflation; it is now `extensionEvents: opencode`).
|
|
407
|
+
const HOOK_EVENT_SURFACES = Object.freeze({
|
|
408
|
+
claude: Object.freeze(['SessionStart', 'PreToolUse', 'PostToolUse', 'Stop', 'SessionEnd', 'PreCompact']),
|
|
409
|
+
gemini: Object.freeze(['SessionStart', 'BeforeTool', 'AfterTool', 'SessionEnd']),
|
|
410
|
+
});
|
|
411
|
+
/**
|
|
412
|
+
* Resolve the managed-hook event surface for a `hookEvents` dialect.
|
|
413
|
+
* Returns null for unknown/missing dialects (fail-closed). Pure, never throws.
|
|
414
|
+
*/
|
|
415
|
+
function hookEventSurfaceFor(hookEvents) {
|
|
416
|
+
if (typeof hookEvents !== 'string')
|
|
417
|
+
return null;
|
|
418
|
+
return HOOK_EVENT_SURFACES[hookEvents] || null;
|
|
419
|
+
}
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
// Extension-system event surface (ADR-1239 amendment / #1943)
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
// The events a host's PLUGIN/EXTENSION API exposes — for imperative-embedding
|
|
424
|
+
// hosts that load GSD as a plugin. This is a SEPARATE vocabulary + descriptor
|
|
425
|
+
// field (`extensionEvents`) from `hookEvents`: hookEvents = the managed-hook
|
|
426
|
+
// dialect (declarative hosts' settings.json); extensionEvents = the plugin-owned
|
|
427
|
+
// event subset (imperative hosts' extension API). They are not the same thing.
|
|
428
|
+
//
|
|
429
|
+
// Values are documentation-sourced (ADR-1239 §research): OpenCode ~25 plugin
|
|
430
|
+
// events (session/tool/file/permission); pi ~30 fine-grained extension events;
|
|
431
|
+
// 'none' = the host exposes no extension surface and the engine owns the bus
|
|
432
|
+
// (VS Code). Declarative hosts (no plugin API) do not set `extensionEvents`.
|
|
433
|
+
const EXTENSION_EVENT_SURFACES = Object.freeze({
|
|
434
|
+
opencode: Object.freeze([
|
|
435
|
+
'session.created', 'session.idle', 'experimental.session.compacting',
|
|
436
|
+
'tool.execute.before', 'tool.execute.after', 'file.edited',
|
|
437
|
+
]),
|
|
438
|
+
pi: Object.freeze(['tool_call']),
|
|
439
|
+
none: Object.freeze([]),
|
|
440
|
+
});
|
|
441
|
+
/**
|
|
442
|
+
* Resolve the extension-system event surface for an `extensionEvents` dialect.
|
|
443
|
+
* Returns null for unknown/missing dialects (fail-closed). Pure, never throws.
|
|
444
|
+
*
|
|
445
|
+
* A non-null result is what makes an `extensionEvents` value a CONSUMED value
|
|
446
|
+
* rather than reserved vocab. For 'opencode' it carries NO workflow-phase events
|
|
447
|
+
* — the engine owns phase sequencing internally on such hosts (ADR-1239 §OpenCode).
|
|
448
|
+
*/
|
|
449
|
+
function extensionEventSurfaceFor(extensionEvents) {
|
|
450
|
+
if (typeof extensionEvents !== 'string')
|
|
451
|
+
return null;
|
|
452
|
+
return EXTENSION_EVENT_SURFACES[extensionEvents] || null;
|
|
453
|
+
}
|
|
397
454
|
module.exports = {
|
|
398
455
|
PROTOCOL_VERSION,
|
|
399
456
|
UNDOCUMENTED,
|
|
@@ -401,8 +458,12 @@ module.exports = {
|
|
|
401
458
|
INTERFACE_POINTS,
|
|
402
459
|
PROFILE_BASELINES,
|
|
403
460
|
DEFAULT_ENGINE,
|
|
461
|
+
HOOK_EVENT_SURFACES,
|
|
462
|
+
EXTENSION_EVENT_SURFACES,
|
|
404
463
|
degradationFor,
|
|
405
464
|
profileOf,
|
|
406
465
|
negotiateHostCapabilities,
|
|
407
466
|
shouldFlattenDispatch,
|
|
467
|
+
hookEventSurfaceFor,
|
|
468
|
+
extensionEventSurfaceFor,
|
|
408
469
|
};
|
|
@@ -60,9 +60,9 @@ const { resolveModelInternal, resolveGranularityInternal, assertValidGranularity
|
|
|
60
60
|
const { findPhaseInternal } = phaseLocator;
|
|
61
61
|
const { getRoadmapPhaseInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, } = roadmapParser;
|
|
62
62
|
const { pathExistsInternal, generateSlugInternal, toPosixPath } = coreUtils;
|
|
63
|
-
const { normalizePhaseName, phaseTokenMatches } = phaseId;
|
|
63
|
+
const { normalizePhaseName, phaseTokenMatches, stripProjectCodePrefix } = phaseId;
|
|
64
64
|
const { pruneOrphanedWorktrees } = worktreeSafety;
|
|
65
|
-
const { planningPaths, planningDir, planningRoot, findContextMdIn, } = planningWorkspace;
|
|
65
|
+
const { planningPaths, planningDir, planningRoot, getActiveWorkstream, findContextMdIn, } = planningWorkspace;
|
|
66
66
|
const { determinePhaseStatus } = commandsMod;
|
|
67
67
|
const { extractFrontmatter } = frontmatterMod;
|
|
68
68
|
const { readVerificationStatus } = verificationMod;
|
|
@@ -919,9 +919,12 @@ function cmdInitMilestoneOp(cwd, raw) {
|
|
|
919
919
|
const roadmapPath = node_path_1.default.join(planningDir(cwd), 'ROADMAP.md');
|
|
920
920
|
const roadmapRaw = node_fs_1.default.readFileSync(roadmapPath, 'utf-8');
|
|
921
921
|
const currentSection = extractCurrentMilestone(roadmapRaw, cwd);
|
|
922
|
-
|
|
922
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
923
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)(?:\s*\([^)\n]*\))?\s*:/gi;
|
|
923
924
|
let m;
|
|
924
925
|
while ((m = phasePattern.exec(currentSection)) !== null) {
|
|
926
|
+
if (/^999(?:\.|$)/.test(m[1]))
|
|
927
|
+
continue;
|
|
925
928
|
roadmapPhaseNumbers.push(m[1]);
|
|
926
929
|
}
|
|
927
930
|
}
|
|
@@ -938,7 +941,7 @@ function cmdInitMilestoneOp(cwd, raw) {
|
|
|
938
941
|
for (const e of entries) {
|
|
939
942
|
if (!e.isDirectory())
|
|
940
943
|
continue;
|
|
941
|
-
const m = e.name.match(/^(\d+[A-Z]?(?:\.\d+)*)/);
|
|
944
|
+
const m = stripProjectCodePrefix(e.name).match(/^(\d+[A-Z]?(?:\.\d+)*)/);
|
|
942
945
|
if (!m)
|
|
943
946
|
continue;
|
|
944
947
|
diskPhaseDirs.set(canonicalizePhase(m[1]), e.name);
|
|
@@ -1071,7 +1074,8 @@ function cmdInitManager(cwd, raw) {
|
|
|
1071
1074
|
while ((_cbMatch = _cbPattern.exec(content)) !== null) {
|
|
1072
1075
|
_checkboxStates.set(_cbMatch[2], _cbMatch[1].toLowerCase() === 'x');
|
|
1073
1076
|
}
|
|
1074
|
-
|
|
1077
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
1078
|
+
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)(?:\s*\([^)\n]*\))?\s*:\s*([^\n]+)/gi;
|
|
1075
1079
|
const phases = [];
|
|
1076
1080
|
let match;
|
|
1077
1081
|
while ((match = phasePattern.exec(content)) !== null) {
|
|
@@ -1355,6 +1359,29 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1355
1359
|
const config = loadConfig(cwd);
|
|
1356
1360
|
const milestone = getMilestoneInfo(cwd);
|
|
1357
1361
|
const _slashRuntime = (0, runtime_slash_cjs_1.resolveRuntime)(cwd);
|
|
1362
|
+
// #1912: fail safe in workstream mode with no active workstream. With no active
|
|
1363
|
+
// workstream and no --ws, planningDir(cwd) resolves to root .planning — silently
|
|
1364
|
+
// reporting a stale root milestone. Require an explicit workstream instead.
|
|
1365
|
+
// Mirror planningDir's resolution (GSD_WORKSTREAM env > stored active pointer) so
|
|
1366
|
+
// an explicit --ws (which sets GSD_WORKSTREAM) satisfies the check.
|
|
1367
|
+
const _wsRoot = node_path_1.default.join(planningRoot(cwd), 'workstreams');
|
|
1368
|
+
let _availableWorkstreams = [];
|
|
1369
|
+
try {
|
|
1370
|
+
_availableWorkstreams = node_fs_1.default
|
|
1371
|
+
.readdirSync(_wsRoot, { withFileTypes: true })
|
|
1372
|
+
.filter((e) => e.isDirectory())
|
|
1373
|
+
.map((e) => e.name)
|
|
1374
|
+
.sort();
|
|
1375
|
+
}
|
|
1376
|
+
catch {
|
|
1377
|
+
/* no workstreams dir → flat mode */
|
|
1378
|
+
}
|
|
1379
|
+
const _resolvedWorkstream = process.env['GSD_WORKSTREAM'] || getActiveWorkstream(cwd);
|
|
1380
|
+
if (_availableWorkstreams.length > 0 && !_resolvedWorkstream) {
|
|
1381
|
+
error(`init.progress requires a workstream in workstream mode — no active workstream is set, so root STATE.md (likely stale) would be reported. ` +
|
|
1382
|
+
`Pass --ws <name> or run ${(0, runtime_slash_cjs_1.formatGsdSlash)('workstream set', _slashRuntime)} first. ` +
|
|
1383
|
+
`Available workstreams: ${_availableWorkstreams.join(', ')}`);
|
|
1384
|
+
}
|
|
1358
1385
|
const phasesDir = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
1359
1386
|
const phases = [];
|
|
1360
1387
|
let currentPhase = null;
|
|
@@ -1364,7 +1391,8 @@ function cmdInitProgress(cwd, raw) {
|
|
|
1364
1391
|
const roadmapCheckboxStates = new Map();
|
|
1365
1392
|
try {
|
|
1366
1393
|
const roadmapContent = extractCurrentMilestone(node_fs_1.default.readFileSync(node_path_1.default.join(planningDir(cwd), 'ROADMAP.md'), 'utf-8'), cwd);
|
|
1367
|
-
|
|
1394
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
1395
|
+
const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)(?:\s*\([^)\n]*\))?\s*:\s*([^\n]+)/gi;
|
|
1368
1396
|
let hm;
|
|
1369
1397
|
while ((hm = headingPattern.exec(roadmapContent)) !== null) {
|
|
1370
1398
|
roadmapPhaseNums.add(hm[1]);
|