@opengsd/gsd-core 1.6.0-rc.2 → 1.6.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/plugin.json +2 -1
- package/agents/gsd-planner.md +8 -57
- package/agents/gsd-security-auditor.md +37 -18
- package/bin/install.js +151 -13
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +35 -3
- package/gsd-core/bin/lib/audit-command-router.cjs +52 -14
- package/gsd-core/bin/lib/capability-lifecycle.cjs +30 -7
- package/gsd-core/bin/lib/capability-registry.cjs +96 -83
- package/gsd-core/bin/lib/capability-validator.cjs +22 -0
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +40 -2
- package/gsd-core/bin/lib/command-routing-hub.cjs +10 -3
- package/gsd-core/bin/lib/config-schema.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +67 -24
- package/gsd-core/bin/lib/coverage.cjs +464 -0
- package/gsd-core/bin/lib/graphify-command-router.cjs +53 -36
- package/gsd-core/bin/lib/init.cjs +8 -4
- package/gsd-core/bin/lib/install-profiles.cjs +6 -3
- package/gsd-core/bin/lib/intel-command-router.cjs +79 -60
- package/gsd-core/bin/lib/planning-workspace.cjs +157 -13
- package/gsd-core/bin/lib/profile-output.cjs +18 -6
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +53 -16
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +21 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +15 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +47 -2
- package/gsd-core/bin/lib/state.cjs +364 -52
- package/gsd-core/bin/lib/surface.cjs +42 -10
- package/gsd-core/bin/lib/update-context.cjs +2 -2
- package/gsd-core/references/planner-guidance.md +66 -0
- package/gsd-core/references/planning-config.md +2 -2
- package/gsd-core/references/security-asvs-levels.md +27 -0
- package/gsd-core/templates/SECURITY.md +6 -4
- package/gsd-core/templates/summary-complex.md +4 -0
- package/gsd-core/templates/summary-minimal.md +3 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +41 -0
- package/gsd-core/workflows/execute-plan.md +5 -0
- package/gsd-core/workflows/new-project.md +4 -4
- package/gsd-core/workflows/plan-phase.md +15 -0
- package/gsd-core/workflows/secure-phase.md +13 -7
- package/gsd-core/workflows/verify-work.md +30 -1
- package/package.json +4 -2
- package/scripts/gen-plugin-skills.cjs +117 -0
- package/scripts/lint-test-file-count.allowlist.json +2 -1
- package/scripts/prompt-injection-scan.sh +4 -0
- package/scripts/release-notes/conventional-title.cjs +88 -0
- package/scripts/release-notes/format-github-release-notes.cjs +4 -3
- package/skills/gsd-add-tests/SKILL.md +38 -0
- package/skills/gsd-ai-integration-phase/SKILL.md +37 -0
- package/skills/gsd-audit-fix/SKILL.md +33 -0
- package/skills/gsd-audit-milestone/SKILL.md +37 -0
- package/skills/gsd-audit-uat/SKILL.md +25 -0
- package/skills/gsd-autonomous/SKILL.md +51 -0
- package/skills/gsd-capture/SKILL.md +67 -0
- package/skills/gsd-cleanup/SKILL.md +24 -0
- package/skills/gsd-code-review/SKILL.md +59 -0
- package/skills/gsd-complete-milestone/SKILL.md +142 -0
- package/skills/gsd-config/SKILL.md +56 -0
- package/skills/gsd-debug/SKILL.md +53 -0
- package/skills/gsd-discuss-phase/SKILL.md +77 -0
- package/skills/gsd-docs-update/SKILL.md +49 -0
- package/skills/gsd-eval-review/SKILL.md +33 -0
- package/skills/gsd-execute-phase/SKILL.md +65 -0
- package/skills/gsd-explore/SKILL.md +28 -0
- package/skills/gsd-extract-learnings/SKILL.md +22 -0
- package/skills/gsd-fast/SKILL.md +31 -0
- package/skills/gsd-forensics/SKILL.md +56 -0
- package/skills/gsd-graphify/SKILL.md +204 -0
- package/skills/gsd-health/SKILL.md +31 -0
- package/skills/gsd-help/SKILL.md +29 -0
- package/skills/gsd-import/SKILL.md +46 -0
- package/skills/gsd-inbox/SKILL.md +39 -0
- package/skills/gsd-ingest-docs/SKILL.md +43 -0
- package/skills/gsd-manager/SKILL.md +45 -0
- package/skills/gsd-map-codebase/SKILL.md +83 -0
- package/skills/gsd-mempalace-capture/SKILL.md +71 -0
- package/skills/gsd-mempalace-recall/SKILL.md +102 -0
- package/skills/gsd-milestone-summary/SKILL.md +51 -0
- package/skills/gsd-mvp-phase/SKILL.md +45 -0
- package/skills/gsd-new-milestone/SKILL.md +45 -0
- package/skills/gsd-new-project/SKILL.md +47 -0
- package/skills/gsd-ns-context/SKILL.md +24 -0
- package/skills/gsd-ns-ideate/SKILL.md +23 -0
- package/skills/gsd-ns-manage/SKILL.md +35 -0
- package/skills/gsd-ns-project/SKILL.md +26 -0
- package/skills/gsd-ns-review/SKILL.md +28 -0
- package/skills/gsd-ns-workflow/SKILL.md +33 -0
- package/skills/gsd-pause-work/SKILL.md +43 -0
- package/skills/gsd-phase/SKILL.md +57 -0
- package/skills/gsd-plan-phase/SKILL.md +63 -0
- package/skills/gsd-plan-review-convergence/SKILL.md +60 -0
- package/skills/gsd-pr-branch/SKILL.md +26 -0
- package/skills/gsd-profile-user/SKILL.md +47 -0
- package/skills/gsd-progress/SKILL.md +49 -0
- package/skills/gsd-quick/SKILL.md +174 -0
- package/skills/gsd-resume-work/SKILL.md +31 -0
- package/skills/gsd-review/SKILL.md +42 -0
- package/skills/gsd-review-backlog/SKILL.md +63 -0
- package/skills/gsd-secure-phase/SKILL.md +36 -0
- package/skills/gsd-settings/SKILL.md +29 -0
- package/skills/gsd-ship/SKILL.md +24 -0
- package/skills/gsd-sketch/SKILL.md +60 -0
- package/skills/gsd-spec-phase/SKILL.md +63 -0
- package/skills/gsd-spike/SKILL.md +57 -0
- package/skills/gsd-stats/SKILL.md +20 -0
- package/skills/gsd-surface/SKILL.md +162 -0
- package/skills/gsd-thread/SKILL.md +24 -0
- package/skills/gsd-ui-phase/SKILL.md +35 -0
- package/skills/gsd-ui-review/SKILL.md +33 -0
- package/skills/gsd-ultraplan-phase/SKILL.md +34 -0
- package/skills/gsd-undo/SKILL.md +35 -0
- package/skills/gsd-update/SKILL.md +50 -0
- package/skills/gsd-validate-phase/SKILL.md +36 -0
- package/skills/gsd-verify-work/SKILL.md +39 -0
- package/skills/gsd-workspace/SKILL.md +53 -0
- package/skills/gsd-workstreams/SKILL.md +70 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Coverage metadata — deterministic UAT routing (#1602)
|
|
4
|
+
*
|
|
5
|
+
* Parses the optional `coverage:` block in a SUMMARY.md frontmatter, validates
|
|
6
|
+
* each deliverable entry against the coverage schema, and classifies each into
|
|
7
|
+
* `auto_passed` (deterministically covered — no human prompt) or `present`
|
|
8
|
+
* (a human UAT checkpoint is required).
|
|
9
|
+
*
|
|
10
|
+
* Design constraints (see issue #1602, plus the Postel/Goodhart/Hyrum analysis):
|
|
11
|
+
* - Lenient parse, strict auto-pass. The parser NEVER throws on malformed
|
|
12
|
+
* input; a structurally surprising entry degrades to `present` + an error.
|
|
13
|
+
* - Fail-safe asymmetry. Auto-pass is the narrow, fully-proven case
|
|
14
|
+
* (strict-boolean `human_judgment:false` AND non-empty all-`pass`
|
|
15
|
+
* verification AND zero validation errors). Everything else is presented to
|
|
16
|
+
* the human. A false-negative is a redundant prompt (the status quo); a
|
|
17
|
+
* false-positive ships a bug UAT existed to catch.
|
|
18
|
+
* - Absent block ≠ empty block. No `coverage:` key → `mode: legacy` so the
|
|
19
|
+
* caller falls through to today's prose-based extraction (byte-identical for
|
|
20
|
+
* un-migrated phases). `coverage: []` → `mode: coverage`, zero entries.
|
|
21
|
+
*
|
|
22
|
+
* The classifier is deterministic code, not a prompt heuristic — the issue's
|
|
23
|
+
* central thesis. Tests assert on the frozen typed-IR surface below, not prose.
|
|
24
|
+
*/
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
29
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
|
+
const io = require("./io.cjs");
|
|
32
|
+
const { output, error } = io;
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
34
|
+
const coreUtils = require("./core-utils.cjs");
|
|
35
|
+
const { toPosixPath } = coreUtils;
|
|
36
|
+
const security_cjs_1 = require("./security.cjs");
|
|
37
|
+
// ─── Frozen typed-IR surface ────────────────────────────────────────────────
|
|
38
|
+
const MODE = Object.freeze({
|
|
39
|
+
COVERAGE: 'coverage',
|
|
40
|
+
LEGACY: 'legacy',
|
|
41
|
+
});
|
|
42
|
+
/** Why an entry was routed to the human path. Order of precedence below. */
|
|
43
|
+
const PRESENT_REASON = Object.freeze({
|
|
44
|
+
VALIDATION_FAILED: 'validation_failed',
|
|
45
|
+
HUMAN_JUDGMENT: 'human_judgment',
|
|
46
|
+
NO_VERIFICATION: 'no_verification',
|
|
47
|
+
VERIFICATION_NOT_PASSING: 'verification_not_passing',
|
|
48
|
+
});
|
|
49
|
+
/** Per-entry validation error codes. */
|
|
50
|
+
const ERROR_CODE = Object.freeze({
|
|
51
|
+
MISSING_ID: 'missing_id',
|
|
52
|
+
MISSING_DESCRIPTION: 'missing_description',
|
|
53
|
+
MISSING_HUMAN_JUDGMENT: 'missing_human_judgment',
|
|
54
|
+
INVALID_HUMAN_JUDGMENT: 'invalid_human_judgment',
|
|
55
|
+
MISSING_RATIONALE: 'missing_rationale',
|
|
56
|
+
DUPLICATE_ID: 'duplicate_id',
|
|
57
|
+
VERIFICATION_NOT_LIST: 'verification_not_list',
|
|
58
|
+
INVALID_KIND: 'invalid_kind',
|
|
59
|
+
INVALID_STATUS: 'invalid_status',
|
|
60
|
+
MISSING_REF: 'missing_ref',
|
|
61
|
+
MALFORMED_ENTRY: 'malformed_entry',
|
|
62
|
+
MALFORMED_BLOCK: 'malformed_block',
|
|
63
|
+
});
|
|
64
|
+
const VALID_KINDS = Object.freeze([
|
|
65
|
+
'unit', 'integration', 'e2e', 'automated_ui', 'manual_procedural', 'other',
|
|
66
|
+
]);
|
|
67
|
+
const VALID_STATUSES = Object.freeze(['pass', 'fail', 'unknown']);
|
|
68
|
+
// ─── YAML-subset block parser (scoped to the coverage schema) ────────────────
|
|
69
|
+
//
|
|
70
|
+
// `extractFrontmatter` (src/frontmatter.cts) flattens `- ` list items to
|
|
71
|
+
// scalars and cannot represent the coverage schema's list-of-maps-with-nested-
|
|
72
|
+
// list-of-maps. `parseMustHavesBlock` is the existing precedent for hand-rolling
|
|
73
|
+
// a focused parser for one schema; this is the same approach, one level deeper.
|
|
74
|
+
// We deliberately do NOT pull in a general YAML engine (no external deps in
|
|
75
|
+
// core; Greenspun's-tenth restraint).
|
|
76
|
+
function lineIndent(line) {
|
|
77
|
+
const m = /^( *)/.exec(line);
|
|
78
|
+
return m ? m[1].length : 0;
|
|
79
|
+
}
|
|
80
|
+
function isSignificant(line) {
|
|
81
|
+
return line.trim() !== '';
|
|
82
|
+
}
|
|
83
|
+
function parseScalar(raw) {
|
|
84
|
+
const t = raw.trim();
|
|
85
|
+
if (t === '')
|
|
86
|
+
return '';
|
|
87
|
+
if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) {
|
|
88
|
+
return t.slice(1, -1);
|
|
89
|
+
}
|
|
90
|
+
if (t === 'true')
|
|
91
|
+
return true;
|
|
92
|
+
if (t === 'false')
|
|
93
|
+
return false;
|
|
94
|
+
if (t === 'null' || t === '~')
|
|
95
|
+
return null;
|
|
96
|
+
return t;
|
|
97
|
+
}
|
|
98
|
+
/** Parse a block of lines (all indented ≥ `indent`) into a value. */
|
|
99
|
+
function parseNode(lines, indent) {
|
|
100
|
+
const firstSig = lines.find(isSignificant);
|
|
101
|
+
if (firstSig === undefined)
|
|
102
|
+
return null;
|
|
103
|
+
if (lineIndent(firstSig) === indent && /^ *-(?: |$)/.test(firstSig)) {
|
|
104
|
+
return parseSequence(lines, indent);
|
|
105
|
+
}
|
|
106
|
+
return parseMapping(lines, indent);
|
|
107
|
+
}
|
|
108
|
+
function parseSequence(lines, indent) {
|
|
109
|
+
const items = [];
|
|
110
|
+
// Item-start lines: at exactly `indent`, beginning with a dash.
|
|
111
|
+
const starts = [];
|
|
112
|
+
for (let i = 0; i < lines.length; i++) {
|
|
113
|
+
if (!isSignificant(lines[i]))
|
|
114
|
+
continue;
|
|
115
|
+
if (lineIndent(lines[i]) === indent && /^ *-(?: |$)/.test(lines[i]))
|
|
116
|
+
starts.push(i);
|
|
117
|
+
}
|
|
118
|
+
for (let k = 0; k < starts.length; k++) {
|
|
119
|
+
const start = starts[k];
|
|
120
|
+
const end = k + 1 < starts.length ? starts[k + 1] : lines.length;
|
|
121
|
+
const itemLines = lines.slice(start, end);
|
|
122
|
+
// Re-base the dash line: replace the `indent` + "- " prefix with spaces so
|
|
123
|
+
// the inline content aligns at `indent + 2` and parses as a normal node.
|
|
124
|
+
itemLines[0] = ' '.repeat(indent + 2) + itemLines[0].slice(indent + 2);
|
|
125
|
+
const itemFirst = itemLines.find(isSignificant);
|
|
126
|
+
const head = itemFirst ? itemFirst.trim() : '';
|
|
127
|
+
if (/^[\w-]+:(?: |$)/.test(head)) {
|
|
128
|
+
items.push(parseMapping(itemLines, indent + 2));
|
|
129
|
+
}
|
|
130
|
+
else if (head === '') {
|
|
131
|
+
items.push(null);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
items.push(parseScalar(head));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return items;
|
|
138
|
+
}
|
|
139
|
+
function parseMapping(lines, indent) {
|
|
140
|
+
const map = {};
|
|
141
|
+
let i = 0;
|
|
142
|
+
while (i < lines.length) {
|
|
143
|
+
const line = lines[i];
|
|
144
|
+
if (!isSignificant(line) || lineIndent(line) !== indent) {
|
|
145
|
+
i++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const km = /^[\w-]+:\s*(.*)$/.exec(line.trim());
|
|
149
|
+
if (!km) {
|
|
150
|
+
i++;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const key = /^([\w-]+):/.exec(line.trim())[1];
|
|
154
|
+
const inlineVal = km[1];
|
|
155
|
+
if (inlineVal === '[]') {
|
|
156
|
+
setKey(map, key, []);
|
|
157
|
+
i++;
|
|
158
|
+
}
|
|
159
|
+
else if (inlineVal === '') {
|
|
160
|
+
// Nested block: following lines indented deeper than `indent`.
|
|
161
|
+
let j = i + 1;
|
|
162
|
+
while (j < lines.length && (!isSignificant(lines[j]) || lineIndent(lines[j]) > indent))
|
|
163
|
+
j++;
|
|
164
|
+
const block = lines.slice(i + 1, j);
|
|
165
|
+
const blockFirst = block.find(isSignificant);
|
|
166
|
+
if (blockFirst === undefined) {
|
|
167
|
+
setKey(map, key, null);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
setKey(map, key, parseNode(block, lineIndent(blockFirst)));
|
|
171
|
+
}
|
|
172
|
+
i = j;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
setKey(map, key, parseScalar(inlineVal));
|
|
176
|
+
i++;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return map;
|
|
180
|
+
}
|
|
181
|
+
// Prototype-pollution-safe assignment (CodeQL js/prototype-pollution-utility:
|
|
182
|
+
// inline literal key guard at the write site).
|
|
183
|
+
function setKey(obj, key, value) {
|
|
184
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype')
|
|
185
|
+
return;
|
|
186
|
+
obj[key] = value;
|
|
187
|
+
}
|
|
188
|
+
// ─── Frontmatter region helpers ──────────────────────────────────────────────
|
|
189
|
+
function getFrontmatterYaml(content) {
|
|
190
|
+
const headerEnd = content.startsWith('---\r\n') ? 5 : content.startsWith('---\n') ? 4 : -1;
|
|
191
|
+
if (headerEnd === -1)
|
|
192
|
+
return null;
|
|
193
|
+
const closingLineStart = content.indexOf('\n---', headerEnd);
|
|
194
|
+
if (closingLineStart === -1)
|
|
195
|
+
return null;
|
|
196
|
+
const yamlEnd = content[closingLineStart - 1] === '\r' ? closingLineStart - 1 : closingLineStart;
|
|
197
|
+
return content.slice(headerEnd, yamlEnd);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Locate and parse the top-level `coverage:` block from a SUMMARY document.
|
|
201
|
+
* `malformed` is true when a `coverage:` key IS present with body content that
|
|
202
|
+
* does NOT parse into a non-empty sequence of entries — a distinct, fail-safe
|
|
203
|
+
* signal so a broken block can never masquerade as "all covered" (the caller
|
|
204
|
+
* falls back to prose extraction and surfaces the error). Distinct from
|
|
205
|
+
* `coverage: []` / an empty body, which is the legitimate zero-entry case.
|
|
206
|
+
*/
|
|
207
|
+
function parseCoverage(content) {
|
|
208
|
+
const yaml = getFrontmatterYaml(content);
|
|
209
|
+
if (yaml === null)
|
|
210
|
+
return { found: false, entries: [], malformed: false };
|
|
211
|
+
const lines = yaml.split(/\r?\n/);
|
|
212
|
+
let covIdx = -1;
|
|
213
|
+
for (let i = 0; i < lines.length; i++) {
|
|
214
|
+
if (/^coverage:(?:\s|$)/.test(lines[i])) {
|
|
215
|
+
covIdx = i;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (covIdx === -1)
|
|
220
|
+
return { found: false, entries: [], malformed: false };
|
|
221
|
+
// Strip a trailing YAML comment from the header value. The `coverage:` header
|
|
222
|
+
// only ever carries `[]` or a comment — refs (which legitimately contain `#`)
|
|
223
|
+
// live in quoted scalars on deeper lines, never on this line.
|
|
224
|
+
const rawInline = /^coverage:\s*(.*)$/.exec(lines[covIdx])[1];
|
|
225
|
+
const inline = rawInline.replace(/\s*#.*$/, '').trim();
|
|
226
|
+
if (inline === '[]')
|
|
227
|
+
return { found: true, entries: [], malformed: false };
|
|
228
|
+
if (inline !== '') {
|
|
229
|
+
// A non-empty, non-`[]` inline scalar where a block was expected is malformed.
|
|
230
|
+
return { found: true, entries: [], malformed: true };
|
|
231
|
+
}
|
|
232
|
+
// Gather the block body: every line after the header up to the next top-level
|
|
233
|
+
// frontmatter key (a `key:` at column 0) or end of frontmatter. Mis-indented
|
|
234
|
+
// lines (tabs, wrong column) are INCLUDED so they surface as a malformed block
|
|
235
|
+
// rather than being silently excluded and the block read as falsely empty.
|
|
236
|
+
let j = covIdx + 1;
|
|
237
|
+
while (j < lines.length) {
|
|
238
|
+
const l = lines[j];
|
|
239
|
+
if (l.trim() === '') {
|
|
240
|
+
j++;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (/^[A-Za-z0-9_-]+:(?:\s|$)/.test(l))
|
|
244
|
+
break; // next top-level key
|
|
245
|
+
j++;
|
|
246
|
+
}
|
|
247
|
+
const block = lines.slice(covIdx + 1, j);
|
|
248
|
+
const blockFirst = block.find(isSignificant);
|
|
249
|
+
if (blockFirst === undefined)
|
|
250
|
+
return { found: true, entries: [], malformed: false }; // empty body == coverage: []
|
|
251
|
+
const node = parseNode(block, lineIndent(blockFirst));
|
|
252
|
+
if (!Array.isArray(node) || node.length === 0) {
|
|
253
|
+
// Body had content but did not parse into a sequence of entries → malformed.
|
|
254
|
+
return { found: true, entries: [], malformed: true };
|
|
255
|
+
}
|
|
256
|
+
return { found: true, entries: node, malformed: false };
|
|
257
|
+
}
|
|
258
|
+
// ─── Validation ───────────────────────────────────────────────────────────────
|
|
259
|
+
function isPlainObject(v) {
|
|
260
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
261
|
+
}
|
|
262
|
+
function validateEntry(entry, index, seenIds) {
|
|
263
|
+
const errors = [];
|
|
264
|
+
// Object-check FIRST — before any property access — so a `null`/scalar
|
|
265
|
+
// sequence item (e.g. a bare `-` or `- "string"`) can never throw.
|
|
266
|
+
if (!isPlainObject(entry)) {
|
|
267
|
+
errors.push({ index, id: null, code: ERROR_CODE.MALFORMED_ENTRY, message: 'coverage entry is not a mapping' });
|
|
268
|
+
return errors;
|
|
269
|
+
}
|
|
270
|
+
const id = typeof entry.id === 'string' ? entry.id : null;
|
|
271
|
+
const push = (code, message, field) => {
|
|
272
|
+
errors.push({ index, id, code, field, message });
|
|
273
|
+
};
|
|
274
|
+
if (typeof entry.id !== 'string' || entry.id.trim() === '') {
|
|
275
|
+
push(ERROR_CODE.MISSING_ID, 'entry is missing a non-empty id', 'id');
|
|
276
|
+
}
|
|
277
|
+
else if (seenIds.has(entry.id)) {
|
|
278
|
+
push(ERROR_CODE.DUPLICATE_ID, `duplicate coverage id "${entry.id}"`, 'id');
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
seenIds.add(entry.id);
|
|
282
|
+
}
|
|
283
|
+
if (typeof entry.description !== 'string' || entry.description.trim() === '') {
|
|
284
|
+
push(ERROR_CODE.MISSING_DESCRIPTION, 'entry is missing a non-empty description', 'description');
|
|
285
|
+
}
|
|
286
|
+
if (!('human_judgment' in entry)) {
|
|
287
|
+
push(ERROR_CODE.MISSING_HUMAN_JUDGMENT, 'entry is missing the required human_judgment flag', 'human_judgment');
|
|
288
|
+
}
|
|
289
|
+
else if (typeof entry.human_judgment !== 'boolean') {
|
|
290
|
+
push(ERROR_CODE.INVALID_HUMAN_JUDGMENT, 'human_judgment must be a boolean (true|false)', 'human_judgment');
|
|
291
|
+
}
|
|
292
|
+
if (entry.human_judgment === true && (typeof entry.rationale !== 'string' || entry.rationale.trim() === '')) {
|
|
293
|
+
push(ERROR_CODE.MISSING_RATIONALE, 'rationale is required when human_judgment is true', 'rationale');
|
|
294
|
+
}
|
|
295
|
+
const v = entry.verification;
|
|
296
|
+
if (v !== undefined && !Array.isArray(v)) {
|
|
297
|
+
push(ERROR_CODE.VERIFICATION_NOT_LIST, 'verification must be a list', 'verification');
|
|
298
|
+
}
|
|
299
|
+
else if (Array.isArray(v)) {
|
|
300
|
+
v.forEach((ve, vi) => {
|
|
301
|
+
if (!isPlainObject(ve)) {
|
|
302
|
+
push(ERROR_CODE.MALFORMED_ENTRY, 'verification item is not a mapping', `verification[${vi}]`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (typeof ve.kind !== 'string' || !VALID_KINDS.includes(ve.kind)) {
|
|
306
|
+
push(ERROR_CODE.INVALID_KIND, `verification kind must be one of ${VALID_KINDS.join(', ')}`, `verification[${vi}].kind`);
|
|
307
|
+
}
|
|
308
|
+
if (typeof ve.status !== 'string' || !VALID_STATUSES.includes(ve.status)) {
|
|
309
|
+
push(ERROR_CODE.INVALID_STATUS, `verification status must be one of ${VALID_STATUSES.join(', ')}`, `verification[${vi}].status`);
|
|
310
|
+
}
|
|
311
|
+
if (typeof ve.ref !== 'string' || ve.ref.trim() === '') {
|
|
312
|
+
push(ERROR_CODE.MISSING_REF, 'verification entry is missing a non-empty ref', `verification[${vi}].ref`);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return errors;
|
|
317
|
+
}
|
|
318
|
+
// ─── Classification ───────────────────────────────────────────────────────────
|
|
319
|
+
function verificationList(entry) {
|
|
320
|
+
return Array.isArray(entry.verification) ? entry.verification : [];
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Auto-pass is the narrow, fully-proven case:
|
|
324
|
+
* - zero validation errors, AND
|
|
325
|
+
* - human_judgment is the strict boolean `false`, AND
|
|
326
|
+
* - verification is a NON-EMPTY list, AND
|
|
327
|
+
* - every verification entry has status === 'pass'.
|
|
328
|
+
* The non-empty guard defeats the vacuous-`every` trap; the strict-boolean
|
|
329
|
+
* guard defeats a gamed string flag; the zero-errors guard means a malformed
|
|
330
|
+
* entry can never auto-pass.
|
|
331
|
+
*/
|
|
332
|
+
function isAutoPass(entry, errors) {
|
|
333
|
+
if (errors.length > 0)
|
|
334
|
+
return false;
|
|
335
|
+
if (entry.human_judgment !== false)
|
|
336
|
+
return false;
|
|
337
|
+
const v = verificationList(entry);
|
|
338
|
+
if (v.length === 0)
|
|
339
|
+
return false;
|
|
340
|
+
return v.every((ve) => isPlainObject(ve) && ve.status === 'pass');
|
|
341
|
+
}
|
|
342
|
+
function presentReason(entry, errors) {
|
|
343
|
+
if (errors.length > 0)
|
|
344
|
+
return PRESENT_REASON.VALIDATION_FAILED;
|
|
345
|
+
if (entry.human_judgment === true)
|
|
346
|
+
return PRESENT_REASON.HUMAN_JUDGMENT;
|
|
347
|
+
const v = verificationList(entry);
|
|
348
|
+
if (v.length === 0)
|
|
349
|
+
return PRESENT_REASON.NO_VERIFICATION;
|
|
350
|
+
return PRESENT_REASON.VERIFICATION_NOT_PASSING;
|
|
351
|
+
}
|
|
352
|
+
function san(value) {
|
|
353
|
+
return typeof value === 'string' ? (0, security_cjs_1.sanitizeForDisplay)(value) : null;
|
|
354
|
+
}
|
|
355
|
+
function entryView(entry) {
|
|
356
|
+
// Null-safe: a malformed (non-object) entry still gets a minimal view so it
|
|
357
|
+
// can be presented to the human rather than dropped or throwing.
|
|
358
|
+
if (!isPlainObject(entry)) {
|
|
359
|
+
return { id: null, description: null, verification: [], human_judgment: null };
|
|
360
|
+
}
|
|
361
|
+
const verification = verificationList(entry).map((ve) => ({
|
|
362
|
+
kind: isPlainObject(ve) && typeof ve.kind === 'string' ? ve.kind : null,
|
|
363
|
+
ref: isPlainObject(ve) ? san(ve.ref) : null,
|
|
364
|
+
status: isPlainObject(ve) && typeof ve.status === 'string' ? ve.status : null,
|
|
365
|
+
}));
|
|
366
|
+
const view = {
|
|
367
|
+
id: san(entry.id),
|
|
368
|
+
description: san(entry.description),
|
|
369
|
+
verification,
|
|
370
|
+
human_judgment: typeof entry.human_judgment === 'boolean' ? entry.human_judgment : null,
|
|
371
|
+
};
|
|
372
|
+
if (typeof entry.requirement === 'string')
|
|
373
|
+
view.requirement = (0, security_cjs_1.sanitizeForDisplay)(entry.requirement);
|
|
374
|
+
if (typeof entry.rationale === 'string')
|
|
375
|
+
view.rationale = (0, security_cjs_1.sanitizeForDisplay)(entry.rationale);
|
|
376
|
+
return view;
|
|
377
|
+
}
|
|
378
|
+
function legacyResult(summaryFile, errors) {
|
|
379
|
+
return {
|
|
380
|
+
mode: MODE.LEGACY,
|
|
381
|
+
summary_file: summaryFile,
|
|
382
|
+
total: 0,
|
|
383
|
+
all_auto_covered: false,
|
|
384
|
+
auto_passed: [],
|
|
385
|
+
present: [],
|
|
386
|
+
errors,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
/** Pure classification core — no I/O. Testable in isolation. */
|
|
390
|
+
function classifyContent(content, summaryFile) {
|
|
391
|
+
const { found, entries, malformed } = parseCoverage(content);
|
|
392
|
+
if (!found)
|
|
393
|
+
return legacyResult(summaryFile, []);
|
|
394
|
+
if (malformed) {
|
|
395
|
+
// A coverage block is present but unparseable. Fail-safe: fall back to the
|
|
396
|
+
// prose `## Accomplishments` path (the human still gets UAT) and surface the
|
|
397
|
+
// error so the author can fix the block. NEVER report all_auto_covered here.
|
|
398
|
+
return legacyResult(summaryFile, [{
|
|
399
|
+
index: -1,
|
|
400
|
+
id: null,
|
|
401
|
+
code: ERROR_CODE.MALFORMED_BLOCK,
|
|
402
|
+
message: 'coverage block is present but could not be parsed into entries; falling back to prose extraction',
|
|
403
|
+
}]);
|
|
404
|
+
}
|
|
405
|
+
const seenIds = new Set();
|
|
406
|
+
const autoPassed = [];
|
|
407
|
+
const present = [];
|
|
408
|
+
const allErrors = [];
|
|
409
|
+
entries.forEach((entry, index) => {
|
|
410
|
+
const errs = validateEntry(entry, index, seenIds);
|
|
411
|
+
allErrors.push(...errs);
|
|
412
|
+
const view = entryView(entry);
|
|
413
|
+
if (isAutoPass(entry, errs)) {
|
|
414
|
+
autoPassed.push({ ...view, source: 'automated' });
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
present.push({ ...view, reason: presentReason(entry, errs) });
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
return {
|
|
421
|
+
mode: MODE.COVERAGE,
|
|
422
|
+
summary_file: summaryFile,
|
|
423
|
+
total: entries.length,
|
|
424
|
+
all_auto_covered: present.length === 0,
|
|
425
|
+
auto_passed: autoPassed,
|
|
426
|
+
present,
|
|
427
|
+
errors: allErrors,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
// ─── CLI command ────────────────────────────────────────────────────────────
|
|
431
|
+
function cmdClassify(cwd, options = {}, raw) {
|
|
432
|
+
const filePath = options.summary || options.file;
|
|
433
|
+
if (!filePath) {
|
|
434
|
+
error('SUMMARY file required: use uat classify-coverage --summary <path>');
|
|
435
|
+
}
|
|
436
|
+
let resolvedPath;
|
|
437
|
+
try {
|
|
438
|
+
resolvedPath = (0, security_cjs_1.requireSafePath)(filePath, cwd, 'SUMMARY file', { allowAbsolute: true });
|
|
439
|
+
}
|
|
440
|
+
catch (e) {
|
|
441
|
+
// Emit a structured command error instead of leaking a raw stack trace.
|
|
442
|
+
error(`Invalid SUMMARY path: ${e instanceof Error ? e.message : 'unsafe path'}`);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (!node_fs_1.default.existsSync(resolvedPath)) {
|
|
446
|
+
error(`SUMMARY file not found: ${filePath}`);
|
|
447
|
+
}
|
|
448
|
+
const content = node_fs_1.default.readFileSync(resolvedPath, 'utf-8');
|
|
449
|
+
const result = classifyContent(content, toPosixPath(node_path_1.default.relative(cwd, resolvedPath)));
|
|
450
|
+
output(result, raw, undefined);
|
|
451
|
+
}
|
|
452
|
+
module.exports = {
|
|
453
|
+
cmdClassify,
|
|
454
|
+
classifyContent,
|
|
455
|
+
parseCoverage,
|
|
456
|
+
validateEntry,
|
|
457
|
+
isAutoPass,
|
|
458
|
+
presentReason,
|
|
459
|
+
MODE,
|
|
460
|
+
PRESENT_REASON,
|
|
461
|
+
ERROR_CODE,
|
|
462
|
+
VALID_KINDS,
|
|
463
|
+
VALID_STATUSES,
|
|
464
|
+
};
|
|
@@ -26,46 +26,63 @@
|
|
|
26
26
|
const graphify = require("./graphify.cjs");
|
|
27
27
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
28
|
const io = require("./io.cjs");
|
|
29
|
+
// Phase 2 (#1646): route through the Hub per ADR-959 §III(B) line 75.
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
|
+
const commandRoutingHub = require("./command-routing-hub.cjs");
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
33
|
+
const cjsCommandRouterAdapter = require("./cjs-command-router-adapter.cjs");
|
|
29
34
|
const { output, ERROR_REASON } = io;
|
|
35
|
+
const { makeInvalidArgs } = commandRoutingHub;
|
|
36
|
+
const { routeHubCommandFamily } = cjsCommandRouterAdapter;
|
|
30
37
|
// ─── Implementation ───────────────────────────────────────────────────────────
|
|
31
38
|
function routeGraphifyCommand({ args, cwd, raw, error, _graphify }) {
|
|
32
|
-
const subcommand = args[1];
|
|
33
39
|
const g = _graphify ?? graphify;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
40
|
+
// Phase 2 (#1646): routes through the Command Routing Hub per ADR-959 §III(B)
|
|
41
|
+
// line 75. Validation handlers return `makeInvalidArgs(...)` Results (Q2=C,
|
|
42
|
+
// Q4=ii); the Hub → adapter translation preserves ERROR_REASON granularity
|
|
43
|
+
// via the exitReason field (Phase 1, #1644). Success handlers keep direct
|
|
44
|
+
// `output()` calls (audit's formatAuditReport quirk sets this precedent).
|
|
45
|
+
// The unknown-subcommand path is owned by the Hub's manifest check; the
|
|
46
|
+
// adapter passes SDK_UNKNOWN_COMMAND for UnknownCommand Results.
|
|
47
|
+
routeHubCommandFamily({
|
|
48
|
+
family: 'graphify',
|
|
49
|
+
args,
|
|
50
|
+
// Alphabetical order produces a stable, byte-identical `Available:` list
|
|
51
|
+
// in the unknown-subcommand message (matches the pre-conversion text).
|
|
52
|
+
subcommands: ['build', 'diff', 'query', 'status'],
|
|
53
|
+
handlers: {
|
|
54
|
+
query: () => {
|
|
55
|
+
const term = args[2];
|
|
56
|
+
if (!term) {
|
|
57
|
+
return makeInvalidArgs('term', 'Usage: gsd-tools graphify query <term>', ERROR_REASON.USAGE);
|
|
58
|
+
}
|
|
59
|
+
const budgetIdx = args.indexOf('--budget');
|
|
60
|
+
let budget = null;
|
|
61
|
+
if (budgetIdx !== -1) {
|
|
62
|
+
const rawBudget = args[budgetIdx + 1];
|
|
63
|
+
if (rawBudget === undefined || Number.isNaN(parseInt(rawBudget, 10))) {
|
|
64
|
+
return makeInvalidArgs('--budget', 'Usage: gsd-tools graphify query <term> [--budget <N>]', ERROR_REASON.USAGE);
|
|
65
|
+
}
|
|
66
|
+
budget = parseInt(rawBudget, 10);
|
|
67
|
+
}
|
|
68
|
+
output(g.graphifyQuery(cwd, term, { budget }), raw);
|
|
69
|
+
},
|
|
70
|
+
status: () => output(g.graphifyStatus(cwd), raw),
|
|
71
|
+
diff: () => output(g.graphifyDiff(cwd), raw),
|
|
72
|
+
build: () => {
|
|
73
|
+
if (args[2] === 'snapshot') {
|
|
74
|
+
output(g.writeSnapshot(cwd), raw);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
output(g.graphifyBuild(cwd), raw);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
unknownMessage: (subcommand, available) => `Unknown graphify subcommand. Available: ${available.join(', ')}`,
|
|
82
|
+
error,
|
|
83
|
+
cwd,
|
|
84
|
+
raw,
|
|
85
|
+
});
|
|
69
86
|
}
|
|
70
87
|
module.exports = {
|
|
71
88
|
routeGraphifyCommand,
|
|
@@ -1724,10 +1724,14 @@ function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
|
|
|
1724
1724
|
}, raw);
|
|
1725
1725
|
return;
|
|
1726
1726
|
}
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
process.exit(
|
|
1727
|
+
// #1400: emit the raw block via the synchronous-flush output() helper (the same
|
|
1728
|
+
// one the --json branch uses) rather than process.stdout.write + process.exit(0).
|
|
1729
|
+
// When stdout is a pipe/file (how workflows consume this via command
|
|
1730
|
+
// substitution) the async stdout buffer is torn down by process.exit() before
|
|
1731
|
+
// it drains — on Windows this reliably truncates the write to 0 bytes, so every
|
|
1732
|
+
// ${AGENT_SKILLS_*} substitution expands empty. output() writes every byte with
|
|
1733
|
+
// writeAllSync and returns, letting the event loop drain naturally.
|
|
1734
|
+
output(block || '', true, block || '');
|
|
1731
1735
|
}
|
|
1732
1736
|
function buildSkillManifest(cwd, skillsDir = null) {
|
|
1733
1737
|
const canonicalRoots = skillsDir
|
|
@@ -523,9 +523,12 @@ function stageSkillsForRuntimeAsSkills(srcCommandsDir, resolvedProfile, converte
|
|
|
523
523
|
*
|
|
524
524
|
* @param srcAgentsDir source agents directory (e.g. agents/)
|
|
525
525
|
* @param resolvedProfile profile filter from resolveProfile()
|
|
526
|
-
* @param converter (content: string) → string
|
|
526
|
+
* @param converter (content: string, isGlobal?: boolean) → string per-file
|
|
527
|
+
* converter; scope-aware converters (copilot/antigravity)
|
|
528
|
+
* read isGlobal, single-arg converters ignore it (#1173)
|
|
529
|
+
* @param isGlobal install scope passed through to the converter
|
|
527
530
|
*/
|
|
528
|
-
function stageAgentsForRuntimeWithConverter(srcAgentsDir, resolvedProfile, converter) {
|
|
531
|
+
function stageAgentsForRuntimeWithConverter(srcAgentsDir, resolvedProfile, converter, isGlobal = false) {
|
|
529
532
|
if (!node_fs_1.default.existsSync(srcAgentsDir))
|
|
530
533
|
return srcAgentsDir;
|
|
531
534
|
const stageDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'gsd-profile-runtime-agents-'));
|
|
@@ -544,7 +547,7 @@ function stageAgentsForRuntimeWithConverter(srcAgentsDir, resolvedProfile, conve
|
|
|
544
547
|
}
|
|
545
548
|
}
|
|
546
549
|
const content = node_fs_1.default.readFileSync(node_path_1.default.join(srcAgentsDir, entry.name), 'utf8');
|
|
547
|
-
const converted = converter(content);
|
|
550
|
+
const converted = converter(content, isGlobal);
|
|
548
551
|
node_fs_1.default.writeFileSync(node_path_1.default.join(stageDir, entry.name), converted, 'utf8');
|
|
549
552
|
}
|
|
550
553
|
}
|