@hegemonart/get-design-done 1.31.0 → 1.31.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +44 -0
- package/NOTICE +224 -0
- package/README.md +1 -1
- package/agents/design-authority-watcher.md +1 -1
- package/agents/perf-analyzer.md +2 -2
- package/bin/gdd-mcp +78 -0
- package/bin/gdd-sdk +34 -24
- package/bin/gdd-state-mcp +78 -0
- package/{README.de.md → docs/i18n/README.de.md} +1 -1
- package/{README.fr.md → docs/i18n/README.fr.md} +1 -1
- package/{README.it.md → docs/i18n/README.it.md} +1 -1
- package/{README.ja.md → docs/i18n/README.ja.md} +1 -1
- package/{README.ko.md → docs/i18n/README.ko.md} +1 -1
- package/{README.zh-CN.md → docs/i18n/README.zh-CN.md} +1 -1
- package/hooks/_hook-emit.js +1 -1
- package/hooks/budget-enforcer.ts +5 -5
- package/hooks/context-exhaustion.ts +2 -2
- package/hooks/gdd-precompact-snapshot.js +3 -3
- package/hooks/gdd-read-injection-scanner.ts +2 -2
- package/hooks/gdd-sessionstart-recap.js +1 -1
- package/hooks/gdd-turn-closeout.js +1 -1
- package/package.json +20 -9
- package/recipes/.gitkeep +0 -0
- package/reference/schemas/recipe.schema.json +33 -0
- package/scripts/cli/gdd-events.mjs +5 -5
- package/scripts/lib/cache/gdd-cache-manager.cjs +1 -1
- package/scripts/lib/cli/index.ts +22 -160
- package/scripts/lib/connection-probe/index.cjs +1 -1
- package/scripts/lib/discuss-parallel-runner/aggregator.ts +1 -1
- package/scripts/lib/discuss-parallel-runner/index.ts +1 -1
- package/scripts/lib/error-classifier.cjs +24 -227
- package/scripts/lib/event-stream/index.ts +25 -193
- package/scripts/lib/gdd-errors/index.ts +24 -213
- package/scripts/lib/gdd-state/index.ts +23 -161
- package/scripts/lib/iteration-budget.cjs +23 -199
- package/scripts/lib/jittered-backoff.cjs +24 -107
- package/scripts/lib/lockfile.cjs +23 -195
- package/scripts/lib/logger/index.ts +1 -1
- package/scripts/lib/parallelism-engine/concurrency-tuner.cjs +1 -1
- package/scripts/lib/perf-analyzer/index.cjs +1 -1
- package/scripts/lib/pipeline-runner/index.ts +4 -4
- package/scripts/lib/pipeline-runner/state-machine.ts +1 -1
- package/scripts/lib/prompt-dedup/index.cjs +1 -1
- package/scripts/lib/rate-guard.cjs +2 -2
- package/scripts/lib/recipe-loader.cjs +142 -0
- package/scripts/lib/session-runner/errors.ts +3 -3
- package/scripts/lib/session-runner/index.ts +3 -3
- package/scripts/lib/session-runner/transcript.ts +1 -1
- package/scripts/lib/tool-scoping/index.ts +1 -1
- package/scripts/mcp-servers/gdd-mcp/server.ts +29 -311
- package/scripts/mcp-servers/gdd-state/server.ts +28 -282
- package/sdk/README.md +45 -0
- package/{scripts/lib → sdk}/cli/commands/audit.ts +3 -3
- package/{scripts/lib → sdk}/cli/commands/init.ts +3 -3
- package/{scripts/lib → sdk}/cli/commands/query.ts +4 -4
- package/{scripts/lib → sdk}/cli/commands/run.ts +5 -5
- package/{scripts/lib → sdk}/cli/commands/stage.ts +5 -5
- package/sdk/cli/index.js +8091 -0
- package/sdk/cli/index.ts +172 -0
- package/{scripts/lib → sdk}/cli/parse-args.ts +2 -2
- package/{scripts/lib/gdd-errors → sdk/errors}/classification.ts +1 -1
- package/sdk/errors/index.ts +218 -0
- package/{scripts/lib → sdk}/event-stream/emitter.ts +1 -1
- package/sdk/event-stream/index.ts +197 -0
- package/{scripts/lib → sdk}/event-stream/reader.ts +1 -1
- package/{scripts/lib → sdk}/event-stream/types.ts +2 -2
- package/{scripts/lib → sdk}/event-stream/writer.ts +1 -1
- package/sdk/index.ts +19 -0
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/README.md +3 -3
- package/sdk/mcp/gdd-mcp/server.js +1924 -0
- package/sdk/mcp/gdd-mcp/server.ts +325 -0
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_cycle_recap.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_decisions_list.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_events_tail.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_health.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_intel_get.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_learnings_digest.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_phase_current.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_phases_list.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_plans_list.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_reflections_latest.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_status.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/gdd_telemetry_query.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/index.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/tools/shared.ts +3 -3
- package/sdk/mcp/gdd-state/server.js +2790 -0
- package/sdk/mcp/gdd-state/server.ts +294 -0
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/add_blocker.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/add_decision.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/add_must_have.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/checkpoint.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/frontmatter_update.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/get.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/index.ts +1 -1
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/probe_connections.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/resolve_blocker.ts +3 -3
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/set_status.ts +2 -2
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/shared.ts +8 -8
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/transition_stage.ts +4 -4
- package/{scripts/mcp-servers → sdk/mcp}/gdd-state/tools/update_progress.ts +2 -2
- package/sdk/primitives/error-classifier.cjs +232 -0
- package/sdk/primitives/iteration-budget.cjs +205 -0
- package/sdk/primitives/jittered-backoff.cjs +112 -0
- package/sdk/primitives/lockfile.cjs +201 -0
- package/{scripts/lib/gdd-state → sdk/state}/gates.ts +1 -1
- package/sdk/state/index.ts +167 -0
- package/{scripts/lib/gdd-state → sdk/state}/lockfile.ts +1 -1
- package/{scripts/lib/gdd-state → sdk/state}/mutator.ts +1 -1
- package/{scripts/lib/gdd-state → sdk/state}/parser.ts +1 -1
- package/{scripts/lib/gdd-state → sdk/state}/types.ts +4 -4
- package/skills/quality-gate/SKILL.md +2 -2
- package/scripts/aggregate-agent-metrics.ts +0 -282
- package/scripts/bootstrap-manifest.txt +0 -3
- package/scripts/bootstrap.sh +0 -80
- package/scripts/build-distribution-bundles.cjs +0 -549
- package/scripts/build-intel.cjs +0 -486
- package/scripts/codegen-schema-types.ts +0 -149
- package/scripts/detect-stale-refs.cjs +0 -107
- package/scripts/e2e/run-headless.ts +0 -514
- package/scripts/extract-changelog-section.cjs +0 -58
- package/scripts/gsd-cleanup-incubator.cjs +0 -367
- package/scripts/injection-patterns.cjs +0 -58
- package/scripts/lint-agentskills-spec.cjs +0 -457
- package/scripts/release-smoke-test.cjs +0 -200
- package/scripts/rollback-release.sh +0 -42
- package/scripts/run-injection-scanner-ci.cjs +0 -83
- package/scripts/tests/test-authority-rejected-kinds.sh +0 -58
- package/scripts/tests/test-authority-watcher-diff.sh +0 -113
- package/scripts/tests/test-motion-provenance.sh +0 -64
- package/scripts/validate-frontmatter.ts +0 -409
- package/scripts/validate-incubator-scope.cjs +0 -133
- package/scripts/validate-schemas.ts +0 -401
- package/scripts/validate-skill-length.cjs +0 -283
- package/scripts/verify-version-sync.cjs +0 -30
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_cycle_recap.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_decisions_list.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_events_tail.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_health.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_intel_get.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_learnings_digest.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_phase_current.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_phases_list.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_plans_list.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_reflections_latest.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_status.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-mcp/schemas/gdd_telemetry_query.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/add_blocker.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/add_decision.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/add_must_have.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/checkpoint.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/frontmatter_update.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/get.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/probe_connections.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/resolve_blocker.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/set_status.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/transition_stage.schema.json +0 -0
- /package/{scripts/mcp-servers → sdk/mcp}/gdd-state/schemas/update_progress.schema.json +0 -0
- /package/{scripts/lib → sdk/primitives}/error-classifier.d.cts +0 -0
- /package/{scripts/lib → sdk/primitives}/iteration-budget.d.cts +0 -0
- /package/{scripts/lib → sdk/primitives}/jittered-backoff.d.cts +0 -0
- /package/{scripts/lib → sdk/primitives}/lockfile.d.cts +0 -0
|
@@ -1,457 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
/**
|
|
4
|
-
* scripts/lint-agentskills-spec.cjs — agentskills.io spec lint.
|
|
5
|
-
*
|
|
6
|
-
* Phase 28.8 Plan 28-8-A1 (D-13 `lint-only` outcome).
|
|
7
|
-
* See .planning/research/agentskills-io-2026-05-19.md § Implementation Implications
|
|
8
|
-
* → Plan 28-8-A1 — what to ship — for the source-of-truth rule list.
|
|
9
|
-
*
|
|
10
|
-
* Walks `skills/<name>/SKILL.md` and applies the following rules per skill:
|
|
11
|
-
*
|
|
12
|
-
* R1 (FAIL) — frontmatter contains a non-empty `name`.
|
|
13
|
-
* R2 (FAIL) — `name` matches /^[a-z0-9]+(-[a-z0-9]+)*$/ AND length ≤ 64.
|
|
14
|
-
* R3 (FAIL) — `name` matches the parent directory (allow bare slug OR `gdd-`-prefixed
|
|
15
|
-
* slug, because source-tree uses bare and install-tree uses prefixed per
|
|
16
|
-
* Phase 28.7 D-05).
|
|
17
|
-
* R4 (FAIL) — `description` is non-empty AND ≤ 1024 chars (spec hard cap).
|
|
18
|
-
* R5 (FAIL) — SKILL.md body line count ≤ 500 (spec readability guidance).
|
|
19
|
-
*
|
|
20
|
-
* W1 (WARN) — both `tools` and `allowed-tools` are present. `allowed-tools` is marked
|
|
21
|
-
* Experimental in the spec; pick one form to avoid drift.
|
|
22
|
-
* W2 (WARN) — description length > 200 chars (Phase 28.5 D-01 advisory; distinct
|
|
23
|
-
* from R4's 1024-char hard cap).
|
|
24
|
-
* W3 — reserved slot (covered today by R2). No emission.
|
|
25
|
-
*
|
|
26
|
-
* CLI:
|
|
27
|
-
* node scripts/lint-agentskills-spec.cjs # default: lint ./skills
|
|
28
|
-
* node scripts/lint-agentskills-spec.cjs <dir> # lint <dir>/<name>/SKILL.md
|
|
29
|
-
* node scripts/lint-agentskills-spec.cjs --json # emit JSON instead of table
|
|
30
|
-
* node scripts/lint-agentskills-spec.cjs --summary # one-line PASS/WARN/FAIL counts
|
|
31
|
-
* node scripts/lint-agentskills-spec.cjs --summary --json
|
|
32
|
-
* # JSON {pass,warn,fail} counts
|
|
33
|
-
*
|
|
34
|
-
* Exit codes:
|
|
35
|
-
* 0 — no FAIL rows (WARN rows do NOT fail the run)
|
|
36
|
-
* 1 — at least one FAIL row
|
|
37
|
-
* 2 — internal error (I/O failure, parse exception, bad CLI arg)
|
|
38
|
-
*
|
|
39
|
-
* Empty / missing skills directory:
|
|
40
|
-
* Prints `Lint: no skills found at <dir> — nothing to lint.` and exits 0.
|
|
41
|
-
* Under `--summary`, prints `PASS=0 WARN=0 FAIL=0` (or `{"pass":0,"warn":0,"fail":0}`
|
|
42
|
-
* with `--summary --json`) and exits 0 — empty dirs never fail the run.
|
|
43
|
-
*
|
|
44
|
-
* Exports (for tests + Plan 28-8-X2 in-process consumption):
|
|
45
|
-
* lint(skillsDir, opts?) → { rows, summary, emptyDir }
|
|
46
|
-
* lintSummary({sourceRoot}) → { pass, warn, fail } // Plan 28-8-X2 doctor seam
|
|
47
|
-
* main(argv) → number (exit code; pure — does NOT call process.exit)
|
|
48
|
-
* parseFrontmatter(content) → { frontmatter, body, hasFrontmatter }
|
|
49
|
-
* lintSkill(skillDir, skillName) → Array<{status, skill, rule, detail}>
|
|
50
|
-
*
|
|
51
|
-
* Plan 28-8-X2 wiring:
|
|
52
|
-
* `lintSummary` is consumed in-process by `scripts/lib/install/doctor-tier2.cjs`
|
|
53
|
-
* (Tier-2 doctor aggregator). It returns `{pass, warn, fail}` counts only — no
|
|
54
|
-
* table, no JSON, no IO beyond fs.readFileSync of SKILL.md files. The doctor wraps
|
|
55
|
-
* it as the agentskills.io channel state per D-13 (lint-only adoption).
|
|
56
|
-
*/
|
|
57
|
-
|
|
58
|
-
const fs = require('fs');
|
|
59
|
-
const path = require('path');
|
|
60
|
-
|
|
61
|
-
const NAME_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
62
|
-
const NAME_MAX = 64;
|
|
63
|
-
const DESC_MAX = 1024;
|
|
64
|
-
const DESC_ADVISORY = 200;
|
|
65
|
-
const BODY_MAX_LINES = 500;
|
|
66
|
-
const DETAIL_TRUNCATE = 100;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Parse YAML-ish frontmatter at the top of a markdown document.
|
|
70
|
-
*
|
|
71
|
-
* Zero-dep: handles only what our Phase 28.5 frontmatter contract emits:
|
|
72
|
-
* - leading `---\n` block delimited by `\n---\n`
|
|
73
|
-
* - scalar `key: value` lines (no nested maps, no arrays)
|
|
74
|
-
* - surrounding single or double quotes stripped from value
|
|
75
|
-
* - for values containing colons (URLs in description), the substring after the FIRST `:`
|
|
76
|
-
* is taken — so `description: "https://example.com"` works.
|
|
77
|
-
*
|
|
78
|
-
* Returns:
|
|
79
|
-
* { frontmatter: object, body: string, hasFrontmatter: boolean }
|
|
80
|
-
*
|
|
81
|
-
* If the opening delimiter is missing or the closing delimiter is not found, returns
|
|
82
|
-
* `hasFrontmatter: false` with `frontmatter: {}` and `body` set to the original content.
|
|
83
|
-
*/
|
|
84
|
-
function parseFrontmatter(content) {
|
|
85
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
86
|
-
if (!match) {
|
|
87
|
-
return { frontmatter: {}, body: content, hasFrontmatter: false };
|
|
88
|
-
}
|
|
89
|
-
const block = match[1];
|
|
90
|
-
const body = match[2];
|
|
91
|
-
const frontmatter = {};
|
|
92
|
-
const lines = block.split(/\r?\n/);
|
|
93
|
-
for (const line of lines) {
|
|
94
|
-
if (!line.trim()) continue;
|
|
95
|
-
const colonIdx = line.indexOf(':');
|
|
96
|
-
if (colonIdx < 0) continue;
|
|
97
|
-
const key = line.slice(0, colonIdx).trim();
|
|
98
|
-
let value = line.slice(colonIdx + 1).trim();
|
|
99
|
-
if (
|
|
100
|
-
value.length >= 2 &&
|
|
101
|
-
((value.startsWith('"') && value.endsWith('"')) ||
|
|
102
|
-
(value.startsWith("'") && value.endsWith("'")))
|
|
103
|
-
) {
|
|
104
|
-
value = value.slice(1, -1);
|
|
105
|
-
}
|
|
106
|
-
frontmatter[key] = value;
|
|
107
|
-
}
|
|
108
|
-
return { frontmatter, body, hasFrontmatter: true };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Lint a single skill directory. Returns one or more rows.
|
|
113
|
-
*
|
|
114
|
-
* Row shape: { status: 'PASS'|'WARN'|'FAIL', skill: string, rule: string, detail: string }
|
|
115
|
-
*
|
|
116
|
-
* If no rule fires, returns a single PASS row with rule='-' and detail='-'.
|
|
117
|
-
*/
|
|
118
|
-
function lintSkill(skillDir, skillName) {
|
|
119
|
-
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
120
|
-
let content;
|
|
121
|
-
try {
|
|
122
|
-
content = fs.readFileSync(skillPath, 'utf8');
|
|
123
|
-
} catch (err) {
|
|
124
|
-
if (err && err.code === 'ENOENT') {
|
|
125
|
-
return [
|
|
126
|
-
{
|
|
127
|
-
status: 'FAIL',
|
|
128
|
-
skill: skillName,
|
|
129
|
-
rule: 'IO',
|
|
130
|
-
detail: 'SKILL.md not found',
|
|
131
|
-
},
|
|
132
|
-
];
|
|
133
|
-
}
|
|
134
|
-
throw err;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const { frontmatter, body } = parseFrontmatter(content);
|
|
138
|
-
const rows = [];
|
|
139
|
-
|
|
140
|
-
const name = (frontmatter.name || '').trim();
|
|
141
|
-
const description = (frontmatter.description || '').trim();
|
|
142
|
-
const hasTools = Object.prototype.hasOwnProperty.call(frontmatter, 'tools');
|
|
143
|
-
const hasAllowedTools = Object.prototype.hasOwnProperty.call(frontmatter, 'allowed-tools');
|
|
144
|
-
|
|
145
|
-
// R1
|
|
146
|
-
if (!name) {
|
|
147
|
-
rows.push({
|
|
148
|
-
status: 'FAIL',
|
|
149
|
-
skill: skillName,
|
|
150
|
-
rule: 'R1',
|
|
151
|
-
detail: 'frontmatter missing or empty `name`',
|
|
152
|
-
});
|
|
153
|
-
} else {
|
|
154
|
-
// R2
|
|
155
|
-
if (!NAME_REGEX.test(name) || name.length > NAME_MAX) {
|
|
156
|
-
rows.push({
|
|
157
|
-
status: 'FAIL',
|
|
158
|
-
skill: skillName,
|
|
159
|
-
rule: 'R2',
|
|
160
|
-
detail: `name "${name}" fails slug regex /^[a-z0-9]+(-[a-z0-9]+)*$/ or exceeds ${NAME_MAX} chars`,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
// R3 — name must match parent dir (bare or gdd-prefixed)
|
|
164
|
-
if (
|
|
165
|
-
name !== skillName &&
|
|
166
|
-
name !== `gdd-${skillName}` &&
|
|
167
|
-
skillName !== `gdd-${name}`
|
|
168
|
-
) {
|
|
169
|
-
rows.push({
|
|
170
|
-
status: 'FAIL',
|
|
171
|
-
skill: skillName,
|
|
172
|
-
rule: 'R3',
|
|
173
|
-
detail: `name "${name}" does not match parent dir "${skillName}" (allowed: bare slug or gdd-prefixed slug per Phase 28.7 D-05)`,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// R4
|
|
179
|
-
if (!description) {
|
|
180
|
-
rows.push({
|
|
181
|
-
status: 'FAIL',
|
|
182
|
-
skill: skillName,
|
|
183
|
-
rule: 'R4',
|
|
184
|
-
detail: 'description missing or empty',
|
|
185
|
-
});
|
|
186
|
-
} else if (description.length > DESC_MAX) {
|
|
187
|
-
rows.push({
|
|
188
|
-
status: 'FAIL',
|
|
189
|
-
skill: skillName,
|
|
190
|
-
rule: 'R4',
|
|
191
|
-
detail: `description: ${description.length} chars (>${DESC_MAX} hard cap)`,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// R5 — body line count
|
|
196
|
-
const bodyLines = body ? body.split(/\r?\n/).length : 0;
|
|
197
|
-
if (bodyLines > BODY_MAX_LINES) {
|
|
198
|
-
rows.push({
|
|
199
|
-
status: 'FAIL',
|
|
200
|
-
skill: skillName,
|
|
201
|
-
rule: 'R5',
|
|
202
|
-
detail: `body ${bodyLines} lines (>${BODY_MAX_LINES} spec guidance)`,
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// W1 — both tools and allowed-tools present
|
|
207
|
-
if (hasTools && hasAllowedTools) {
|
|
208
|
-
rows.push({
|
|
209
|
-
status: 'WARN',
|
|
210
|
-
skill: skillName,
|
|
211
|
-
rule: 'W1',
|
|
212
|
-
detail: '`tools` and `allowed-tools` both present; spec marks `allowed-tools` Experimental — pick one to avoid drift',
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// W2 — description over advisory cap but under hard cap
|
|
217
|
-
if (
|
|
218
|
-
description &&
|
|
219
|
-
description.length > DESC_ADVISORY &&
|
|
220
|
-
description.length <= DESC_MAX
|
|
221
|
-
) {
|
|
222
|
-
rows.push({
|
|
223
|
-
status: 'WARN',
|
|
224
|
-
skill: skillName,
|
|
225
|
-
rule: 'W2',
|
|
226
|
-
detail: `description: ${description.length} chars (>${DESC_ADVISORY} advisory cap, Phase 28.5 D-01)`,
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// W3 — reserved (covered by R2).
|
|
231
|
-
|
|
232
|
-
if (rows.length === 0) {
|
|
233
|
-
rows.push({
|
|
234
|
-
status: 'PASS',
|
|
235
|
-
skill: skillName,
|
|
236
|
-
rule: '-',
|
|
237
|
-
detail: '-',
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
return rows;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Walk a skills directory and lint each child subdirectory containing SKILL.md.
|
|
245
|
-
*
|
|
246
|
-
* Returns:
|
|
247
|
-
* { rows: Array, summary: {total, pass, warn, fail}, emptyDir: boolean }
|
|
248
|
-
*/
|
|
249
|
-
function lint(skillsDir, opts) {
|
|
250
|
-
const _opts = opts || {};
|
|
251
|
-
if (!fs.existsSync(skillsDir)) {
|
|
252
|
-
return {
|
|
253
|
-
rows: [],
|
|
254
|
-
summary: { total: 0, pass: 0, warn: 0, fail: 0 },
|
|
255
|
-
emptyDir: true,
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
let stat;
|
|
259
|
-
try {
|
|
260
|
-
stat = fs.statSync(skillsDir);
|
|
261
|
-
} catch (err) {
|
|
262
|
-
return {
|
|
263
|
-
rows: [],
|
|
264
|
-
summary: { total: 0, pass: 0, warn: 0, fail: 0 },
|
|
265
|
-
emptyDir: true,
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
if (!stat.isDirectory()) {
|
|
269
|
-
return {
|
|
270
|
-
rows: [],
|
|
271
|
-
summary: { total: 0, pass: 0, warn: 0, fail: 0 },
|
|
272
|
-
emptyDir: true,
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
277
|
-
const skillDirs = entries
|
|
278
|
-
.filter((e) => e.isDirectory())
|
|
279
|
-
.map((e) => e.name)
|
|
280
|
-
.filter((name) => fs.existsSync(path.join(skillsDir, name, 'SKILL.md')))
|
|
281
|
-
.sort();
|
|
282
|
-
|
|
283
|
-
if (skillDirs.length === 0) {
|
|
284
|
-
return {
|
|
285
|
-
rows: [],
|
|
286
|
-
summary: { total: 0, pass: 0, warn: 0, fail: 0 },
|
|
287
|
-
emptyDir: true,
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const rows = [];
|
|
292
|
-
for (const skillName of skillDirs) {
|
|
293
|
-
const skillDir = path.join(skillsDir, skillName);
|
|
294
|
-
const skillRows = lintSkill(skillDir, skillName);
|
|
295
|
-
for (const row of skillRows) rows.push(row);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const summary = {
|
|
299
|
-
total: skillDirs.length,
|
|
300
|
-
pass: rows.filter((r) => r.status === 'PASS').length,
|
|
301
|
-
warn: rows.filter((r) => r.status === 'WARN').length,
|
|
302
|
-
fail: rows.filter((r) => r.status === 'FAIL').length,
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
return { rows, summary, emptyDir: false };
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Plan 28-8-X2 seam — return only the PASS/WARN/FAIL counts as a flat object.
|
|
310
|
-
*
|
|
311
|
-
* Consumed in-process by `scripts/lib/install/doctor-tier2.cjs` (Tier-2 doctor
|
|
312
|
-
* aggregator). Wraps `lint()` and projects its `summary` onto a 3-field shape
|
|
313
|
-
* matching the X2 interface contract: `{ pass, warn, fail }`. Empty dirs
|
|
314
|
-
* yield `{ pass: 0, warn: 0, fail: 0 }`. The `total` field is dropped — callers
|
|
315
|
-
* compute it from `pass + warn + fail` if needed, matching D-13 lint-only contract.
|
|
316
|
-
*
|
|
317
|
-
* Pure: no IO beyond what `lint()` already does. Never calls process.exit.
|
|
318
|
-
*
|
|
319
|
-
* @param {{ sourceRoot?: string }} [opts] Optional. `sourceRoot` is the directory
|
|
320
|
-
* containing `skills/` (NOT the skills/
|
|
321
|
-
* directory itself — matches Phase 28.7
|
|
322
|
-
* `findInstallSourceRoot()` return contract).
|
|
323
|
-
* Defaults to `process.cwd()` when omitted.
|
|
324
|
-
* @returns {{ pass: number, warn: number, fail: number }}
|
|
325
|
-
*/
|
|
326
|
-
function lintSummary(opts) {
|
|
327
|
-
const _opts = opts || {};
|
|
328
|
-
const sourceRoot = _opts.sourceRoot || process.cwd();
|
|
329
|
-
const skillsDir = path.join(sourceRoot, 'skills');
|
|
330
|
-
const result = lint(skillsDir);
|
|
331
|
-
return {
|
|
332
|
-
pass: result.summary.pass,
|
|
333
|
-
warn: result.summary.warn,
|
|
334
|
-
fail: result.summary.fail,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Format the row set as an aligned plain-text table.
|
|
340
|
-
*
|
|
341
|
-
* Columns: STATUS SKILL RULE DETAIL
|
|
342
|
-
* DETAIL is truncated to DETAIL_TRUNCATE chars with a `…` suffix for terminal sanity.
|
|
343
|
-
*/
|
|
344
|
-
function formatTable(rows) {
|
|
345
|
-
const headers = ['STATUS', 'SKILL', 'RULE', 'DETAIL'];
|
|
346
|
-
const display = rows.map((r) => {
|
|
347
|
-
let detail = String(r.detail);
|
|
348
|
-
if (detail.length > DETAIL_TRUNCATE) {
|
|
349
|
-
detail = detail.slice(0, DETAIL_TRUNCATE) + '…';
|
|
350
|
-
}
|
|
351
|
-
return [r.status, r.skill, r.rule, detail];
|
|
352
|
-
});
|
|
353
|
-
const widths = headers.map((h, i) =>
|
|
354
|
-
Math.max(h.length, ...display.map((row) => row[i].length))
|
|
355
|
-
);
|
|
356
|
-
const pad = (cells) =>
|
|
357
|
-
cells.map((c, i) => c.padEnd(widths[i])).join(' ');
|
|
358
|
-
const sep = widths.map((w) => '-'.repeat(w)).join(' ');
|
|
359
|
-
const out = [pad(headers), sep];
|
|
360
|
-
for (const row of display) out.push(pad(row));
|
|
361
|
-
return out.join('\n');
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Main CLI entry. Pure — returns exit code rather than calling process.exit.
|
|
366
|
-
*
|
|
367
|
-
* argv is the argv slice AFTER node + script (i.e. process.argv.slice(2)).
|
|
368
|
-
*/
|
|
369
|
-
function main(argv) {
|
|
370
|
-
try {
|
|
371
|
-
let skillsDir = './skills';
|
|
372
|
-
let jsonMode = false;
|
|
373
|
-
let summaryMode = false;
|
|
374
|
-
for (const arg of argv) {
|
|
375
|
-
if (arg === '--json') {
|
|
376
|
-
jsonMode = true;
|
|
377
|
-
} else if (arg === '--summary') {
|
|
378
|
-
summaryMode = true;
|
|
379
|
-
} else if (arg === '--help' || arg === '-h') {
|
|
380
|
-
process.stdout.write(
|
|
381
|
-
'lint-agentskills-spec.cjs — agentskills.io spec lint over skills/<name>/SKILL.md\n' +
|
|
382
|
-
'\n' +
|
|
383
|
-
'Usage:\n' +
|
|
384
|
-
' node scripts/lint-agentskills-spec.cjs [<dir>] [--json]\n' +
|
|
385
|
-
' node scripts/lint-agentskills-spec.cjs [<dir>] --summary [--json]\n' +
|
|
386
|
-
'\n' +
|
|
387
|
-
'Modes:\n' +
|
|
388
|
-
' default Aligned table of all rows + final summary line\n' +
|
|
389
|
-
' --json JSON {rows, summary} object\n' +
|
|
390
|
-
' --summary One-line `PASS=N WARN=N FAIL=N`\n' +
|
|
391
|
-
' --summary --json JSON {pass, warn, fail} (Plan 28-8-X2 seam)\n' +
|
|
392
|
-
'\n' +
|
|
393
|
-
'Exit codes:\n' +
|
|
394
|
-
' 0 no FAIL rows (WARN rows do NOT fail the run)\n' +
|
|
395
|
-
' 1 at least one FAIL row\n' +
|
|
396
|
-
' 2 internal error\n'
|
|
397
|
-
);
|
|
398
|
-
return 0;
|
|
399
|
-
} else if (arg.startsWith('--')) {
|
|
400
|
-
process.stderr.write(`lint-agentskills-spec: unknown flag: ${arg}\n`);
|
|
401
|
-
return 2;
|
|
402
|
-
} else {
|
|
403
|
-
skillsDir = arg;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const result = lint(skillsDir);
|
|
408
|
-
|
|
409
|
-
// Plan 28-8-X2 — --summary mode short-circuits the table renderer for
|
|
410
|
-
// doctor-tier2 callers. Empty dir is treated as 0-everything (exits 0)
|
|
411
|
-
// matching the default-mode "no skills found" exit-0 contract.
|
|
412
|
-
if (summaryMode) {
|
|
413
|
-
const pass = result.summary.pass || 0;
|
|
414
|
-
const warn = result.summary.warn || 0;
|
|
415
|
-
const fail = result.summary.fail || 0;
|
|
416
|
-
if (jsonMode) {
|
|
417
|
-
process.stdout.write(JSON.stringify({ pass, warn, fail }) + '\n');
|
|
418
|
-
} else {
|
|
419
|
-
process.stdout.write(`PASS=${pass} WARN=${warn} FAIL=${fail}\n`);
|
|
420
|
-
}
|
|
421
|
-
return fail > 0 ? 1 : 0;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (result.emptyDir) {
|
|
425
|
-
process.stdout.write(
|
|
426
|
-
`Lint: no skills found at ${skillsDir} — nothing to lint.\n`
|
|
427
|
-
);
|
|
428
|
-
return 0;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (jsonMode) {
|
|
432
|
-
process.stdout.write(
|
|
433
|
-
JSON.stringify({ rows: result.rows, summary: result.summary }, null, 2) +
|
|
434
|
-
'\n'
|
|
435
|
-
);
|
|
436
|
-
} else {
|
|
437
|
-
process.stdout.write(formatTable(result.rows) + '\n');
|
|
438
|
-
const { total, pass, warn, fail } = result.summary;
|
|
439
|
-
process.stdout.write(
|
|
440
|
-
`\nLint summary: ${total} skills, ${pass} PASS, ${warn} WARN, ${fail} FAIL\n`
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
return result.summary.fail > 0 ? 1 : 0;
|
|
445
|
-
} catch (err) {
|
|
446
|
-
process.stderr.write(
|
|
447
|
-
`lint-agentskills-spec: internal error: ${err && err.message ? err.message : err}\n`
|
|
448
|
-
);
|
|
449
|
-
return 2;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
if (require.main === module) {
|
|
454
|
-
process.exit(main(process.argv.slice(2)));
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
module.exports = { lint, lintSummary, main, parseFrontmatter, lintSkill };
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
// release-smoke-test.cjs — release-time smoke test per D-22.
|
|
4
|
-
//
|
|
5
|
-
// Validates that the freshly-checked-out tag produces a consistent plugin
|
|
6
|
-
// surface. Runs deterministic (non-LLM) portions of /gdd:explore against
|
|
7
|
-
// test-fixture/src/ in an isolated temp dir and diffs resulting artifacts
|
|
8
|
-
// against the provided baseline directory. Exit code:
|
|
9
|
-
// 0 — zero diffs, zero missing artifacts
|
|
10
|
-
// 1 — one or more diffs or missing artifacts
|
|
11
|
-
// 2 — baseline not found / argument error
|
|
12
|
-
//
|
|
13
|
-
// Does NOT invoke the `claude` CLI (unavailable on stock GitHub runners).
|
|
14
|
-
// The LLM-dependent portions of /gdd:explore are intentionally out of scope;
|
|
15
|
-
// this smoke test covers the intel builder + static analysis surface.
|
|
16
|
-
//
|
|
17
|
-
// Usage:
|
|
18
|
-
// node scripts/release-smoke-test.cjs --baseline <path>
|
|
19
|
-
// node scripts/release-smoke-test.cjs --baseline test-fixture/baselines/phase-13
|
|
20
|
-
// node scripts/release-smoke-test.cjs --baseline <path> --keep # keep tmp dir
|
|
21
|
-
|
|
22
|
-
const fs = require('fs');
|
|
23
|
-
const path = require('path');
|
|
24
|
-
const os = require('os');
|
|
25
|
-
const { spawnSync } = require('child_process');
|
|
26
|
-
|
|
27
|
-
const args = process.argv.slice(2);
|
|
28
|
-
const baselineIdx = args.indexOf('--baseline');
|
|
29
|
-
const KEEP = args.includes('--keep');
|
|
30
|
-
|
|
31
|
-
if (baselineIdx < 0 || !args[baselineIdx + 1]) {
|
|
32
|
-
console.error('Usage: node scripts/release-smoke-test.cjs --baseline <path> [--keep]');
|
|
33
|
-
process.exit(2);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const baselineDir = path.resolve(args[baselineIdx + 1]);
|
|
37
|
-
if (!fs.existsSync(baselineDir) || !fs.statSync(baselineDir).isDirectory()) {
|
|
38
|
-
console.error(`baseline not found: ${baselineDir}`);
|
|
39
|
-
process.exit(2);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
43
|
-
const FIXTURE_SRC = path.join(REPO_ROOT, 'test-fixture', 'src');
|
|
44
|
-
|
|
45
|
-
if (!fs.existsSync(FIXTURE_SRC)) {
|
|
46
|
-
console.error(`fixture not found: ${FIXTURE_SRC}`);
|
|
47
|
-
process.exit(2);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const tmpDir = path.join(os.tmpdir(), `gdd-smoke-${Date.now()}`);
|
|
51
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
52
|
-
|
|
53
|
-
// Snapshot REPO_ROOT/.design/ contents BEFORE the smoke test runs. This lets
|
|
54
|
-
// the post-test pollution assertion (below) detect actual pollution (new
|
|
55
|
-
// files created during the run) rather than tripping on legitimately-tracked
|
|
56
|
-
// `.design/` files that exist in a fresh checkout — e.g.
|
|
57
|
-
// `.design/config.example.json` shipped by Plan 29-02 for discoverability.
|
|
58
|
-
function snapshotDesignDir() {
|
|
59
|
-
const designDir = path.join(REPO_ROOT, '.design');
|
|
60
|
-
if (!fs.existsSync(designDir)) return '<absent>';
|
|
61
|
-
const entries = [];
|
|
62
|
-
function walk(d) {
|
|
63
|
-
for (const ent of fs.readdirSync(d, { withFileTypes: true })) {
|
|
64
|
-
const full = path.join(d, ent.name);
|
|
65
|
-
if (ent.isDirectory()) walk(full);
|
|
66
|
-
else entries.push(path.relative(designDir, full).replace(/\\/g, '/'));
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
walk(designDir);
|
|
70
|
-
return entries.sort().join('\n');
|
|
71
|
-
}
|
|
72
|
-
const designSnapshotBefore = snapshotDesignDir();
|
|
73
|
-
|
|
74
|
-
function copyRecursive(src, dst) {
|
|
75
|
-
fs.mkdirSync(dst, { recursive: true });
|
|
76
|
-
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
77
|
-
const s = path.join(src, entry.name);
|
|
78
|
-
const d = path.join(dst, entry.name);
|
|
79
|
-
if (entry.isDirectory()) copyRecursive(s, d);
|
|
80
|
-
else fs.copyFileSync(s, d);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function cleanup() {
|
|
85
|
-
if (KEEP) {
|
|
86
|
-
console.log(`(kept) ${tmpDir}`);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
try {
|
|
90
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
91
|
-
} catch (_) {
|
|
92
|
-
/* best effort */
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
process.on('exit', cleanup);
|
|
97
|
-
process.on('SIGINT', () => {
|
|
98
|
-
cleanup();
|
|
99
|
-
process.exit(130);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// ── Step 1: Copy fixture into isolated temp dir.
|
|
103
|
-
const workDir = path.join(tmpDir, 'work');
|
|
104
|
-
copyRecursive(FIXTURE_SRC, path.join(workDir, 'src'));
|
|
105
|
-
|
|
106
|
-
// ── Step 2: Run deterministic intel builder against the temp dir.
|
|
107
|
-
// build-intel.cjs uses process.cwd(); invoke it with cwd=workDir.
|
|
108
|
-
const intelResult = spawnSync(
|
|
109
|
-
'node',
|
|
110
|
-
[path.join(REPO_ROOT, 'scripts', 'build-intel.cjs')],
|
|
111
|
-
{ cwd: workDir, encoding: 'utf8' }
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
// Non-zero intel run is OK (fixture may lack some scan inputs) — log but don't fail.
|
|
115
|
-
if (intelResult.status !== 0) {
|
|
116
|
-
console.log(`(note) build-intel exited ${intelResult.status} on fixture — continuing to baseline diff`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ── Step 3: Compare baseline files against the fresh run.
|
|
120
|
-
const diffs = [];
|
|
121
|
-
const missing = [];
|
|
122
|
-
|
|
123
|
-
function walk(dir, base, out) {
|
|
124
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
125
|
-
const full = path.join(dir, entry.name);
|
|
126
|
-
const rel = path.relative(base, full);
|
|
127
|
-
if (entry.isDirectory()) walk(full, base, out);
|
|
128
|
-
else out.push(rel);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const baselineFiles = [];
|
|
133
|
-
walk(baselineDir, baselineDir, baselineFiles);
|
|
134
|
-
|
|
135
|
-
// Artifact resolution: for each baseline file, look for a sibling file in the
|
|
136
|
-
// fresh run workDir (best-effort: baseline may contain reference manifests
|
|
137
|
-
// that aren't produced by the deterministic pipeline — those count as
|
|
138
|
-
// "present in baseline, not in run" and are recorded as missing_artifacts but
|
|
139
|
-
// not as diffs).
|
|
140
|
-
for (const rel of baselineFiles) {
|
|
141
|
-
const baselineFile = path.join(baselineDir, rel);
|
|
142
|
-
const freshCandidate1 = path.join(workDir, rel);
|
|
143
|
-
const freshCandidate2 = path.join(workDir, '.design', 'intel', path.basename(rel));
|
|
144
|
-
|
|
145
|
-
let freshFile = null;
|
|
146
|
-
if (fs.existsSync(freshCandidate1)) freshFile = freshCandidate1;
|
|
147
|
-
else if (fs.existsSync(freshCandidate2)) freshFile = freshCandidate2;
|
|
148
|
-
|
|
149
|
-
if (!freshFile) {
|
|
150
|
-
missing.push(rel);
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const expected = fs.readFileSync(baselineFile, 'utf8').replace(/\r\n/g, '\n');
|
|
155
|
-
const actual = fs.readFileSync(freshFile, 'utf8').replace(/\r\n/g, '\n');
|
|
156
|
-
|
|
157
|
-
if (expected !== actual) {
|
|
158
|
-
diffs.push({
|
|
159
|
-
rel,
|
|
160
|
-
expected: expected.slice(0, 80),
|
|
161
|
-
actual: actual.slice(0, 80),
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ── Report.
|
|
167
|
-
for (const d of diffs) {
|
|
168
|
-
console.log(`DIFF: ${d.rel}`);
|
|
169
|
-
console.log(` Expected: ${d.expected.replace(/\n/g, '\\n')}`);
|
|
170
|
-
console.log(` Actual: ${d.actual.replace(/\n/g, '\\n')}`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Missing baseline artifacts are reported informationally. Baselines ship
|
|
174
|
-
// reference material (like BASELINE.md) that the deterministic pipeline does
|
|
175
|
-
// not regenerate — those are expected "missing in run" hits and are NOT treated
|
|
176
|
-
// as failures. Only actual byte-level diffs fail the build.
|
|
177
|
-
if (missing.length) {
|
|
178
|
-
console.log(`note: ${missing.length} baseline artifact(s) not regenerated by deterministic run:`);
|
|
179
|
-
for (const m of missing) console.log(` - ${m}`);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
console.log(`smoke-test: ${diffs.length} diffs, ${missing.length} baseline artifacts not in fresh run`);
|
|
183
|
-
|
|
184
|
-
// Ensure .design/ was not created in the real repo root.
|
|
185
|
-
// Pollution check: only fail if NEW files appeared in REPO_ROOT/.design/ during
|
|
186
|
-
// the smoke test. Tracked files (like `.design/config.example.json` shipped by
|
|
187
|
-
// Plan 29-02) are present in the fresh checkout and are NOT pollution. We
|
|
188
|
-
// compare the directory snapshot taken before the test (top of file) against
|
|
189
|
-
// the post-test snapshot.
|
|
190
|
-
const designSnapshotAfter = snapshotDesignDir();
|
|
191
|
-
if (designSnapshotBefore !== designSnapshotAfter) {
|
|
192
|
-
console.error('ERROR: .design/ contents changed during smoke test — pipeline wrote to REPO_ROOT instead of temp dir');
|
|
193
|
-
console.error('Before:');
|
|
194
|
-
for (const line of designSnapshotBefore.split('\n')) console.error(` ${line}`);
|
|
195
|
-
console.error('After:');
|
|
196
|
-
for (const line of designSnapshotAfter.split('\n')) console.error(` ${line}`);
|
|
197
|
-
process.exit(1);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
process.exit(diffs.length > 0 ? 1 : 0);
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# rollback-release.sh — manually delete a tag + GitHub Release. NOT called from CI.
|
|
3
|
-
# Usage: bash scripts/rollback-release.sh <version>
|
|
4
|
-
# Example: bash scripts/rollback-release.sh 1.0.7
|
|
5
|
-
|
|
6
|
-
set -euo pipefail
|
|
7
|
-
|
|
8
|
-
VERSION="${1:-}"
|
|
9
|
-
if [ -z "$VERSION" ]; then
|
|
10
|
-
echo "Usage: $0 <version>"
|
|
11
|
-
echo "Example: $0 1.0.7"
|
|
12
|
-
exit 1
|
|
13
|
-
fi
|
|
14
|
-
|
|
15
|
-
TAG="v${VERSION}"
|
|
16
|
-
REPO="${GITHUB_REPOSITORY:-hegemonart/get-design-done}"
|
|
17
|
-
|
|
18
|
-
if ! command -v gh >/dev/null 2>&1; then
|
|
19
|
-
echo "ERROR: gh CLI not found."
|
|
20
|
-
exit 1
|
|
21
|
-
fi
|
|
22
|
-
|
|
23
|
-
echo "About to delete release + tag $TAG on $REPO."
|
|
24
|
-
echo "This is a DESTRUCTIVE operation. Confirm (y/N): "
|
|
25
|
-
read -r CONFIRM
|
|
26
|
-
if [ "$CONFIRM" != "y" ]; then
|
|
27
|
-
echo "Aborted."
|
|
28
|
-
exit 0
|
|
29
|
-
fi
|
|
30
|
-
|
|
31
|
-
# Delete the GitHub Release (this also deletes the tag ref on github.com).
|
|
32
|
-
gh release delete "$TAG" --repo "$REPO" --yes --cleanup-tag || {
|
|
33
|
-
echo "Release delete failed or no release existed; attempting tag-only delete."
|
|
34
|
-
gh api -X DELETE "repos/${REPO}/git/refs/tags/${TAG}" || true
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
# Also delete locally if the user has the tag.
|
|
38
|
-
git tag -d "$TAG" 2>/dev/null || true
|
|
39
|
-
|
|
40
|
-
echo "Rollback complete for $TAG."
|
|
41
|
-
echo "Note: clones that already pulled the tag retain it. Consider posting"
|
|
42
|
-
echo " a deprecation note in the next CHANGELOG entry."
|