@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
|
@@ -34,6 +34,9 @@ const SCHEMA_VERSION = '1';
|
|
|
34
34
|
// registry generator and the loop-host-contract generator share one source of truth.
|
|
35
35
|
const { LOOP_HOST_CONTRACT } = require('../gsd-core/bin/lib/loop-host-contract.cjs');
|
|
36
36
|
|
|
37
|
+
// Wired-points helper — tells us which points actually have render-hooks call sites.
|
|
38
|
+
const { getWiredLoopPoints } = require('./gen-loop-host-contract.cjs');
|
|
39
|
+
|
|
37
40
|
// Canonical point order — explicit constant (do NOT rely on Set insertion order).
|
|
38
41
|
// Used for point-ordering semantics in consumes-satisfiability validation and topo-sort.
|
|
39
42
|
const POINT_ORDER = [
|
|
@@ -93,16 +96,42 @@ for (const entry of LOOP_HOST_CONTRACT) {
|
|
|
93
96
|
* Loads the set of keys from the central config-schema manifest.
|
|
94
97
|
* Returns a Set<string>. Used for collision detection.
|
|
95
98
|
*
|
|
96
|
-
*
|
|
97
|
-
* (
|
|
99
|
+
* Contract:
|
|
100
|
+
* - ENOENT (file not found): returns empty Set silently — legitimate absent case.
|
|
101
|
+
* - Any other read error OR JSON parse error: writes a prominent warning to stderr
|
|
102
|
+
* naming the schema path and the underlying error, then throws ExitError(1).
|
|
103
|
+
* A parse error clearly states the schema is broken (not merely absent).
|
|
104
|
+
*
|
|
105
|
+
* @param {string} [schemaPath] Path to the config-schema manifest. Defaults to
|
|
106
|
+
* CONFIG_SCHEMA_PATH (the real production path).
|
|
107
|
+
* Overridable for unit testing with fixture paths.
|
|
108
|
+
* @returns {Set<string>}
|
|
98
109
|
*/
|
|
99
|
-
function loadCentralConfigKeys() {
|
|
110
|
+
function loadCentralConfigKeys(schemaPath = CONFIG_SCHEMA_PATH) {
|
|
111
|
+
let raw;
|
|
100
112
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
113
|
+
raw = fs.readFileSync(schemaPath, 'utf8');
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (err.code === 'ENOENT') {
|
|
116
|
+
return new Set();
|
|
117
|
+
}
|
|
118
|
+
process.stderr.write(
|
|
119
|
+
' ERROR Failed to read config-schema manifest at ' + schemaPath + ': ' + err.message + '\n',
|
|
120
|
+
);
|
|
121
|
+
throw new ExitError(1, 'could not read config-schema manifest');
|
|
105
122
|
}
|
|
123
|
+
|
|
124
|
+
let manifest;
|
|
125
|
+
try {
|
|
126
|
+
manifest = JSON.parse(raw);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
process.stderr.write(
|
|
129
|
+
' ERROR Config-schema manifest at ' + schemaPath + ' is broken (JSON parse error): ' + err.message + '\n',
|
|
130
|
+
);
|
|
131
|
+
throw new ExitError(1, 'config-schema manifest JSON is malformed');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return new Set(Array.isArray(manifest.validKeys) ? manifest.validKeys : []);
|
|
106
135
|
}
|
|
107
136
|
|
|
108
137
|
// ─── Config-slice validation ──────────────────────────────────────────────────
|
|
@@ -205,6 +234,7 @@ const KEBAB_RE = /^[a-z][a-z0-9-]*$/;
|
|
|
205
234
|
const VALID_ROLES = new Set(['feature', 'runtime']);
|
|
206
235
|
const VALID_TIERS = new Set(['core', 'standard', 'full']);
|
|
207
236
|
const VALID_ON_ERROR = new Set(['skip', 'halt']);
|
|
237
|
+
const RUNTIME_COMPAT_WILDCARD = '*';
|
|
208
238
|
|
|
209
239
|
/**
|
|
210
240
|
* Validate a single capability declaration.
|
|
@@ -337,9 +367,74 @@ function validateCommandEntry(capId, entry, prefix) {
|
|
|
337
367
|
return errors;
|
|
338
368
|
}
|
|
339
369
|
|
|
370
|
+
function validateRuntimeCompat(capId, runtimeCompat) {
|
|
371
|
+
const errors = [];
|
|
372
|
+
const ctx = 'capability "' + capId + '" runtimeCompat';
|
|
373
|
+
|
|
374
|
+
if (typeof runtimeCompat !== 'object' || runtimeCompat === null || Array.isArray(runtimeCompat)) {
|
|
375
|
+
errors.push(ctx + ' must be an object with supported and unsupported arrays');
|
|
376
|
+
return errors;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const validateRuntimeArray = (field, { allowWildcard }) => {
|
|
380
|
+
const value = runtimeCompat[field];
|
|
381
|
+
if (!Array.isArray(value)) {
|
|
382
|
+
errors.push(ctx + '.' + field + ' must be an array of runtime ids' + (allowWildcard ? ' or ["*"]' : ''));
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (field === 'supported' && value.length === 0) {
|
|
386
|
+
errors.push(ctx + '.supported must be a non-empty array');
|
|
387
|
+
}
|
|
388
|
+
let hasWildcard = false;
|
|
389
|
+
for (let i = 0; i < value.length; i++) {
|
|
390
|
+
const entry = value[i];
|
|
391
|
+
if (typeof entry !== 'string' || entry.length === 0) {
|
|
392
|
+
errors.push(ctx + '.' + field + '[' + i + '] must be a non-empty string');
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (entry === '__proto__' || entry === 'constructor' || entry === 'prototype') {
|
|
396
|
+
errors.push(ctx + '.' + field + '[' + i + '] "' + entry + '" is a reserved name');
|
|
397
|
+
}
|
|
398
|
+
if (entry === RUNTIME_COMPAT_WILDCARD) {
|
|
399
|
+
if (!allowWildcard) {
|
|
400
|
+
errors.push(ctx + '.' + field + ' must not include wildcard "*"');
|
|
401
|
+
}
|
|
402
|
+
hasWildcard = true;
|
|
403
|
+
} else if (!KEBAB_RE.test(entry)) {
|
|
404
|
+
errors.push(ctx + '.' + field + '[' + i + '] must be a kebab-case runtime id or "*"');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (hasWildcard && value.length > 1) {
|
|
408
|
+
errors.push(ctx + '.' + field + ' wildcard "*" cannot be mixed with runtime ids');
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
validateRuntimeArray('supported', { allowWildcard: true });
|
|
413
|
+
validateRuntimeArray('unsupported', { allowWildcard: false });
|
|
414
|
+
|
|
415
|
+
if (runtimeCompat.notes !== undefined) {
|
|
416
|
+
if (typeof runtimeCompat.notes !== 'object' || runtimeCompat.notes === null || Array.isArray(runtimeCompat.notes)) {
|
|
417
|
+
errors.push(ctx + '.notes must be an object of runtime id to string if present');
|
|
418
|
+
} else {
|
|
419
|
+
for (const [key, value] of Object.entries(runtimeCompat.notes)) {
|
|
420
|
+
if (key !== RUNTIME_COMPAT_WILDCARD && !KEBAB_RE.test(key)) {
|
|
421
|
+
errors.push(ctx + '.notes key "' + key + '" must be a kebab-case runtime id or "*"');
|
|
422
|
+
}
|
|
423
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
424
|
+
errors.push(ctx + '.notes["' + key + '"] must be a non-empty string');
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return errors;
|
|
431
|
+
}
|
|
432
|
+
|
|
340
433
|
function validateFeatureBody(cap) {
|
|
341
434
|
const errors = [];
|
|
342
435
|
|
|
436
|
+
errors.push(...validateRuntimeCompat(cap.id || '(unknown)', cap.runtimeCompat));
|
|
437
|
+
|
|
343
438
|
if (!Array.isArray(cap.skills)) {
|
|
344
439
|
errors.push('skills must be an array of strings');
|
|
345
440
|
} else {
|
|
@@ -451,8 +546,10 @@ function validateFeatureBody(cap) {
|
|
|
451
546
|
}
|
|
452
547
|
|
|
453
548
|
// ADR-857 phase 5e: Closed ConverterName enum — complete set used across 16 runtime descriptors,
|
|
454
|
-
// all exported by bin/install.js
|
|
549
|
+
// all exported by bin/install.js (commands/skills) and src/runtime-artifact-conversion.cts (agents).
|
|
550
|
+
// Any ArtifactKind with a non-null converter must use one of these.
|
|
455
551
|
const VALID_CONVERTER_NAMES = new Set([
|
|
552
|
+
// commands / skills converters (pre-existing)
|
|
456
553
|
'convertClaudeCommandToAntigravitySkill',
|
|
457
554
|
'convertClaudeCommandToAugmentSkill',
|
|
458
555
|
'convertClaudeCommandToClineSkill',
|
|
@@ -468,6 +565,16 @@ const VALID_CONVERTER_NAMES = new Set([
|
|
|
468
565
|
'convertClaudeCommandToOpencodeSkill',
|
|
469
566
|
'convertClaudeCommandToTraeSkill',
|
|
470
567
|
'convertClaudeCommandToWindsurfSkill',
|
|
568
|
+
// agent converters (#1173 — descriptor-driven agent conversion wiring)
|
|
569
|
+
'convertClaudeAgentToCopilotAgent',
|
|
570
|
+
'convertClaudeAgentToAntigravityAgent',
|
|
571
|
+
'convertClaudeAgentToCursorAgent',
|
|
572
|
+
'convertClaudeAgentToWindsurfAgent',
|
|
573
|
+
'convertClaudeAgentToAugmentAgent',
|
|
574
|
+
'convertClaudeAgentToTraeAgent',
|
|
575
|
+
'convertClaudeAgentToCodebuddyAgent',
|
|
576
|
+
'convertClaudeAgentToClineAgent',
|
|
577
|
+
'convertClaudeAgentToCodexAgent',
|
|
471
578
|
]);
|
|
472
579
|
|
|
473
580
|
// C3: Validate role:runtime body
|
|
@@ -847,6 +954,78 @@ function validateRuntimeBody(cap) {
|
|
|
847
954
|
return errors;
|
|
848
955
|
}
|
|
849
956
|
|
|
957
|
+
function materializeHookFragments(cap, capDir) {
|
|
958
|
+
const errors = [];
|
|
959
|
+
const hookGroups = [
|
|
960
|
+
['steps', Array.isArray(cap.steps) ? cap.steps : []],
|
|
961
|
+
['contributions', Array.isArray(cap.contributions) ? cap.contributions : []],
|
|
962
|
+
];
|
|
963
|
+
|
|
964
|
+
for (const [groupName, hooks] of hookGroups) {
|
|
965
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
966
|
+
const hook = hooks[i];
|
|
967
|
+
if (!hook || typeof hook !== 'object' || Array.isArray(hook)) continue;
|
|
968
|
+
const fragment = hook.fragment;
|
|
969
|
+
if (!fragment || typeof fragment !== 'object' || Array.isArray(fragment)) continue;
|
|
970
|
+
if (typeof fragment.inline === 'string') continue;
|
|
971
|
+
if (typeof fragment.path !== 'string') continue;
|
|
972
|
+
|
|
973
|
+
const abs = path.resolve(capDir, fragment.path);
|
|
974
|
+
const capRoot = path.resolve(capDir);
|
|
975
|
+
if (abs !== capRoot && !abs.startsWith(capRoot + path.sep)) {
|
|
976
|
+
errors.push(
|
|
977
|
+
cap.id + '/' + groupName + '[' + i + '].fragment.path escapes capability directory: ' +
|
|
978
|
+
fragment.path,
|
|
979
|
+
);
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
try {
|
|
984
|
+
fragment.inline = fs.readFileSync(abs, 'utf8');
|
|
985
|
+
} catch (err) {
|
|
986
|
+
errors.push(
|
|
987
|
+
cap.id + '/' + groupName + '[' + i + '].fragment.path could not be read: ' +
|
|
988
|
+
fragment.path + ' (' + err.message + ')',
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return errors;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function validateFragment(fragment, prefix) {
|
|
998
|
+
const errors = [];
|
|
999
|
+
|
|
1000
|
+
if (typeof fragment !== 'object' || fragment === null || Array.isArray(fragment)) {
|
|
1001
|
+
errors.push(prefix + ' must be an object with path or inline key');
|
|
1002
|
+
return errors;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const hasPath = Object.prototype.hasOwnProperty.call(fragment, 'path');
|
|
1006
|
+
const hasInline = Object.prototype.hasOwnProperty.call(fragment, 'inline');
|
|
1007
|
+
if (!hasPath && !hasInline) {
|
|
1008
|
+
errors.push(prefix + ' must have a "path" or "inline" key');
|
|
1009
|
+
}
|
|
1010
|
+
if (hasInline) {
|
|
1011
|
+
const inline = fragment.inline;
|
|
1012
|
+
if (typeof inline !== 'string') {
|
|
1013
|
+
errors.push(prefix + '.inline must be a string');
|
|
1014
|
+
} else if (inline === '') {
|
|
1015
|
+
errors.push(prefix + '.inline must be a non-empty string');
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
// S1: fragment.path traversal guard — must be a relative path with no ".." segments
|
|
1019
|
+
if (hasPath) {
|
|
1020
|
+
const p = fragment.path;
|
|
1021
|
+
if (typeof p !== 'string' || p === '' || path.isAbsolute(p) || p.split(/[\\/]/).includes('..')) {
|
|
1022
|
+
errors.push(prefix + '.path must be a relative path with no ".." segments');
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
return errors;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
850
1029
|
/**
|
|
851
1030
|
* Validate a single step entry.
|
|
852
1031
|
*
|
|
@@ -866,15 +1045,17 @@ function validateStep(step, prefix, declaredSkills, declaredAgents) {
|
|
|
866
1045
|
}
|
|
867
1046
|
|
|
868
1047
|
if (typeof step.ref !== 'object' || step.ref === null) {
|
|
869
|
-
errors.push(prefix + '.ref must be an object with skill or
|
|
1048
|
+
errors.push(prefix + '.ref must be an object with skill, agent, or command key');
|
|
870
1049
|
} else {
|
|
871
1050
|
const hasSkill = Object.prototype.hasOwnProperty.call(step.ref, 'skill');
|
|
872
1051
|
const hasAgent = Object.prototype.hasOwnProperty.call(step.ref, 'agent');
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1052
|
+
const hasCommand = Object.prototype.hasOwnProperty.call(step.ref, 'command');
|
|
1053
|
+
const dispatchCount = [hasSkill, hasAgent, hasCommand].filter(Boolean).length;
|
|
1054
|
+
if (dispatchCount === 0) {
|
|
1055
|
+
errors.push(prefix + '.ref must have a "skill", "agent", or "command" key');
|
|
1056
|
+
} else if (dispatchCount > 1) {
|
|
1057
|
+
// ref must be exclusive: skill XOR agent XOR command
|
|
1058
|
+
errors.push(prefix + '.ref must have exactly one of "skill", "agent", or "command", not multiple');
|
|
878
1059
|
}
|
|
879
1060
|
if (hasSkill && typeof step.ref.skill !== 'string') {
|
|
880
1061
|
errors.push(prefix + '.ref.skill must be a string');
|
|
@@ -904,6 +1085,9 @@ function validateStep(step, prefix, declaredSkills, declaredAgents) {
|
|
|
904
1085
|
[...declaredAgents].join(', ') + ']',
|
|
905
1086
|
);
|
|
906
1087
|
}
|
|
1088
|
+
if (hasCommand && typeof step.ref.command !== 'string') {
|
|
1089
|
+
errors.push(prefix + '.ref.command must be a string');
|
|
1090
|
+
}
|
|
907
1091
|
}
|
|
908
1092
|
|
|
909
1093
|
if (!Array.isArray(step.produces)) {
|
|
@@ -926,6 +1110,10 @@ function validateStep(step, prefix, declaredSkills, declaredAgents) {
|
|
|
926
1110
|
errors.push(prefix + '.when must be a string if present');
|
|
927
1111
|
}
|
|
928
1112
|
|
|
1113
|
+
if (step.fragment !== undefined) {
|
|
1114
|
+
errors.push(...validateFragment(step.fragment, prefix + '.fragment'));
|
|
1115
|
+
}
|
|
1116
|
+
|
|
929
1117
|
if (!VALID_ON_ERROR.has(step.onError)) {
|
|
930
1118
|
errors.push(prefix + '.onError must be "skip" or "halt" (got: ' + step.onError + ')');
|
|
931
1119
|
}
|
|
@@ -944,23 +1132,24 @@ function validateContribution(contrib, prefix) {
|
|
|
944
1132
|
errors.push(prefix + '.into must be a string (agent role name)');
|
|
945
1133
|
}
|
|
946
1134
|
|
|
947
|
-
if (
|
|
948
|
-
errors.push(prefix + '.
|
|
1135
|
+
if (!Array.isArray(contrib.produces)) {
|
|
1136
|
+
errors.push(prefix + '.produces must be an array');
|
|
949
1137
|
} else {
|
|
950
|
-
const
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
}
|
|
1138
|
+
for (const p of contrib.produces) {
|
|
1139
|
+
if (typeof p !== 'string') errors.push(prefix + '.produces entries must be strings');
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
if (!Array.isArray(contrib.consumes)) {
|
|
1144
|
+
errors.push(prefix + '.consumes must be an array');
|
|
1145
|
+
} else {
|
|
1146
|
+
for (const c of contrib.consumes) {
|
|
1147
|
+
if (typeof c !== 'string') errors.push(prefix + '.consumes entries must be strings');
|
|
961
1148
|
}
|
|
962
1149
|
}
|
|
963
1150
|
|
|
1151
|
+
errors.push(...validateFragment(contrib.fragment, prefix + '.fragment'));
|
|
1152
|
+
|
|
964
1153
|
if (contrib.when !== undefined && typeof contrib.when !== 'string') {
|
|
965
1154
|
errors.push(prefix + '.when must be a string if present');
|
|
966
1155
|
}
|
|
@@ -1146,8 +1335,38 @@ function validateConsumesGlobal(capMap) {
|
|
|
1146
1335
|
}
|
|
1147
1336
|
}
|
|
1148
1337
|
|
|
1149
|
-
//
|
|
1150
|
-
// at the same
|
|
1338
|
+
// Duplicate-producer invariant: two capability steps may not produce the same artifact
|
|
1339
|
+
// at the same Loop Extension Point. Same-point dual production makes data-flow resolution
|
|
1340
|
+
// ambiguous and is rejected at gen time (Decision #6).
|
|
1341
|
+
for (const artifact of Object.keys(capHookProducers)) {
|
|
1342
|
+
if (artifact === '__proto__' || artifact === 'constructor' || artifact === 'prototype') continue;
|
|
1343
|
+
const producers = capHookProducers[artifact];
|
|
1344
|
+
// Group by pointIdx
|
|
1345
|
+
const byPoint = Object.create(null);
|
|
1346
|
+
for (const entry of producers) {
|
|
1347
|
+
if (!byPoint[entry.pointIdx]) byPoint[entry.pointIdx] = [];
|
|
1348
|
+
byPoint[entry.pointIdx].push(entry);
|
|
1349
|
+
}
|
|
1350
|
+
for (const pointIdxStr of Object.keys(byPoint)) {
|
|
1351
|
+
const group = byPoint[pointIdxStr];
|
|
1352
|
+
// Count distinct (capId, stepIdx) producer steps — a single step listing the same
|
|
1353
|
+
// artifact twice in its produces array pushes duplicate entries but represents only
|
|
1354
|
+
// ONE producer step and must not false-positive the cross-step gate.
|
|
1355
|
+
const distinctProducers = new Set(group.map((e) => e.capId + ' ' + e.stepIdx));
|
|
1356
|
+
if (distinctProducers.size >= 2) {
|
|
1357
|
+
const pointIdx = Number(pointIdxStr);
|
|
1358
|
+
const pointName = POINT_ORDER[pointIdx];
|
|
1359
|
+
const capIds = [...new Set(group.map((e) => e.capId))].sort().join(', ');
|
|
1360
|
+
throw new Error(
|
|
1361
|
+
'duplicate-producer invariant violated: artifact "' + artifact + '" is produced by ' +
|
|
1362
|
+
'two or more capability steps at the same Loop Extension Point "' + pointName + '" ' +
|
|
1363
|
+
'(capabilities: ' + capIds + '). ' +
|
|
1364
|
+
'Two capability steps producing the same artifact at the same Loop Extension Point ' +
|
|
1365
|
+
'makes data-flow resolution ambiguous and is rejected at gen time.',
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1151
1370
|
|
|
1152
1371
|
// Now check every hook step's consumes.
|
|
1153
1372
|
// Self-consume rule: a step H cannot satisfy its own consumes[A] from its own produces[A].
|
|
@@ -1297,6 +1516,39 @@ function validateCrossCapability(capMap, centralKeys) {
|
|
|
1297
1516
|
}
|
|
1298
1517
|
}
|
|
1299
1518
|
|
|
1519
|
+
// runtimeCompat: explicit runtime ids must reference runtime capabilities.
|
|
1520
|
+
// The wildcard "*" means descriptor-backed runtimes are supported by default.
|
|
1521
|
+
const runtimeIds = new Set();
|
|
1522
|
+
for (const [id, cap] of capMap) {
|
|
1523
|
+
if (cap.role === 'runtime') runtimeIds.add(id);
|
|
1524
|
+
}
|
|
1525
|
+
for (const [capId, cap] of capMap) {
|
|
1526
|
+
if (cap.role !== 'feature' || typeof cap.runtimeCompat !== 'object' || cap.runtimeCompat === null) continue;
|
|
1527
|
+
for (const field of ['supported', 'unsupported']) {
|
|
1528
|
+
const entries = Array.isArray(cap.runtimeCompat[field]) ? cap.runtimeCompat[field] : [];
|
|
1529
|
+
for (const runtimeId of entries) {
|
|
1530
|
+
if (runtimeId === RUNTIME_COMPAT_WILDCARD) continue;
|
|
1531
|
+
if (typeof runtimeId !== 'string' || runtimeId.length === 0) continue;
|
|
1532
|
+
if (!runtimeIds.has(runtimeId)) {
|
|
1533
|
+
errors.push(
|
|
1534
|
+
'capability "' + capId + '" runtimeCompat.' + field +
|
|
1535
|
+
' references unknown runtime "' + runtimeId + '"',
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
if (cap.runtimeCompat.notes && typeof cap.runtimeCompat.notes === 'object') {
|
|
1541
|
+
for (const runtimeId of Object.keys(cap.runtimeCompat.notes)) {
|
|
1542
|
+
if (runtimeId === RUNTIME_COMPAT_WILDCARD) continue;
|
|
1543
|
+
if (!runtimeIds.has(runtimeId)) {
|
|
1544
|
+
errors.push(
|
|
1545
|
+
'capability "' + capId + '" runtimeCompat.notes references unknown runtime "' + runtimeId + '"',
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1300
1552
|
// requires: acyclic
|
|
1301
1553
|
const cycleErrors = detectRequiresCycles(capMap);
|
|
1302
1554
|
errors.push(...cycleErrors);
|
|
@@ -1384,14 +1636,7 @@ function computeRequiresClosure(id, capMap) {
|
|
|
1384
1636
|
|
|
1385
1637
|
// ─── Topological ordering ─────────────────────────────────────────────────────
|
|
1386
1638
|
|
|
1387
|
-
|
|
1388
|
-
* Topologically sort steps at a given point by produces/consumes.
|
|
1389
|
-
* Capability-id tiebreak for determinism.
|
|
1390
|
-
*
|
|
1391
|
-
* @param {{ capId: string, step: object }[]} entries
|
|
1392
|
-
* @returns {{ capId: string, step: object }[]}
|
|
1393
|
-
*/
|
|
1394
|
-
function topoSortSteps(entries) {
|
|
1639
|
+
function topoSortHookEntries(entries, hookKey, hookKind) {
|
|
1395
1640
|
if (entries.length <= 1) return entries;
|
|
1396
1641
|
|
|
1397
1642
|
// Build adjacency: entry A must come before entry B if B consumes something A produces
|
|
@@ -1400,10 +1645,10 @@ function topoSortSteps(entries) {
|
|
|
1400
1645
|
const adj = Array.from({ length: n }, () => []);
|
|
1401
1646
|
|
|
1402
1647
|
for (let i = 0; i < n; i++) {
|
|
1403
|
-
const producesI = new Set(entries[i].
|
|
1648
|
+
const producesI = new Set(entries[i][hookKey].produces || []);
|
|
1404
1649
|
for (let j = 0; j < n; j++) {
|
|
1405
1650
|
if (i === j) continue;
|
|
1406
|
-
const consumesJ = entries[j].
|
|
1651
|
+
const consumesJ = entries[j][hookKey].consumes || [];
|
|
1407
1652
|
for (const artifact of consumesJ) {
|
|
1408
1653
|
if (producesI.has(artifact)) {
|
|
1409
1654
|
adj[i].push(j);
|
|
@@ -1441,8 +1686,8 @@ function topoSortSteps(entries) {
|
|
|
1441
1686
|
if (result.length < n) {
|
|
1442
1687
|
const sortedIds = entries.map((e) => e.capId).join(', ');
|
|
1443
1688
|
throw new Error(
|
|
1444
|
-
'produces/consumes cycle detected in
|
|
1445
|
-
(entries[0] && entries[0]
|
|
1689
|
+
'produces/consumes cycle detected in ' + hookKind + ' at point "' +
|
|
1690
|
+
(entries[0] && entries[0][hookKey] ? entries[0][hookKey].point : '?') +
|
|
1446
1691
|
'" among capabilities [' + sortedIds + ']: ' +
|
|
1447
1692
|
'a cycle in hook produces/consumes prevents deterministic ordering',
|
|
1448
1693
|
);
|
|
@@ -1450,6 +1695,21 @@ function topoSortSteps(entries) {
|
|
|
1450
1695
|
return result;
|
|
1451
1696
|
}
|
|
1452
1697
|
|
|
1698
|
+
/**
|
|
1699
|
+
* Topologically sort steps at a given point by produces/consumes.
|
|
1700
|
+
* Capability-id tiebreak for determinism.
|
|
1701
|
+
*
|
|
1702
|
+
* @param {{ capId: string, step: object }[]} entries
|
|
1703
|
+
* @returns {{ capId: string, step: object }[]}
|
|
1704
|
+
*/
|
|
1705
|
+
function topoSortSteps(entries) {
|
|
1706
|
+
return topoSortHookEntries(entries, 'step', 'steps');
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
function topoSortContributions(entries) {
|
|
1710
|
+
return topoSortHookEntries(entries, 'contrib', 'contributions');
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1453
1713
|
// ─── ADR-857 Phase 4a: Derived views ─────────────────────────────────────────
|
|
1454
1714
|
|
|
1455
1715
|
// FIX 5 (lazy requires): paths are declared at top level but the actual require()
|
|
@@ -1716,6 +1976,54 @@ function runConfigFormatParityGate(capMap) {
|
|
|
1716
1976
|
}
|
|
1717
1977
|
}
|
|
1718
1978
|
|
|
1979
|
+
// ─── Gen-time wired guard ─────────────────────────────────────────────────────
|
|
1980
|
+
|
|
1981
|
+
/**
|
|
1982
|
+
* Validate that every hook point declared by a capability has a corresponding
|
|
1983
|
+
* `loop render-hooks <point>` call site in one of the host-loop workflow files.
|
|
1984
|
+
*
|
|
1985
|
+
* Only valid loop points (in VALID_LOOP_POINTS) are checked here. Invalid points
|
|
1986
|
+
* are already caught by validateStep/validateContribution/validateGate — do not
|
|
1987
|
+
* double-report.
|
|
1988
|
+
*
|
|
1989
|
+
* @param {object} cap Validated capability object.
|
|
1990
|
+
* @param {Set<string>} wiredSet Set of points that have call sites in host workflows.
|
|
1991
|
+
* @returns {string[]} Array of error strings; empty means all points are wired.
|
|
1992
|
+
*/
|
|
1993
|
+
function validateHooksWired(cap, wiredSet) {
|
|
1994
|
+
const errors = [];
|
|
1995
|
+
const capId = cap.id || '(unknown)';
|
|
1996
|
+
|
|
1997
|
+
function checkPoint(point, groupName, idx) {
|
|
1998
|
+
// Only flag valid points that are unwired — invalid points are schema-validator's job.
|
|
1999
|
+
if (!VALID_LOOP_POINTS.has(point)) return;
|
|
2000
|
+
if (!wiredSet.has(point)) {
|
|
2001
|
+
errors.push(
|
|
2002
|
+
'capability "' + capId + '" ' + groupName + '[' + idx + '].point "' + point +
|
|
2003
|
+
'" is declared but not wired in any host-loop workflow ' +
|
|
2004
|
+
'(no `loop render-hooks ' + point + '` call site). ' +
|
|
2005
|
+
'Wire the call site in the host workflow ' +
|
|
2006
|
+
'(see scripts/gen-loop-host-contract.cjs STEP_WORKFLOWS) or remove the hook.',
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
for (let i = 0; i < (cap.steps || []).length; i++) {
|
|
2012
|
+
const hook = cap.steps[i];
|
|
2013
|
+
if (hook.point !== undefined) checkPoint(hook.point, 'steps', i);
|
|
2014
|
+
}
|
|
2015
|
+
for (let i = 0; i < (cap.contributions || []).length; i++) {
|
|
2016
|
+
const hook = cap.contributions[i];
|
|
2017
|
+
if (hook.point !== undefined) checkPoint(hook.point, 'contributions', i);
|
|
2018
|
+
}
|
|
2019
|
+
for (let i = 0; i < (cap.gates || []).length; i++) {
|
|
2020
|
+
const hook = cap.gates[i];
|
|
2021
|
+
if (hook.point !== undefined) checkPoint(hook.point, 'gates', i);
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
return errors;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
1719
2027
|
// ─── Registry builder ─────────────────────────────────────────────────────────
|
|
1720
2028
|
|
|
1721
2029
|
/**
|
|
@@ -1737,6 +2045,10 @@ function loadAndValidate(centralKeys, capabilitiesDir) {
|
|
|
1737
2045
|
return { capMap, errors };
|
|
1738
2046
|
}
|
|
1739
2047
|
|
|
2048
|
+
// Compute wired points ONCE before iterating capabilities so the filesystem
|
|
2049
|
+
// scan is not repeated per-capability. ROOT is the repo root (defined at top of file).
|
|
2050
|
+
const wiredSet = getWiredLoopPoints(ROOT);
|
|
2051
|
+
|
|
1740
2052
|
const folderEntries = fs.readdirSync(resolvedCapDir, { withFileTypes: true })
|
|
1741
2053
|
.filter((e) => e.isDirectory())
|
|
1742
2054
|
.map((e) => e.name)
|
|
@@ -1768,6 +2080,19 @@ function loadAndValidate(centralKeys, capabilitiesDir) {
|
|
|
1768
2080
|
continue;
|
|
1769
2081
|
}
|
|
1770
2082
|
|
|
2083
|
+
// Gen-time wired guard: reject hooks that declare a valid point with no call site.
|
|
2084
|
+
const wiredErrors = validateHooksWired(cap, wiredSet);
|
|
2085
|
+
if (wiredErrors.length > 0) {
|
|
2086
|
+
for (const e of wiredErrors) errors.push(folderId + '/capability.json: ' + e);
|
|
2087
|
+
continue;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
const fragmentErrors = materializeHookFragments(cap, path.dirname(capPath));
|
|
2091
|
+
if (fragmentErrors.length > 0) {
|
|
2092
|
+
for (const e of fragmentErrors) errors.push(folderId + '/capability.json: ' + e);
|
|
2093
|
+
continue;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
1771
2096
|
capMap.set(cap.id, cap);
|
|
1772
2097
|
}
|
|
1773
2098
|
|
|
@@ -1892,14 +2217,9 @@ function buildRegistry(capMap) {
|
|
|
1892
2217
|
...e.step,
|
|
1893
2218
|
}));
|
|
1894
2219
|
|
|
1895
|
-
// Contributions:
|
|
1896
|
-
const
|
|
1897
|
-
|
|
1898
|
-
const intoCompare = a.contrib.into.localeCompare(b.contrib.into);
|
|
1899
|
-
if (intoCompare !== 0) return intoCompare;
|
|
1900
|
-
return a.capId.localeCompare(b.capId);
|
|
1901
|
-
});
|
|
1902
|
-
byLoopPoint[point].contributions = contribs.map((e) => ({
|
|
2220
|
+
// Contributions: topological sort by produces/consumes, cap-id tiebreak
|
|
2221
|
+
const sortedContribs = topoSortContributions(pointContribs.get(point));
|
|
2222
|
+
byLoopPoint[point].contributions = sortedContribs.map((e) => ({
|
|
1903
2223
|
capId: e.capId,
|
|
1904
2224
|
...e.contrib,
|
|
1905
2225
|
}));
|
|
@@ -2238,12 +2558,14 @@ module.exports = {
|
|
|
2238
2558
|
validateConsumesGlobal,
|
|
2239
2559
|
validateCrossCapability,
|
|
2240
2560
|
classifyCrossErrors,
|
|
2561
|
+
loadCentralConfigKeys,
|
|
2241
2562
|
loadAndValidate,
|
|
2242
2563
|
buildRegistry,
|
|
2243
2564
|
serializeRegistry,
|
|
2244
2565
|
computeRequiresClosure,
|
|
2245
2566
|
topoSortSteps,
|
|
2246
2567
|
normalizeLineEndings,
|
|
2568
|
+
stripGeneratedComment,
|
|
2247
2569
|
validateConfigSliceEntry,
|
|
2248
2570
|
VALID_CONFIG_SLICE_TYPES,
|
|
2249
2571
|
LOOP_HOST_CONTRACT,
|
|
@@ -2252,12 +2574,14 @@ module.exports = {
|
|
|
2252
2574
|
POINT_TO_CONTRACT,
|
|
2253
2575
|
HOST_ARTIFACT_EARLIEST_POINT_IDX,
|
|
2254
2576
|
SCHEMA_VERSION,
|
|
2577
|
+
validateHooksWired,
|
|
2255
2578
|
// ADR-857 phase 4a: derived views + gates
|
|
2256
2579
|
deriveCapabilityClusters,
|
|
2257
2580
|
deriveProfileMembership,
|
|
2258
2581
|
runConsistencyGate,
|
|
2259
2582
|
// ADR-959: command entry validation
|
|
2260
2583
|
validateCommandEntry,
|
|
2584
|
+
validateRuntimeCompat,
|
|
2261
2585
|
// ADR-1016 phase 5a: runtime body validators + closed-vocab sets
|
|
2262
2586
|
validateConfigHome,
|
|
2263
2587
|
validateArtifactLayout,
|
|
@@ -61,7 +61,7 @@ const FAMILIES = [
|
|
|
61
61
|
];
|
|
62
62
|
|
|
63
63
|
function buildManifest() {
|
|
64
|
-
const manifest = {
|
|
64
|
+
const manifest = { families: {} };
|
|
65
65
|
for (const { name, dir, filter, toName } of FAMILIES) {
|
|
66
66
|
manifest.families[name] = fs
|
|
67
67
|
.readdirSync(dir)
|
|
@@ -78,9 +78,6 @@ function main() {
|
|
|
78
78
|
if (flag === '--check') {
|
|
79
79
|
const committed = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
|
|
80
80
|
const live = buildManifest();
|
|
81
|
-
// Strip the generated date for comparison
|
|
82
|
-
delete committed.generated;
|
|
83
|
-
delete live.generated;
|
|
84
81
|
const committedStr = JSON.stringify(committed, null, 2);
|
|
85
82
|
const liveStr = JSON.stringify(live, null, 2);
|
|
86
83
|
if (committedStr !== liveStr) {
|