@opengsd/gsd-core 1.5.0-rc.2 → 1.5.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/plugin.json +1 -1
- package/agents/gsd-advisor-researcher.md +1 -1
- package/agents/gsd-assumptions-analyzer.md +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-code-reviewer.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-writer.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-integration-checker.md +1 -1
- package/agents/gsd-mempalace-curator.md +47 -0
- package/agents/gsd-nyquist-auditor.md +1 -0
- package/agents/gsd-phase-researcher.md +1 -1
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-roadmapper.md +55 -2
- package/agents/gsd-security-auditor.md +1 -0
- package/agents/gsd-ui-auditor.md +1 -1
- package/agents/gsd-ui-checker.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +13 -2
- package/bin/install.js +61 -64
- package/commands/gsd/mempalace-capture.md +71 -0
- package/commands/gsd/mempalace-recall.md +102 -0
- package/commands/gsd/ns-context.md +4 -2
- package/commands/gsd/progress.md +2 -1
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +277 -95
- package/gsd-core/bin/lib/active-workstream-store.cjs +6 -0
- package/gsd-core/bin/lib/capability-activation.cjs +86 -0
- package/gsd-core/bin/lib/capability-registry.cjs +1468 -11
- package/gsd-core/bin/lib/capability-state.cjs +128 -21
- package/gsd-core/bin/lib/capability-writer.cjs +354 -0
- package/gsd-core/bin/lib/check-command-router.cjs +328 -1
- package/gsd-core/bin/lib/clusters.cjs +2 -0
- package/gsd-core/bin/lib/command-roster.cjs +19 -0
- package/gsd-core/bin/lib/commands.cjs +33 -10
- package/gsd-core/bin/lib/config-loader.cjs +7 -8
- package/gsd-core/bin/lib/config-schema.cjs +32 -3
- package/gsd-core/bin/lib/config.cjs +81 -26
- package/gsd-core/bin/lib/core.cjs +5 -2
- package/gsd-core/bin/lib/edge-probe.cjs +25 -2
- package/gsd-core/bin/lib/frontmatter.cjs +53 -1
- package/gsd-core/bin/lib/git-base-branch.cjs +194 -0
- package/gsd-core/bin/lib/init.cjs +36 -11
- package/gsd-core/bin/lib/install-profiles.cjs +57 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +157 -16
- package/gsd-core/bin/lib/model-resolver.cjs +47 -5
- package/gsd-core/bin/lib/phase.cjs +99 -23
- package/gsd-core/bin/lib/plan-drift-guard.cjs +117 -0
- package/gsd-core/bin/lib/probe-core.cjs +117 -1
- package/gsd-core/bin/lib/profile-output.cjs +45 -4
- package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +138 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +13 -3
- package/gsd-core/bin/lib/roadmap.cjs +97 -7
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +1946 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +54 -30
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +27 -19
- package/gsd-core/bin/lib/runtime-homes.cjs +26 -20
- package/gsd-core/bin/lib/state-command-router.cjs +15 -3
- package/gsd-core/bin/lib/state-document.cjs +46 -1
- package/gsd-core/bin/lib/state.cjs +461 -94
- package/gsd-core/bin/lib/verify.cjs +92 -8
- package/gsd-core/bin/lib/worktree-safety.cjs +2 -1
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -2
- package/gsd-core/bin/shared/config-schema.manifest.json +0 -18
- package/gsd-core/bin/shared/model-catalog.json +1 -0
- package/gsd-core/references/edge-probe.md +11 -0
- package/gsd-core/references/loop-hook-dispatch.md +61 -0
- package/gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json +14 -0
- package/gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json +4 -0
- package/gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json +32 -0
- package/gsd-core/references/prohibition-probe.md +248 -0
- package/gsd-core/templates/config.json +1 -1
- package/gsd-core/templates/spec.md +14 -0
- package/gsd-core/workflows/audit-milestone.md +5 -3
- package/gsd-core/workflows/autonomous.md +10 -5
- package/gsd-core/workflows/code-review-fix.md +9 -7
- package/gsd-core/workflows/code-review.md +8 -6
- package/gsd-core/workflows/complete-milestone.md +1 -5
- package/gsd-core/workflows/discuss-phase.md +14 -0
- package/gsd-core/workflows/execute-phase.md +86 -146
- package/gsd-core/workflows/execute-plan.md +21 -6
- package/gsd-core/workflows/help/modes/full.md +7 -1
- package/gsd-core/workflows/new-project.md +3 -3
- package/gsd-core/workflows/next.md +50 -2
- package/gsd-core/workflows/pause-work.md +7 -1
- package/gsd-core/workflows/plan-phase.md +91 -221
- package/gsd-core/workflows/plan-review-convergence.md +14 -4
- package/gsd-core/workflows/pr-branch.md +4 -2
- package/gsd-core/workflows/profile-user.md +3 -1
- package/gsd-core/workflows/progress.md +58 -1
- package/gsd-core/workflows/quick.md +12 -8
- package/gsd-core/workflows/resume-project.md +17 -1
- package/gsd-core/workflows/review.md +19 -2
- package/gsd-core/workflows/secure-phase.md +4 -2
- package/gsd-core/workflows/settings-advanced.md +2 -0
- package/gsd-core/workflows/settings.md +27 -1
- package/gsd-core/workflows/ship.md +58 -5
- package/gsd-core/workflows/spec-phase.md +75 -0
- package/gsd-core/workflows/validate-phase.md +4 -2
- package/gsd-core/workflows/verify-phase.md +14 -4
- package/gsd-core/workflows/verify-work.md +27 -11
- package/hooks/dist/gsd-ensure-canonical-path.js +305 -0
- package/hooks/dist/gsd-statusline.js +1 -1
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-ensure-canonical-path.js +305 -0
- package/hooks/gsd-statusline.js +1 -1
- package/hooks/hooks.json +1 -0
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +5 -4
- package/scripts/affected-tests-lib.cjs +16 -4
- package/scripts/build-hooks.js +7 -0
- package/scripts/changeset/new.cjs +17 -3
- package/scripts/fix-slash-commands.cjs +15 -3
- package/scripts/gen-capability-registry.cjs +373 -49
- package/scripts/gen-inventory-manifest.cjs +1 -4
- package/scripts/gen-loop-host-contract.cjs +55 -0
- package/scripts/issue-version-gate.cjs +140 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +327 -0
- package/scripts/lint-allow-test-rule-refs.cjs +162 -0
- package/scripts/lint-test-file-count.allowlist.json +14 -0
- package/scripts/mutation-matrix.cjs +108 -7
- package/scripts/pr-target-policy.cjs +63 -0
- package/scripts/release-tarball-smoke.cjs +7 -1
- package/scripts/research-profiles.cjs +5 -5
- package/scripts/run-tests.cjs +178 -17
|
@@ -206,7 +206,6 @@ function buildNewProjectConfig(userChoices) {
|
|
|
206
206
|
ui_phase: true,
|
|
207
207
|
ui_safety_gate: true,
|
|
208
208
|
ai_integration_phase: true,
|
|
209
|
-
tdd_mode: false,
|
|
210
209
|
human_verify_mode: 'end-of-phase',
|
|
211
210
|
text_mode: false,
|
|
212
211
|
research_before_questions: false,
|
|
@@ -234,7 +233,7 @@ function buildNewProjectConfig(userChoices) {
|
|
|
234
233
|
project_code: null,
|
|
235
234
|
phase_naming: 'sequential',
|
|
236
235
|
agent_skills: {},
|
|
237
|
-
claude_md_path: '
|
|
236
|
+
claude_md_path: './.claude/CLAUDE.md',
|
|
238
237
|
plan_review: {
|
|
239
238
|
source_grounding: true,
|
|
240
239
|
source_grounding_authority: 'grep',
|
|
@@ -369,6 +368,42 @@ function cmdConfigEnsureSection(cwd, raw) {
|
|
|
369
368
|
output(ensureConfigFileResult, raw, 'exists');
|
|
370
369
|
}
|
|
371
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Shared helper: write a single key-path into an in-memory config object.
|
|
373
|
+
*
|
|
374
|
+
* Prototype-pollution guard: reject dangerous segments via inline literal
|
|
375
|
+
* comparisons on the exact key used to index `current`, immediately before
|
|
376
|
+
* each write. The inline comparison is the barrier CodeQL's
|
|
377
|
+
* js/prototype-pollution-utility query recognises — the previous Set-based
|
|
378
|
+
* pre-loop check was functionally correct but not traced through, so
|
|
379
|
+
* code-scanning alert #26 kept firing. Behaviour is unchanged from #663.
|
|
380
|
+
*
|
|
381
|
+
* Returns the previous value at the leaf key (undefined if absent).
|
|
382
|
+
* Never writes to disk — callers handle persistence.
|
|
383
|
+
* Calls error() (process.exit(1)) on prototype-pollution attempts.
|
|
384
|
+
*/
|
|
385
|
+
function _setNestedValue(config, keyPath, parsedValue) {
|
|
386
|
+
const keys = keyPath.split('.');
|
|
387
|
+
let current = config;
|
|
388
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
389
|
+
const key = keys[i];
|
|
390
|
+
if (key === '__proto__' || key === 'prototype' || key === 'constructor') {
|
|
391
|
+
error('Invalid config key (prototype pollution guard): ' + keyPath, ERROR_REASON.CONFIG_PARSE_FAILED);
|
|
392
|
+
}
|
|
393
|
+
const existingChild = current[key];
|
|
394
|
+
if (existingChild === undefined || existingChild === null || typeof existingChild !== 'object' || Array.isArray(existingChild)) {
|
|
395
|
+
current[key] = {};
|
|
396
|
+
}
|
|
397
|
+
current = current[key];
|
|
398
|
+
}
|
|
399
|
+
const lastKey = keys[keys.length - 1];
|
|
400
|
+
if (lastKey === '__proto__' || lastKey === 'prototype' || lastKey === 'constructor') {
|
|
401
|
+
error('Invalid config key (prototype pollution guard): ' + keyPath, ERROR_REASON.CONFIG_PARSE_FAILED);
|
|
402
|
+
}
|
|
403
|
+
const previousValue = current[lastKey];
|
|
404
|
+
current[lastKey] = parsedValue;
|
|
405
|
+
return previousValue;
|
|
406
|
+
}
|
|
372
407
|
/**
|
|
373
408
|
* Sets a value in the config file, allowing nested values via dot notation (e.g.,
|
|
374
409
|
* "workflow.research").
|
|
@@ -389,35 +424,52 @@ function setConfigValue(cwd, keyPath, parsedValue) {
|
|
|
389
424
|
catch (err) {
|
|
390
425
|
error('Failed to read config.json: ' + err.message, ERROR_REASON.CONFIG_PARSE_FAILED);
|
|
391
426
|
}
|
|
392
|
-
|
|
393
|
-
//
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
427
|
+
const previousValue = _setNestedValue(config, keyPath, parsedValue);
|
|
428
|
+
// Write back
|
|
429
|
+
try {
|
|
430
|
+
(0, shell_command_projection_cjs_1.platformWriteSync)(configPath, JSON.stringify(config, null, 2));
|
|
431
|
+
return { updated: true, key: keyPath, value: parsedValue, previousValue };
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
error('Failed to write config.json: ' + err.message);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Batched sibling of setConfigValue: apply multiple key-path writes in a
|
|
440
|
+
* single load → set-all → write cycle inside ONE withPlanningLock call.
|
|
441
|
+
*
|
|
442
|
+
* Returns { updated: true, results: SetConfigValueResult[] } on success.
|
|
443
|
+
* An empty entries array is a no-op and returns { updated: false, results: [] }.
|
|
444
|
+
*
|
|
445
|
+
* Prototype-pollution guards are enforced per entry (identical inline-literal
|
|
446
|
+
* guards as setConfigValue — CodeQL barrier requirement).
|
|
447
|
+
*/
|
|
448
|
+
function setConfigValues(cwd, entries) {
|
|
449
|
+
if (entries.length === 0) {
|
|
450
|
+
return { updated: false, results: [] };
|
|
451
|
+
}
|
|
452
|
+
const configPath = node_path_1.default.join(planningDir(cwd), 'config.json');
|
|
453
|
+
return withPlanningLock(cwd, () => {
|
|
454
|
+
// Load existing config or start with empty object
|
|
455
|
+
let config = {};
|
|
456
|
+
try {
|
|
457
|
+
if (node_fs_1.default.existsSync(configPath)) {
|
|
458
|
+
config = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
|
|
408
459
|
}
|
|
409
|
-
current = current[key];
|
|
410
460
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
error('Invalid config key (prototype pollution guard): ' + keyPath, ERROR_REASON.CONFIG_PARSE_FAILED);
|
|
461
|
+
catch (err) {
|
|
462
|
+
error('Failed to read config.json: ' + err.message, ERROR_REASON.CONFIG_PARSE_FAILED);
|
|
414
463
|
}
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
464
|
+
const results = [];
|
|
465
|
+
for (const entry of entries) {
|
|
466
|
+
const previousValue = _setNestedValue(config, entry.keyPath, entry.value);
|
|
467
|
+
results.push({ updated: true, key: entry.keyPath, value: entry.value, previousValue });
|
|
468
|
+
}
|
|
469
|
+
// Write back once for all entries
|
|
418
470
|
try {
|
|
419
471
|
(0, shell_command_projection_cjs_1.platformWriteSync)(configPath, JSON.stringify(config, null, 2));
|
|
420
|
-
return { updated: true,
|
|
472
|
+
return { updated: true, results };
|
|
421
473
|
}
|
|
422
474
|
catch (err) {
|
|
423
475
|
error('Failed to write config.json: ' + err.message);
|
|
@@ -735,4 +787,7 @@ module.exports = {
|
|
|
735
787
|
cmdConfigNewProject,
|
|
736
788
|
cmdConfigPath,
|
|
737
789
|
cmdMigrateConfig,
|
|
790
|
+
// Exported for programmatic use by capability-writer and tests
|
|
791
|
+
setConfigValue,
|
|
792
|
+
setConfigValues,
|
|
738
793
|
};
|
|
@@ -47,7 +47,7 @@ const { loadConfig, isGitIgnored, CONFIG_DEFAULTS, _warnUnknownProfileOverrides,
|
|
|
47
47
|
// ─── Model Resolver Module (extracted from core, ADR-857 phase 2f / #888) ────
|
|
48
48
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
49
49
|
const modelResolverModule = require("./model-resolver.cjs");
|
|
50
|
-
const { resolveTierEntry, resolveModelPolicy, resolveModelInternal, VALID_GRANULARITIES, resolveGranularityInternal, assertValidGranularityOverride, resolveModelForTier, VALID_EFFORTS, EFFORT_SET, nextEffort, resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier, } = modelResolverModule;
|
|
50
|
+
const { resolveTierEntry, resolveModelPolicy, resolveModelInternal, VALID_GRANULARITIES, resolveGranularityInternal, assertValidGranularityOverride, resolveModelForTier, VALID_EFFORTS, EFFORT_SET, nextEffort, resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier, _resetModelPolicyWarningCacheForTests, } = modelResolverModule;
|
|
51
51
|
// ─── Path helpers ────────────────────────────────────────────────────────────
|
|
52
52
|
// toPosixPath and detectSubRepos moved to core-utils.cjs (ADR-857 phase 2c / #877).
|
|
53
53
|
// The destructured bindings above (from coreUtilsModule) make them available to
|
|
@@ -305,7 +305,10 @@ module.exports = {
|
|
|
305
305
|
resolveTierEntry,
|
|
306
306
|
resolveModelPolicy,
|
|
307
307
|
KNOWN_PROVIDERS: model_catalog_cjs_1.KNOWN_PROVIDERS,
|
|
308
|
-
_resetRuntimeWarningCacheForTests
|
|
308
|
+
_resetRuntimeWarningCacheForTests: () => {
|
|
309
|
+
_resetRuntimeWarningCacheForTests();
|
|
310
|
+
_resetModelPolicyWarningCacheForTests();
|
|
311
|
+
},
|
|
309
312
|
pathExistsInternal,
|
|
310
313
|
gitWorktreeInfoInternal,
|
|
311
314
|
generateSlugInternal,
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* edges, and (via probe-core) merges author resolutions into a coverage report.
|
|
20
20
|
*/
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.EDGE_VALIDATORS = exports.TAXONOMY = exports.VALID_SHAPES = exports.SHAPE_CUES = void 0;
|
|
22
|
+
exports.EDGE_VALIDATORS = exports.UNCLASSIFIED_CATEGORY = exports.TAXONOMY = exports.VALID_SHAPES = exports.SHAPE_CUES = void 0;
|
|
23
23
|
exports.classifyShape = classifyShape;
|
|
24
24
|
exports.applicableCategories = applicableCategories;
|
|
25
25
|
exports.validateRequirement = validateRequirement;
|
|
@@ -74,8 +74,15 @@ function applicableCategories(shapes) {
|
|
|
74
74
|
* taxonomy; both verification tiers require a non-empty `resolution` (an explicit AC's text
|
|
75
75
|
* or a backstop note) so plan-phase has a criterion to lift.
|
|
76
76
|
*/
|
|
77
|
+
/**
|
|
78
|
+
* Pseudo-category for a requirement whose prose matched NO shape cue (#1110). It is a soft
|
|
79
|
+
* "review manually" signal, NOT a 9th taxonomy category: it stays out of `TAXONOMY` (the closed
|
|
80
|
+
* eight) and only joins `EDGE_VALIDATORS.categories` so `analyzeCoverage` accepts the item.
|
|
81
|
+
*/
|
|
82
|
+
exports.UNCLASSIFIED_CATEGORY = 'unclassified';
|
|
83
|
+
const UNCLASSIFIED_PROBE = 'unclassified — review manually';
|
|
77
84
|
exports.EDGE_VALIDATORS = {
|
|
78
|
-
categories: exports.TAXONOMY.map((c) => c.id),
|
|
85
|
+
categories: [...exports.TAXONOMY.map((c) => c.id), exports.UNCLASSIFIED_CATEGORY],
|
|
79
86
|
verification: ['explicit', 'backstop'],
|
|
80
87
|
requiredFieldsByVerification: { explicit: ['resolution'], backstop: ['resolution'] },
|
|
81
88
|
};
|
|
@@ -126,6 +133,22 @@ function proposeEdges(requirement) {
|
|
|
126
133
|
}
|
|
127
134
|
else {
|
|
128
135
|
shapes = classifyShape(requirement.text);
|
|
136
|
+
if (shapes.length === 0) {
|
|
137
|
+
// Prose present but no shape cue matched. Do NOT silently drop it (#1110): an
|
|
138
|
+
// edge-relevant requirement whose phrasing missed every cue would otherwise vanish from
|
|
139
|
+
// coverage with no signal — the exact blind spot this probe exists to catch. Surface ONE
|
|
140
|
+
// soft, dismissible "unclassified — review manually" candidate. The explicit `shapes: []`
|
|
141
|
+
// opt-out (handled above) stays silent — that is the author's deliberate "no edge surface".
|
|
142
|
+
return [{
|
|
143
|
+
requirement_id: requirement.id,
|
|
144
|
+
category: exports.UNCLASSIFIED_CATEGORY,
|
|
145
|
+
status: 'unresolved',
|
|
146
|
+
verification: null,
|
|
147
|
+
resolution: null,
|
|
148
|
+
reason: null,
|
|
149
|
+
probe: UNCLASSIFIED_PROBE,
|
|
150
|
+
}];
|
|
151
|
+
}
|
|
129
152
|
}
|
|
130
153
|
return applicableCategories(shapes).map((catId) => {
|
|
131
154
|
const cat = exports.TAXONOMY.find((c) => c.id === catId);
|
|
@@ -202,13 +202,60 @@ function reconstructFrontmatter(obj) {
|
|
|
202
202
|
return lines.join('\n');
|
|
203
203
|
}
|
|
204
204
|
function spliceFrontmatter(content, newObj) {
|
|
205
|
-
const yamlStr = reconstructFrontmatter(newObj);
|
|
206
205
|
const match = content.match(/^---\r?\n[\s\S]+?\r?\n---/);
|
|
207
206
|
if (match) {
|
|
207
|
+
// Identity-preservation (additive, lossless round-trip): `reconstructFrontmatter` is a
|
|
208
|
+
// deliberately lossy serializer — it cannot faithfully re-emit nested object-list items
|
|
209
|
+
// (e.g. must_haves.artifacts / must_haves.prohibitions, whose items are `{ path, provides }`
|
|
210
|
+
// / `{ statement, status, … }` maps). When the caller is writing back a value that is
|
|
211
|
+
// STRUCTURALLY UNCHANGED from the original parse (the canonical CRUD round-trip and the
|
|
212
|
+
// #644 prohibition schema round-trip both do this), regenerating from the lossy object would
|
|
213
|
+
// silently mangle those blocks. Detect that case by deep-equality against a re-parse of the
|
|
214
|
+
// original frontmatter and preserve the ORIGINAL raw text verbatim — a true no-op splice.
|
|
215
|
+
// This touches neither the parser (`extractFrontmatter`) nor `parseMustHavesBlock`; it only
|
|
216
|
+
// makes the existing splice faithful when nothing changed. A genuine mutation (different
|
|
217
|
+
// object) still flows through `reconstructFrontmatter` exactly as before.
|
|
218
|
+
try {
|
|
219
|
+
if (frontmatterDeepEqual(extractFrontmatter(content), newObj)) {
|
|
220
|
+
return content;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
/* fall through to regeneration on any comparison hiccup */
|
|
225
|
+
}
|
|
226
|
+
const yamlStr = reconstructFrontmatter(newObj);
|
|
208
227
|
return `---\n${yamlStr}\n---` + content.slice(match[0].length);
|
|
209
228
|
}
|
|
229
|
+
const yamlStr = reconstructFrontmatter(newObj);
|
|
210
230
|
return `---\n${yamlStr}\n---\n\n` + content;
|
|
211
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Structural deep-equality for two parsed frontmatter objects. Order-sensitive for arrays
|
|
234
|
+
* (YAML lists are ordered), key-order-insensitive for objects. Used only by `spliceFrontmatter`
|
|
235
|
+
* to recognize a no-op write-back; intentionally narrow (handles the string / string[] /
|
|
236
|
+
* nested-object shapes `extractFrontmatter` produces).
|
|
237
|
+
*/
|
|
238
|
+
function frontmatterDeepEqual(a, b) {
|
|
239
|
+
if (a === b)
|
|
240
|
+
return true;
|
|
241
|
+
if (a == null || b == null)
|
|
242
|
+
return a === b;
|
|
243
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
244
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
245
|
+
return false;
|
|
246
|
+
return a.every((v, i) => frontmatterDeepEqual(v, b[i]));
|
|
247
|
+
}
|
|
248
|
+
if (typeof a === 'object' && typeof b === 'object') {
|
|
249
|
+
const ao = a;
|
|
250
|
+
const bo = b;
|
|
251
|
+
const ak = Object.keys(ao);
|
|
252
|
+
const bk = Object.keys(bo);
|
|
253
|
+
if (ak.length !== bk.length)
|
|
254
|
+
return false;
|
|
255
|
+
return ak.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && frontmatterDeepEqual(ao[k], bo[k]));
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
212
259
|
function parseMustHavesBlock(content, blockName) {
|
|
213
260
|
// Extract a specific block from must_haves in raw frontmatter YAML
|
|
214
261
|
// Handles 3-level nesting: must_haves > artifacts/key_links > [{path, provides, ...}]
|
|
@@ -431,6 +478,11 @@ function cmdFrontmatterValidate(cwd, filePath, schemaName, raw) {
|
|
|
431
478
|
}
|
|
432
479
|
module.exports = {
|
|
433
480
|
extractFrontmatter,
|
|
481
|
+
// Additive alias (#644 prohibition-probe schema contract): the probe round-trip seam reads a
|
|
482
|
+
// frontmatter object via `parseFrontmatter` (the name the contract test pins). It is the SAME
|
|
483
|
+
// function as `extractFrontmatter` — a bare-object parse with no behavior change — exposed under
|
|
484
|
+
// the alias so the prohibition schema round-trip and any future caller can use the canonical name.
|
|
485
|
+
parseFrontmatter: extractFrontmatter,
|
|
434
486
|
reconstructFrontmatter,
|
|
435
487
|
spliceFrontmatter,
|
|
436
488
|
parseMustHavesBlock,
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Git Base-Branch Resolver — issue #1146.
|
|
4
|
+
*
|
|
5
|
+
* Single source of truth for detecting the repository's default branch.
|
|
6
|
+
* Replaces the duplicated per-workflow bash detection that only consulted
|
|
7
|
+
* `refs/remotes/origin/HEAD` then hardcoded `:-main`, which silently
|
|
8
|
+
* returned "main" for repos whose default branch is "master" whenever
|
|
9
|
+
* origin/HEAD was unset (git init + remote add / fetch without set-head /
|
|
10
|
+
* most CI checkouts / many worktrees).
|
|
11
|
+
*
|
|
12
|
+
* Precedence ladder (highest to lowest):
|
|
13
|
+
* 1. `git.base_branch` config override from .planning/config.json
|
|
14
|
+
* 2. `git symbolic-ref --short refs/remotes/origin/HEAD` (fast, no network)
|
|
15
|
+
* 3. `git remote show origin` HEAD branch ← AUTHORITATIVE; works when #2 unset
|
|
16
|
+
* 4. Local branch existence: "master" present + "main" absent → "master";
|
|
17
|
+
* "main" present → "main"
|
|
18
|
+
* 5. "main" (last-resort default)
|
|
19
|
+
*
|
|
20
|
+
* Every git subprocess is bounded with a timeout (≤ 30 s); on timeout/error
|
|
21
|
+
* the resolver degrades gracefully to the next tier — it never throws.
|
|
22
|
+
*
|
|
23
|
+
* Pure/testable: all I/O is injectable via the `deps` argument so unit
|
|
24
|
+
* tests can run without touching the real filesystem or spawning real git.
|
|
25
|
+
*/
|
|
26
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
27
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
28
|
+
};
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.readConfigBaseBranch = readConfigBaseBranch;
|
|
31
|
+
exports.trySymbolicRef = trySymbolicRef;
|
|
32
|
+
exports.tryRemoteShow = tryRemoteShow;
|
|
33
|
+
exports.tryLocalBranch = tryLocalBranch;
|
|
34
|
+
exports.resolveBaseBranch = resolveBaseBranch;
|
|
35
|
+
exports.cmdGitBaseBranch = cmdGitBaseBranch;
|
|
36
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
37
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
38
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
39
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
40
|
+
/**
|
|
41
|
+
* Safely look up `git.base_branch` from the project's config.json.
|
|
42
|
+
* Returns the configured value (a non-empty, non-null string) or null.
|
|
43
|
+
*/
|
|
44
|
+
function readConfigBaseBranch(planningDir, deps) {
|
|
45
|
+
const readFile = deps?.readFile ??
|
|
46
|
+
((p) => { try {
|
|
47
|
+
return node_fs_1.default.readFileSync(p, 'utf8');
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
} });
|
|
52
|
+
const configPath = node_path_1.default.join(planningDir, 'config.json');
|
|
53
|
+
const raw = readFile(configPath);
|
|
54
|
+
if (!raw)
|
|
55
|
+
return null;
|
|
56
|
+
let cfg;
|
|
57
|
+
try {
|
|
58
|
+
cfg = JSON.parse(raw);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg))
|
|
64
|
+
return null;
|
|
65
|
+
const top = cfg;
|
|
66
|
+
// Support both "git.base_branch" (nested) and "base_branch" (flat legacy)
|
|
67
|
+
const gitSection = top.git;
|
|
68
|
+
if (gitSection && typeof gitSection === 'object' && !Array.isArray(gitSection)) {
|
|
69
|
+
const nested = gitSection.base_branch;
|
|
70
|
+
if (typeof nested === 'string' && nested.trim())
|
|
71
|
+
return nested.trim();
|
|
72
|
+
}
|
|
73
|
+
const flat = top.base_branch;
|
|
74
|
+
if (typeof flat === 'string' && flat.trim())
|
|
75
|
+
return flat.trim();
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Try `git symbolic-ref --short refs/remotes/origin/HEAD` (no network).
|
|
80
|
+
* Strips the `origin/` prefix to return just the branch name.
|
|
81
|
+
* Returns null if unset or on error/timeout.
|
|
82
|
+
*/
|
|
83
|
+
function trySymbolicRef(cwd, execGit) {
|
|
84
|
+
try {
|
|
85
|
+
const r = execGit(['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], { cwd, timeout: 5_000 });
|
|
86
|
+
if (r.exitCode !== 0 || !r.stdout)
|
|
87
|
+
return null;
|
|
88
|
+
// Output is e.g. "origin/main" — strip the prefix
|
|
89
|
+
const branch = r.stdout.trim().replace(/^origin\//, '');
|
|
90
|
+
return branch || null;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Try `git remote show origin` to read the HEAD branch.
|
|
98
|
+
* This is authoritative when origin/HEAD is unset locally.
|
|
99
|
+
* Requires network access but succeeds in the common CI case where
|
|
100
|
+
* origin/HEAD was never set after `git init && git remote add origin`.
|
|
101
|
+
*
|
|
102
|
+
* Parses the line: `HEAD branch: <name>`
|
|
103
|
+
* Returns null on error, timeout, or if the output is malformed.
|
|
104
|
+
*/
|
|
105
|
+
function tryRemoteShow(cwd, execGit) {
|
|
106
|
+
try {
|
|
107
|
+
const r = execGit(['remote', 'show', 'origin'], { cwd, timeout: 15_000 });
|
|
108
|
+
if (r.exitCode !== 0 || !r.stdout)
|
|
109
|
+
return null;
|
|
110
|
+
// The line looks like: " HEAD branch: master"
|
|
111
|
+
const m = r.stdout.match(/^\s*HEAD branch:\s*(\S+)\s*$/m);
|
|
112
|
+
if (!m)
|
|
113
|
+
return null;
|
|
114
|
+
const branch = m[1];
|
|
115
|
+
// git emits "(unknown)" when the remote is offline but the local cache
|
|
116
|
+
// resolved it; treat that as non-authoritative and fall through.
|
|
117
|
+
if (!branch || branch === '(unknown)')
|
|
118
|
+
return null;
|
|
119
|
+
return branch;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Detect local branch existence as a tie-breaker when no remote info is available.
|
|
127
|
+
*
|
|
128
|
+
* Rules:
|
|
129
|
+
* - "master" present AND "main" absent → "master"
|
|
130
|
+
* - "main" present → "main"
|
|
131
|
+
* - Neither → null (fall through to default)
|
|
132
|
+
*
|
|
133
|
+
* Returns null on error/timeout.
|
|
134
|
+
*/
|
|
135
|
+
function tryLocalBranch(cwd, execGit) {
|
|
136
|
+
try {
|
|
137
|
+
const r = execGit(['branch', '--list', 'main', 'master'], { cwd, timeout: 5_000 });
|
|
138
|
+
if (r.exitCode !== 0 || !r.stdout)
|
|
139
|
+
return null;
|
|
140
|
+
// `git branch --list main master` outputs one line per matching branch
|
|
141
|
+
const lines = r.stdout.split('\n').map(l => l.trim().replace(/^\*\s*/, ''));
|
|
142
|
+
const hasMain = lines.includes('main');
|
|
143
|
+
const hasMaster = lines.includes('master');
|
|
144
|
+
if (hasMaster && !hasMain)
|
|
145
|
+
return 'master';
|
|
146
|
+
if (hasMain)
|
|
147
|
+
return 'main';
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Resolve the default/base branch for the repository at `cwd`.
|
|
156
|
+
*
|
|
157
|
+
* Consults the full precedence ladder and always returns a non-empty string.
|
|
158
|
+
* Never throws.
|
|
159
|
+
*/
|
|
160
|
+
function resolveBaseBranch(cwd, deps) {
|
|
161
|
+
const execGit = deps?.execGit ?? shell_command_projection_cjs_1.execGit;
|
|
162
|
+
// Derive .planning dir relative to cwd (mirrors planningDir() in core.cjs)
|
|
163
|
+
const planningDir = node_path_1.default.join(cwd, '.planning');
|
|
164
|
+
// 1. Config override
|
|
165
|
+
const configured = readConfigBaseBranch(planningDir, deps);
|
|
166
|
+
if (configured)
|
|
167
|
+
return configured;
|
|
168
|
+
// 2. symbolic-ref (fast, no network)
|
|
169
|
+
const symref = trySymbolicRef(cwd, execGit);
|
|
170
|
+
if (symref)
|
|
171
|
+
return symref;
|
|
172
|
+
// 3. git remote show origin (authoritative when origin/HEAD unset)
|
|
173
|
+
const remoteShow = tryRemoteShow(cwd, execGit);
|
|
174
|
+
if (remoteShow)
|
|
175
|
+
return remoteShow;
|
|
176
|
+
// 4. Local branch existence
|
|
177
|
+
const local = tryLocalBranch(cwd, execGit);
|
|
178
|
+
if (local)
|
|
179
|
+
return local;
|
|
180
|
+
// 5. Last-resort default
|
|
181
|
+
return 'main';
|
|
182
|
+
}
|
|
183
|
+
// ─── CLI entry point ──────────────────────────────────────────────────────────
|
|
184
|
+
/**
|
|
185
|
+
* CLI command: `gsd-tools git base-branch`
|
|
186
|
+
* Resolves the default branch and writes it to stdout (raw string, newline-terminated).
|
|
187
|
+
* Called by workflows via `gsd_run query git.base-branch`.
|
|
188
|
+
*/
|
|
189
|
+
function cmdGitBaseBranch(cwd, _args, deps) {
|
|
190
|
+
const branch = resolveBaseBranch(cwd, deps);
|
|
191
|
+
const write = deps?.write ?? ((s) => process.stdout.write(s));
|
|
192
|
+
write(branch + '\n');
|
|
193
|
+
return branch;
|
|
194
|
+
}
|
|
@@ -182,10 +182,11 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
182
182
|
? reqMatch[1].replace(/[\[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean).join(', ')
|
|
183
183
|
: null;
|
|
184
184
|
const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null;
|
|
185
|
+
const wf = (config.workflow ?? {});
|
|
185
186
|
const result = {
|
|
186
187
|
executor_model: resolveModelInternal(cwd, 'gsd-executor'),
|
|
187
188
|
verifier_model: resolveModelInternal(cwd, 'gsd-verifier'),
|
|
188
|
-
tdd_mode: options['tdd'] ||
|
|
189
|
+
tdd_mode: options['tdd'] || Boolean(wf['tdd_mode']) || false,
|
|
189
190
|
commit_docs: config.commit_docs,
|
|
190
191
|
sub_repos: config.sub_repos,
|
|
191
192
|
parallelization: config.parallelization,
|
|
@@ -300,15 +301,16 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
300
301
|
const granularityOverride = options['granularity'];
|
|
301
302
|
assertValidGranularityOverride(granularityOverride, error);
|
|
302
303
|
const granularity = resolveGranularityInternal(cwd, 'planning', granularityOverride || undefined);
|
|
304
|
+
const wf = (config.workflow ?? {});
|
|
303
305
|
const result = {
|
|
304
306
|
researcher_model: resolveModelInternal(cwd, 'gsd-phase-researcher'),
|
|
305
307
|
planner_model: resolveModelInternal(cwd, 'gsd-planner'),
|
|
306
308
|
checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'),
|
|
307
|
-
tdd_mode: options['tdd'] ||
|
|
309
|
+
tdd_mode: options['tdd'] || Boolean(wf['tdd_mode']) || false,
|
|
308
310
|
granularity,
|
|
309
|
-
research_enabled:
|
|
311
|
+
research_enabled: wf['research'],
|
|
310
312
|
plan_checker_enabled: config.plan_checker,
|
|
311
|
-
nyquist_validation_enabled:
|
|
313
|
+
nyquist_validation_enabled: wf['nyquist_validation'],
|
|
312
314
|
commit_docs: config.commit_docs,
|
|
313
315
|
text_mode: config.text_mode,
|
|
314
316
|
auto_advance: !!(config.auto_advance),
|
|
@@ -507,12 +509,13 @@ function cmdInitNewMilestone(cwd, raw) {
|
|
|
507
509
|
catch {
|
|
508
510
|
/* intentionally empty */
|
|
509
511
|
}
|
|
512
|
+
const wf = (config.workflow ?? {});
|
|
510
513
|
const result = {
|
|
511
514
|
researcher_model: resolveModelInternal(cwd, 'gsd-project-researcher'),
|
|
512
515
|
synthesizer_model: resolveModelInternal(cwd, 'gsd-research-synthesizer'),
|
|
513
516
|
roadmapper_model: resolveModelInternal(cwd, 'gsd-roadmapper'),
|
|
514
517
|
commit_docs: config.commit_docs,
|
|
515
|
-
research_enabled:
|
|
518
|
+
research_enabled: wf['research'],
|
|
516
519
|
current_milestone: milestone['version'],
|
|
517
520
|
current_milestone_name: milestone['name'],
|
|
518
521
|
latest_completed_milestone: latestCompleted?.version || null,
|
|
@@ -1530,7 +1533,9 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) {
|
|
|
1530
1533
|
// It returns [] cheaply when no roots are configured, so the realpath cost only
|
|
1531
1534
|
// occurs when the caller has actually set trusted_global_roots.
|
|
1532
1535
|
const trustedGlobalRoots = (0, security_cjs_1.loadTrustedGlobalRoots)(config);
|
|
1533
|
-
|
|
1536
|
+
// Each entry is either a filesystem include ({ kind: 'include', ref, display }) or a
|
|
1537
|
+
// Skill-tool directive ({ kind: 'directive', name }) for plugin-provided namespaced skills.
|
|
1538
|
+
const validEntries = [];
|
|
1534
1539
|
for (const skillPath of skillPaths) {
|
|
1535
1540
|
if (typeof skillPath !== 'string')
|
|
1536
1541
|
continue;
|
|
@@ -1540,10 +1545,25 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) {
|
|
|
1540
1545
|
process.stderr.write(`[agent-skills] WARNING: "global:" prefix with empty skill name — skipping\n`);
|
|
1541
1546
|
continue;
|
|
1542
1547
|
}
|
|
1543
|
-
|
|
1548
|
+
// Accept: one or more [A-Za-z0-9_-]+ segments joined by single colons.
|
|
1549
|
+
// Rejects: empty segments (::), leading/trailing colon, dots, slashes, backslashes.
|
|
1550
|
+
if (!/^[A-Za-z0-9_-]+(:[A-Za-z0-9_-]+)*$/.test(skillName)) {
|
|
1544
1551
|
process.stderr.write(`[agent-skills] WARNING: Invalid global skill name "${skillName}" — skipping\n`);
|
|
1545
1552
|
continue;
|
|
1546
1553
|
}
|
|
1554
|
+
const isNamespaced = skillName.includes(':');
|
|
1555
|
+
if (isNamespaced) {
|
|
1556
|
+
// Plugin-provided namespaced skill: no filesystem path exists locally.
|
|
1557
|
+
if (runtime === 'claude') {
|
|
1558
|
+
// Emit a natural-language Skill-tool directive (not a @-include).
|
|
1559
|
+
validEntries.push({ kind: 'directive', name: skillName });
|
|
1560
|
+
}
|
|
1561
|
+
else {
|
|
1562
|
+
process.stderr.write(`[agent-skills] WARNING: Plugin-namespaced skill "global:${skillName}" requires a Skill-tool-capable runtime (claude) — skipping on runtime "${runtime}"\n`);
|
|
1563
|
+
}
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
// Non-namespaced bare name: attempt filesystem resolution as before.
|
|
1547
1567
|
if (globalSkillsBase === null) {
|
|
1548
1568
|
process.stderr.write(`[agent-skills] WARNING: Runtime "${runtime}" does not use a skills directory — "global:${skillName}" is not supported on this runtime\n`);
|
|
1549
1569
|
continue;
|
|
@@ -1567,7 +1587,7 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) {
|
|
|
1567
1587
|
}
|
|
1568
1588
|
process.stderr.write(`[agent-skills] NOTE: Global skill "${skillName}" accepted via trusted_global_roots (resolves outside the default skills dir)\n`);
|
|
1569
1589
|
}
|
|
1570
|
-
|
|
1590
|
+
validEntries.push({ kind: 'include', ref: `${globalSkillDir}/SKILL.md`, display: displayPath });
|
|
1571
1591
|
continue;
|
|
1572
1592
|
}
|
|
1573
1593
|
const pathCheck = (0, security_cjs_1.validatePath)(skillPath, projectRoot);
|
|
@@ -1580,11 +1600,16 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) {
|
|
|
1580
1600
|
process.stderr.write(`[agent-skills] WARNING: Skill not found at "${skillPath}/SKILL.md" — skipping\n`);
|
|
1581
1601
|
continue;
|
|
1582
1602
|
}
|
|
1583
|
-
|
|
1603
|
+
validEntries.push({ kind: 'include', ref: `${skillPath}/SKILL.md`, display: skillPath });
|
|
1584
1604
|
}
|
|
1585
|
-
if (
|
|
1605
|
+
if (validEntries.length === 0)
|
|
1586
1606
|
return '';
|
|
1587
|
-
const lines =
|
|
1607
|
+
const lines = validEntries.map((entry) => {
|
|
1608
|
+
if (entry.kind === 'directive') {
|
|
1609
|
+
return `- Load the \`${entry.name}\` skill via the Skill tool before proceeding (plugin-provided).`;
|
|
1610
|
+
}
|
|
1611
|
+
return `- @${entry.ref}`;
|
|
1612
|
+
}).join('\n');
|
|
1588
1613
|
return `<agent_skills>\nRead these user-configured skills:\n${lines}\n</agent_skills>`;
|
|
1589
1614
|
}
|
|
1590
1615
|
function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
|