@hegemonart/get-design-done 1.14.5 → 1.14.7
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 +7 -2
- package/.claude-plugin/plugin.json +6 -1
- package/CHANGELOG.md +84 -0
- package/README.md +17 -0
- package/agents/design-executor.md +41 -0
- package/agents/design-figma-writer.md +61 -1
- package/connections/figma.md +10 -0
- package/hooks/gdd-bash-guard.js +49 -0
- package/hooks/gdd-decision-injector.js +196 -0
- package/hooks/gdd-mcp-circuit-breaker.js +140 -0
- package/hooks/gdd-protected-paths.js +114 -0
- package/hooks/hooks.json +36 -0
- package/package.json +1 -1
- package/reference/cycle-handoff-preamble.md +22 -0
- package/reference/figma-sandbox.md +19 -0
- package/reference/mcp-budget.default.json +13 -0
- package/reference/meta-rules.md +66 -0
- package/reference/protected-paths.default.json +18 -0
- package/reference/registry.json +34 -0
- package/reference/registry.schema.json +52 -0
- package/reference/retrieval-contract.md +30 -0
- package/reference/schemas/mcp-budget.schema.json +21 -0
- package/reference/schemas/protected-paths.schema.json +19 -0
- package/reference/shared-preamble.md +6 -57
- package/scripts/build-intel.cjs +20 -0
- package/scripts/injection-patterns.cjs +42 -1
- package/scripts/lib/blast-radius.cjs +97 -0
- package/scripts/lib/dangerous-patterns.cjs +118 -0
- package/scripts/lib/glob-match.cjs +57 -0
- package/scripts/lib/reference-registry.cjs +101 -0
- package/skills/pause/SKILL.md +3 -0
- package/skills/progress/SKILL.md +2 -0
- package/skills/reflect/SKILL.md +2 -0
- package/skills/resume/SKILL.md +3 -0
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# GSD Agent Shared Preamble
|
|
2
2
|
|
|
3
|
-
> **This file is imported via `@reference/shared-preamble.md` as the first line of every agent body in `agents/*.md`. Its placement is load-bearing for Anthropic's 5-minute prompt cache (see `reference/model-tiers.md` and
|
|
3
|
+
> **This file is imported via `@reference/shared-preamble.md` as the first line of every agent body in `agents/*.md`. Its placement is load-bearing for Anthropic's 5-minute prompt cache (see `reference/model-tiers.md` and Phase 10.1 decision D-08 Layer A): because every agent opens with the identical preamble prefix, the second and subsequent agent spawns in a session pay `cached_input_per_1m` rates rather than full `input_per_1m` rates for these bytes. Do not inline this content into agent bodies — always import.**
|
|
4
|
+
>
|
|
5
|
+
> **As of Phase 14.5 this file is an aggregator.** The framework-invariant subsections (Required Reading Discipline, Writes Protocol, Deviation Handling, Completion Markers, Context-Exhaustion & Budget awareness) live in `reference/meta-rules.md` (tier L0) so the L2 heuristics/anti-patterns/checklists churn never invalidates the L0 prefix.
|
|
6
|
+
|
|
7
|
+
@reference/meta-rules.md
|
|
4
8
|
|
|
5
9
|
## Framework Identity
|
|
6
10
|
|
|
@@ -8,61 +12,6 @@ You are a GSD agent operating under the `get-design-done` plugin contract (see `
|
|
|
8
12
|
|
|
9
13
|
You are one step in a pipeline. You do not own the pipeline. The orchestrator decides what runs next based on your output.
|
|
10
14
|
|
|
11
|
-
## Required Reading Discipline
|
|
12
|
-
|
|
13
|
-
When the orchestrator's prompt contains a `<required_reading>` block, you MUST read every file it lists with the `Read` tool before taking any other action. Paths prefixed with `@` are file paths — pass them directly to `Read`. Skipping required reading is a hard violation: you will produce stale output that the downstream verifier catches, wasting a full spawn cycle.
|
|
14
|
-
|
|
15
|
-
## Writes Protocol
|
|
16
|
-
|
|
17
|
-
Only write files declared in your frontmatter `writes:` list. Agents with `reads-only: true` must never call `Write` or `Edit` on any file. If the task appears to require writing outside your declared scope, stop and return a `<blocker>` in STATE.md rather than expanding your write surface.
|
|
18
|
-
|
|
19
|
-
If your agent runs in a phase that enforces atomic commits (most do), commit only files in your declared `writes:` list. Use the repo commit convention: `docs(phase-N-P): short imperative description` for documentation-class changes, `feat(phase-N-P): ...` for new capability, `fix(phase-N-P): ...` for bug fixes. Phase and plan numbers come from `.design/STATE.md` `phase:` and the invoking plan's frontmatter.
|
|
20
|
-
|
|
21
|
-
## Deviation Handling
|
|
22
|
-
|
|
23
|
-
If an expected file is missing, a required reading entry fails to load, or the prompt references an artifact that contradicts STATE.md, **stop** before taking any destructive action. Return a structured blocker to STATE.md and terminate your response with your completion marker:
|
|
24
|
-
|
|
25
|
-
```markdown
|
|
26
|
-
<blocker>
|
|
27
|
-
type: missing-artifact | stale-state | contract-violation
|
|
28
|
-
detail: <one sentence>
|
|
29
|
-
suggested-fix: <one sentence or leave blank>
|
|
30
|
-
</blocker>
|
|
31
|
-
|
|
32
|
-
## {STAGE} COMPLETE
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Valid completion markers per agent class (from `agents/README.md` §Completion Markers):
|
|
36
|
-
- Research agent → `## RESEARCH COMPLETE`
|
|
37
|
-
- Planning agent → `## PLANNING COMPLETE`
|
|
38
|
-
- Execution agent → `## EXECUTION COMPLETE`
|
|
39
|
-
- Verification agent → `## VERIFICATION COMPLETE`
|
|
40
|
-
- Stage-specific agents → the stage name: `## SCAN COMPLETE`, `## DISCOVER COMPLETE`, `## PLAN COMPLETE`, `## DESIGN COMPLETE`, `## VERIFY COMPLETE`.
|
|
41
|
-
|
|
42
|
-
The orchestrator detects failure by reading STATE.md for a `<blocker>`, not by the absence of a marker. Always emit the marker.
|
|
43
|
-
|
|
44
|
-
## Context-Exhaustion Hook Awareness
|
|
45
|
-
|
|
46
|
-
A PostToolUse hook at `hooks/context-exhaustion.js` watches your tool output for the string `<context-exhaustion>` in your response. If you determine you cannot finish the task in the remaining context, emit:
|
|
47
|
-
|
|
48
|
-
```xml
|
|
49
|
-
<context-exhaustion>
|
|
50
|
-
reason: <one-sentence cause — e.g., "required_reading totals 47KB exceeding remaining context">
|
|
51
|
-
resume-hint: <one-sentence instruction for a resumption spawn>
|
|
52
|
-
</context-exhaustion>
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
…before your completion marker. The hook captures this into STATE.md so the orchestrator can re-spawn you with a narrower scope. Do not guess when you're near exhaustion — only emit when a concrete obstacle (file too large to read, required diff too wide) forced the call.
|
|
56
|
-
|
|
57
|
-
## Budget-Enforcer Hook Awareness (Phase 10.1)
|
|
58
|
-
|
|
59
|
-
A PreToolUse hook at `hooks/budget-enforcer.js` intercepts every `Task` spawn (including the one that invoked you). The hook may:
|
|
60
|
-
- **Short-circuit** your spawn with a cached result from `.design/cache-manifest.json` (transparent — you never run).
|
|
61
|
-
- **Downgrade** your tier to Haiku at the 80% per-task cap soft-threshold, silently (`auto_downgrade_on_cap: true` in `.design/budget.json`, D-03).
|
|
62
|
-
- **Hard-block** your spawn at the 100% per-task or per-phase cap with an actionable error (D-02).
|
|
63
|
-
|
|
64
|
-
Implication for you as the agent: **do not assume a specific model tier is live.** Your output must be correct whether you run on Haiku, Sonnet, or Opus. If a task genuinely requires reasoning density beyond Haiku, the `size_budget` + `default-tier` combination should have been set at authoring time so the router routes it correctly — the remedy is a frontmatter update (a Phase 11 reflector proposal), not a mid-run assumption.
|
|
65
|
-
|
|
66
15
|
## Ordering Convention (D-17)
|
|
67
16
|
|
|
68
17
|
Your agent body is structured in this exact order so the cache prefix stays stable:
|
|
@@ -79,4 +28,4 @@ The `/gdd:warm-cache` command (ships in Plan 10.1-02) pre-warms this identical p
|
|
|
79
28
|
|
|
80
29
|
---
|
|
81
30
|
|
|
82
|
-
*Imported by: every file under `agents/*.md` (except `agents/README.md`). Maintained as part of Phase 10.1 (OPT-07). Edits to this file affect every agent simultaneously — verify across the full agent suite before committing.*
|
|
31
|
+
*Imported by: every file under `agents/*.md` (except `agents/README.md`). Maintained as part of Phase 10.1 (OPT-07) and Phase 14.5 (L0/L2 split). Edits to this file affect every agent simultaneously — verify across the full agent suite before committing.*
|
package/scripts/build-intel.cjs
CHANGED
|
@@ -435,6 +435,26 @@ async function main() {
|
|
|
435
435
|
|
|
436
436
|
console.log(` discovered ${allFiles.length} files, ${changed.length} changed`);
|
|
437
437
|
|
|
438
|
+
// Phase 14.5: validate reference registry round-trip whenever any reference/*
|
|
439
|
+
// changed (or on --force). Fail the build on dangling/missing/duplicate.
|
|
440
|
+
const referenceChanged = FORCE || changed.some(f => f.startsWith('reference/'));
|
|
441
|
+
if (referenceChanged) {
|
|
442
|
+
try {
|
|
443
|
+
const { validateRegistry } = require(path.join(__dirname, 'lib', 'reference-registry.cjs'));
|
|
444
|
+
const v = validateRegistry({ cwd: ROOT });
|
|
445
|
+
if (!v.ok) {
|
|
446
|
+
console.error('build-intel: reference registry validation failed:');
|
|
447
|
+
if (v.missingInRegistry.length) console.error(' missing in registry:', v.missingInRegistry);
|
|
448
|
+
if (v.danglingInRegistry.length) console.error(' dangling entries:', v.danglingInRegistry);
|
|
449
|
+
if (v.duplicates.length) console.error(' duplicate entries:', v.duplicates);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
console.log(' reference registry: ok');
|
|
453
|
+
} catch (err) {
|
|
454
|
+
if (err && err.code !== 'MODULE_NOT_FOUND') throw err;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
438
458
|
// Always rebuild files slice when any file changed
|
|
439
459
|
const filesSlice = buildFilesSlice(allFiles);
|
|
440
460
|
writeSlice('files.json', filesSlice);
|
|
@@ -3,15 +3,56 @@
|
|
|
3
3
|
// hooks/gdd-read-injection-scanner.js (runtime hook) and
|
|
4
4
|
// scripts/run-injection-scanner-ci.cjs (CI scanner).
|
|
5
5
|
// Add new patterns here; both consumers pick them up automatically.
|
|
6
|
+
//
|
|
7
|
+
// Phase 14.5 adds three new families: invisible-Unicode obfuscation,
|
|
8
|
+
// HTML-comment instruction hijacks, and secret-exfil trigger patterns.
|
|
9
|
+
|
|
10
|
+
// Zero-width + word-joiner + BOM + bidi overrides. Used for detection
|
|
11
|
+
// AND as a normalization stripper for hooks that run scan after NFKC.
|
|
12
|
+
const _CONTEXT_INVISIBLE_CHARS = /[\u200B-\u200D\u2060\uFEFF\u202A-\u202E]/;
|
|
6
13
|
|
|
7
14
|
const INJECTION_PATTERNS = [
|
|
15
|
+
// ── classic prompt-injection verbs ──────────────────────────────────
|
|
8
16
|
{ name: 'ignore previous', re: /ignore\s+(all\s+)?(previous|prior|above)\s+instructions?/i },
|
|
9
17
|
{ name: 'disregard previous', re: /disregard\s+(all\s+)?(previous|prior|above)\s+instructions?/i },
|
|
18
|
+
{ name: 'forget previous', re: /forget\s+(the\s+|all\s+)?(previous|prior|above)/i },
|
|
10
19
|
{ name: 'you are now a different', re: /you\s+are\s+now\s+a\s+different/i },
|
|
11
20
|
{ name: 'system: you are', re: /system\s*:\s*you\s+are/i },
|
|
12
21
|
{ name: 'role tag injection', re: /<\s*\/?\s*(system|assistant|human)\s*>/i },
|
|
13
22
|
{ name: '[INST] fragment', re: /\[INST\]/i },
|
|
14
23
|
{ name: '### instruction fragment',re: /###\s*instruction/i },
|
|
24
|
+
|
|
25
|
+
// ── invisible-Unicode obfuscation (14.5 new family) ─────────────────
|
|
26
|
+
{ name: 'invisible-unicode chars', re: _CONTEXT_INVISIBLE_CHARS },
|
|
27
|
+
{ name: 'bidi-override instruction', re: /[\u202A-\u202E][^\n]*(ignore|disregard|forget|system\s*:)/i },
|
|
28
|
+
|
|
29
|
+
// ── HTML-comment / hidden-element instruction hijack (14.5 new) ─────
|
|
30
|
+
{ name: 'html-comment system', re: /<!--\s*system\s*:/i },
|
|
31
|
+
{ name: 'html-comment assistant', re: /<!--\s*assistant\s*:/i },
|
|
32
|
+
{ name: 'html-comment ignore', re: /<!--\s*(ignore|disregard|forget)\b/i },
|
|
33
|
+
{ name: 'hidden div system', re: /<div\s+[^>]*style\s*=\s*["'][^"']*display\s*:\s*none[^"']*["'][^>]*>\s*(system|ignore|disregard)/i },
|
|
34
|
+
{ name: 'hidden span system', re: /<span\s+[^>]*style\s*=\s*["'][^"']*visibility\s*:\s*hidden[^"']*["'][^>]*>\s*(system|ignore|disregard)/i },
|
|
35
|
+
{ name: 'zero-font-size trick', re: /style\s*=\s*["'][^"']*font-size\s*:\s*0[^"']*["'][^>]*>\s*(ignore|system|disregard)/i },
|
|
36
|
+
|
|
37
|
+
// ── secret-exfil trigger patterns (14.5 new) ─────────────────────────
|
|
38
|
+
{ name: 'curl-with-api-key-env', re: /curl\s+[^|\n]*\$\{?[A-Z][A-Z0-9_]*_(KEY|TOKEN|SECRET|PASSWORD|AUTH)\}?/ },
|
|
39
|
+
{ name: 'cat-dotenv', re: /\bcat\s+\.env(\.[a-z]+)?\b/ },
|
|
40
|
+
{ name: 'printenv-leak', re: /\bprintenv\b[^\n]{0,80}\|\s*(curl|wget|nc|ssh)/ },
|
|
41
|
+
{ name: 'tar-home-netcat', re: /\btar\s+c[fzvj]+\s+-\s+~[^\n]*\|\s*(nc|ssh|curl)/ },
|
|
42
|
+
{ name: 'env-dot-leak', re: /process\.env\.[A-Z][A-Z0-9_]*_(KEY|TOKEN|SECRET)\s*[^;,\n]*(fetch|axios|XMLHttpRequest|http\.request)/ },
|
|
43
|
+
{ name: 'ssh-key-cat', re: /\bcat\s+~?\/?\.ssh\/id_(rsa|ed25519|ecdsa|dsa)\b/ },
|
|
15
44
|
];
|
|
16
45
|
|
|
17
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Apply patterns to content and return matched pattern names (deduped).
|
|
48
|
+
*/
|
|
49
|
+
function scan(content) {
|
|
50
|
+
if (typeof content !== 'string' || !content) return [];
|
|
51
|
+
const hits = [];
|
|
52
|
+
for (const { name, re } of INJECTION_PATTERNS) {
|
|
53
|
+
if (re.test(content)) hits.push(name);
|
|
54
|
+
}
|
|
55
|
+
return hits;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { INJECTION_PATTERNS, _CONTEXT_INVISIBLE_CHARS, scan };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* scripts/lib/blast-radius.cjs — per-task blast-radius preflight.
|
|
4
|
+
*
|
|
5
|
+
* Exports:
|
|
6
|
+
* estimate({touchedPaths, diffStats, config?}) →
|
|
7
|
+
* { files, lines, exceeds, overBy, limit: {files, lines} }
|
|
8
|
+
* estimateMCPCalls({toolCalls, config?}) →
|
|
9
|
+
* { count, exceeds, overBy, limit }
|
|
10
|
+
* formatDiffSummary({touchedPaths, diffStats, result}) → string
|
|
11
|
+
* loadConfig(cwd?) → { max_files_per_task, max_lines_per_task, max_mcp_calls_per_task }
|
|
12
|
+
*
|
|
13
|
+
* Config precedence: .design/config.json.blast_radius.{max_files_per_task, max_lines_per_task, max_mcp_calls_per_task}
|
|
14
|
+
* then .design/config.json top-level same keys, then built-in defaults.
|
|
15
|
+
*
|
|
16
|
+
* Zero-value limits DISABLE that dimension.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
const DEFAULTS = {
|
|
23
|
+
max_files_per_task: 10,
|
|
24
|
+
max_lines_per_task: 400,
|
|
25
|
+
max_mcp_calls_per_task: 30,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function loadConfig(cwd) {
|
|
29
|
+
const configPath = path.join(cwd || process.cwd(), '.design', 'config.json');
|
|
30
|
+
let cfg = {};
|
|
31
|
+
try { cfg = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch { cfg = {}; }
|
|
32
|
+
const br = (cfg && typeof cfg === 'object' && cfg.blast_radius) || {};
|
|
33
|
+
return {
|
|
34
|
+
max_files_per_task: numberOr(br.max_files_per_task, cfg.max_files_per_task, DEFAULTS.max_files_per_task),
|
|
35
|
+
max_lines_per_task: numberOr(br.max_lines_per_task, cfg.max_lines_per_task, DEFAULTS.max_lines_per_task),
|
|
36
|
+
max_mcp_calls_per_task: numberOr(br.max_mcp_calls_per_task, cfg.max_mcp_calls_per_task, DEFAULTS.max_mcp_calls_per_task),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function numberOr(...candidates) {
|
|
41
|
+
for (const v of candidates) {
|
|
42
|
+
if (typeof v === 'number' && Number.isFinite(v) && v >= 0) return v;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function estimate({ touchedPaths = [], diffStats = {}, config } = {}) {
|
|
48
|
+
const cfg = config || loadConfig();
|
|
49
|
+
const files = new Set((touchedPaths || []).filter(Boolean)).size;
|
|
50
|
+
const lines = (diffStats.insertions || 0) + (diffStats.deletions || 0);
|
|
51
|
+
const fileLimit = cfg.max_files_per_task;
|
|
52
|
+
const lineLimit = cfg.max_lines_per_task;
|
|
53
|
+
const fileExceeds = fileLimit > 0 && files > fileLimit;
|
|
54
|
+
const lineExceeds = lineLimit > 0 && lines > lineLimit;
|
|
55
|
+
return {
|
|
56
|
+
files,
|
|
57
|
+
lines,
|
|
58
|
+
exceeds: fileExceeds || lineExceeds,
|
|
59
|
+
overBy: {
|
|
60
|
+
files: fileExceeds ? files - fileLimit : 0,
|
|
61
|
+
lines: lineExceeds ? lines - lineLimit : 0,
|
|
62
|
+
},
|
|
63
|
+
limit: { files: fileLimit, lines: lineLimit },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function estimateMCPCalls({ toolCalls = [], config } = {}) {
|
|
68
|
+
const cfg = config || loadConfig();
|
|
69
|
+
const count = Array.isArray(toolCalls) ? toolCalls.length : 0;
|
|
70
|
+
const limit = cfg.max_mcp_calls_per_task;
|
|
71
|
+
const exceeds = limit > 0 && count > limit;
|
|
72
|
+
return {
|
|
73
|
+
count,
|
|
74
|
+
exceeds,
|
|
75
|
+
overBy: exceeds ? count - limit : 0,
|
|
76
|
+
limit,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatDiffSummary({ touchedPaths = [], diffStats = {}, result }) {
|
|
81
|
+
const r = result || estimate({ touchedPaths, diffStats });
|
|
82
|
+
const lines = [];
|
|
83
|
+
lines.push('## Blast-Radius Preflight — Over Limit');
|
|
84
|
+
lines.push('');
|
|
85
|
+
lines.push(`Files touched: ${r.files} (limit ${r.limit.files || 'disabled'})`);
|
|
86
|
+
lines.push(`Lines changed: ${r.lines} (limit ${r.limit.lines || 'disabled'})`);
|
|
87
|
+
if (r.overBy.files) lines.push(`Over by: +${r.overBy.files} files`);
|
|
88
|
+
if (r.overBy.lines) lines.push(`Over by: +${r.overBy.lines} lines`);
|
|
89
|
+
lines.push('');
|
|
90
|
+
lines.push('Touched paths:');
|
|
91
|
+
for (const p of touchedPaths) lines.push(` - ${p}`);
|
|
92
|
+
lines.push('');
|
|
93
|
+
lines.push('To proceed: split the task into ≤-limit chunks, or raise the ceiling in `.design/config.json.blast_radius.{max_files_per_task,max_lines_per_task}`.');
|
|
94
|
+
return lines.join('\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { estimate, estimateMCPCalls, formatDiffSummary, loadConfig, DEFAULTS };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* scripts/lib/dangerous-patterns.cjs — canonical dangerous-shell pattern list
|
|
4
|
+
* + Unicode NFKC + ANSI-strip + zero-width/bidi normalization used by
|
|
5
|
+
* hooks/gdd-bash-guard.js and any downstream audit tooling.
|
|
6
|
+
*
|
|
7
|
+
* Contract: exports
|
|
8
|
+
* - normalize(s): string — NFKC + ANSI strip + zero-width/bidi strip
|
|
9
|
+
* - patterns: Array<{name, regex, description, severity}>
|
|
10
|
+
* - match(command): { matched: boolean, pattern?, description?, severity? }
|
|
11
|
+
*
|
|
12
|
+
* Severity levels: 'critical' (system-destroying), 'high' (data-destroying / credential),
|
|
13
|
+
* 'medium' (destructive but scoped).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
17
|
+
const INVISIBLE_RE = /[\u200B-\u200D\u2060\uFEFF\u202A-\u202E]/g;
|
|
18
|
+
|
|
19
|
+
function normalize(s) {
|
|
20
|
+
if (typeof s !== 'string') return '';
|
|
21
|
+
return s.normalize('NFKC').replace(ANSI_RE, '').replace(INVISIBLE_RE, '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Hex-decode helper so obfuscated `\x72\x6d` attacks land in the same pattern space.
|
|
25
|
+
function hexDecodedVariant(s) {
|
|
26
|
+
return s.replace(/\\x([0-9a-fA-F]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Ordered: critical first so the first match is always the worst.
|
|
30
|
+
const patterns = [
|
|
31
|
+
// ── filesystem destruction ────────────────────────────────────────────
|
|
32
|
+
{ name: 'rm-rf-root', regex: /\brm\s+-[rRfF]+\s+\/(\s|$)/, description: 'rm -rf / — root filesystem deletion', severity: 'critical' },
|
|
33
|
+
{ name: 'rm-rf-no-preserve-root', regex: /\brm\s+-[rRfF]+\s+(--no-preserve-root)/, description: 'rm -rf with --no-preserve-root', severity: 'critical' },
|
|
34
|
+
{ name: 'rm-rf-home', regex: /\brm\s+-[rRfF]+\s+(~|\/home(\s|\/|$))/, description: 'rm -rf of home directory', severity: 'critical' },
|
|
35
|
+
{ name: 'rm-rf-etc', regex: /\brm\s+-[rRfF]+\s+\/etc(\s|\/|$)/, description: 'rm -rf of /etc', severity: 'critical' },
|
|
36
|
+
{ name: 'rm-rf-wildcard-root', regex: /\brm\s+-[rRfF]+\s+\/\*/, description: 'rm -rf /*', severity: 'critical' },
|
|
37
|
+
{ name: 'rm-rf-usr', regex: /\brm\s+-[rRfF]+\s+\/usr(\s|\/|$)/, description: 'rm -rf /usr', severity: 'critical' },
|
|
38
|
+
{ name: 'rm-rf-var', regex: /\brm\s+-[rRfF]+\s+\/var(\s|\/|$)/, description: 'rm -rf /var', severity: 'critical' },
|
|
39
|
+
{ name: 'fork-bomb', regex: /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/, description: 'classic :(){ :|:& };: fork bomb', severity: 'critical' },
|
|
40
|
+
|
|
41
|
+
// ── disk / device mutation ───────────────────────────────────────────
|
|
42
|
+
{ name: 'dd-zero-to-device', regex: /\bdd\s+if=\/dev\/(zero|random|urandom)\s+of=\/dev\/(sd|hd|nvme)/, description: 'dd overwrite of block device', severity: 'critical' },
|
|
43
|
+
{ name: 'mkfs-device', regex: /\bmkfs(\.[a-z0-9]+)?\s+\/dev\//, description: 'mkfs on a block device', severity: 'critical' },
|
|
44
|
+
{ name: 'redirect-to-device', regex: />\s*\/dev\/(sd|hd|nvme)[a-z]\d*/, description: 'shell redirect overwrites a block device', severity: 'critical' },
|
|
45
|
+
|
|
46
|
+
// ── permission escalation ────────────────────────────────────────────
|
|
47
|
+
{ name: 'chmod-777', regex: /\bchmod\s+(-[A-Za-z]+\s+)?777\b/, description: 'chmod 777 — world-writable permissions', severity: 'high' },
|
|
48
|
+
{ name: 'chmod-recursive-world', regex: /\bchmod\s+-R\s+[0-7]*[0-7]7[0-7]\b/, description: 'chmod -R with world-writable bits', severity: 'high' },
|
|
49
|
+
{ name: 'chown-root-recursive', regex: /\bchown\s+-R\s+root(:root)?\s+\//, description: 'chown -R root on absolute path', severity: 'high' },
|
|
50
|
+
|
|
51
|
+
// ── pipe to shell ─────────────────────────────────────────────────────
|
|
52
|
+
{ name: 'curl-pipe-sh', regex: /\bcurl\s[^|;&\n]*\|\s*(sh|bash|zsh|sudo\s+sh|sudo\s+bash)\b/i, description: 'curl … | sh (remote-code execution)', severity: 'critical' },
|
|
53
|
+
{ name: 'wget-pipe-sh', regex: /\bwget\b[^|;&\n]*\|\s*(sh|bash|zsh|sudo\s+sh|sudo\s+bash)\b/i, description: 'wget … | sh (remote-code execution)', severity: 'critical' },
|
|
54
|
+
{ name: 'fetch-pipe-sh', regex: /\bfetch\s[^|;&\n]*\|\s*(sh|bash)\b/i, description: 'fetch … | sh', severity: 'critical' },
|
|
55
|
+
{ name: 'eval-curl', regex: /\beval\s*"?\s*\$\s*\(\s*(curl|wget)/i, description: 'eval $(curl|wget …) — remote-code execution via eval', severity: 'critical' },
|
|
56
|
+
|
|
57
|
+
// ── git destruction ─────────────────────────────────────────────────
|
|
58
|
+
{ name: 'git-reset-hard-HEAD', regex: /\bgit\s+reset\s+--hard\s*(HEAD)?(\s|$)/, description: 'git reset --hard (unscoped) — discards uncommitted work', severity: 'high' },
|
|
59
|
+
{ name: 'git-clean-fd', regex: /\bgit\s+clean\s+-[a-z]*[fF][a-z]*[dD][a-z]*\b/, description: 'git clean -fd — untracked file wipe', severity: 'high' },
|
|
60
|
+
{ name: 'git-push-force-main', regex: /\bgit\s+push\s+(--force|-f)(\s+\S+)?\s+(main|master|trunk)/i, description: 'git push --force to a protected branch', severity: 'high' },
|
|
61
|
+
{ name: 'git-branch-delete-main', regex: /\bgit\s+branch\s+-D\s+(main|master|trunk)\b/i, description: 'git branch -D on the main branch', severity: 'high' },
|
|
62
|
+
{ name: 'git-filter-repo', regex: /\bgit\s+filter-(branch|repo)\s+/, description: 'git history rewrite', severity: 'high' },
|
|
63
|
+
{ name: 'git-checkout-all', regex: /\bgit\s+checkout\s+\.\s*$/, description: 'git checkout . — overwrites all uncommitted edits', severity: 'medium' },
|
|
64
|
+
|
|
65
|
+
// ── system mutation / config ────────────────────────────────────────
|
|
66
|
+
{ name: 'sed-inplace-etc', regex: /\bsed\s+-i\s+[^\n]*\/etc\//, description: 'sed -i on /etc/* config', severity: 'high' },
|
|
67
|
+
{ name: 'shutdown-now', regex: /\b(shutdown|halt|poweroff|reboot)\s+(-[a-z]\s+)*(now|0|\+0)\b/, description: 'shutdown/halt/reboot now', severity: 'high' },
|
|
68
|
+
{ name: 'init-0-6', regex: /\binit\s+[06]\b/, description: 'init 0/6 — system halt or reboot', severity: 'high' },
|
|
69
|
+
|
|
70
|
+
// ── process nuking ────────────────────────────────────────────────
|
|
71
|
+
{ name: 'kill-all-pgrep', regex: /\bkill\s+-9\s+\$\(\s*pgrep/, description: 'kill -9 $(pgrep …) — broad process kill', severity: 'medium' },
|
|
72
|
+
{ name: 'killall-9', regex: /\bkillall\s+-9\b/, description: 'killall -9', severity: 'medium' },
|
|
73
|
+
{ name: 'pkill-9-dotall', regex: /\bpkill\s+-9\s+-f\s+['"]?\.\*['"]?/, description: 'pkill -9 -f .* — process kill everything', severity: 'high' },
|
|
74
|
+
|
|
75
|
+
// ── credential exfil ────────────────────────────────────────────────
|
|
76
|
+
{ name: 'env-to-curl', regex: /\b(cat\s+)?\.env(\.[a-z]+)?\s*\|\s*curl/i, description: 'exfil .env via curl', severity: 'critical' },
|
|
77
|
+
{ name: 'ssh-key-to-curl', regex: /\bcat\s+~?\/?\.ssh\/id_(rsa|ed25519|ecdsa|dsa)\b[^\n]*\|\s*(curl|nc|ssh)/, description: 'ssh private-key exfiltration', severity: 'critical' },
|
|
78
|
+
{ name: 'printenv-to-curl', regex: /\bprintenv\b[^\n]*\|\s*(curl|wget|nc)/, description: 'printenv | curl — environment exfiltration', severity: 'critical' },
|
|
79
|
+
{ name: 'tar-home-to-netcat', regex: /\btar\s+c[fzvj]+\s+-\s+~[^\n]*\|\s*(nc|ssh|curl)/, description: 'tar ~ | nc|ssh — home-directory exfil', severity: 'critical' },
|
|
80
|
+
{ name: 'aws-credentials-read', regex: /\bcat\s+~\/\.aws\/credentials\b/, description: 'reading AWS credentials file', severity: 'high' },
|
|
81
|
+
|
|
82
|
+
// ── shell mutation / obfuscation ────────────────────────────────────
|
|
83
|
+
{ name: 'bash-decode-base64', regex: /\becho\s+[A-Za-z0-9+\/=]{40,}\s*\|\s*base64\s+-d\s*\|\s*(sh|bash)/, description: 'base64 decode | shell — obfuscated exec', severity: 'critical' },
|
|
84
|
+
{ name: 'exec-python-c', regex: /\bpython[23]?\s+-c\s+["']import\s+os[^"']*os\.(system|popen|exec)\b/i, description: 'python -c inline os.system shell-out', severity: 'high' },
|
|
85
|
+
{ name: 'bash-c-remote', regex: /\bbash\s+-c\s+["'][^"']*(curl|wget)\s+[^"']*\|/, description: 'bash -c with embedded curl|wget pipe', severity: 'critical' },
|
|
86
|
+
|
|
87
|
+
// ── path traversal ────────────────────────────────────────────────
|
|
88
|
+
{ name: 'path-traversal-deep', regex: /(\.\.\/){5,}/, description: '5+ chained ../../../../../ traversal', severity: 'medium' },
|
|
89
|
+
|
|
90
|
+
// ── npm / package registry abuse ────────────────────────────────────
|
|
91
|
+
{ name: 'npm-install-remote-tgz', regex: /\bnpm\s+(install|i)\s+https?:\/\/[^\s]+\.tgz/, description: 'npm install from an arbitrary HTTP tarball URL', severity: 'high' },
|
|
92
|
+
{ name: 'npm-publish-force', regex: /\bnpm\s+publish\s+(--force|-f)\b/, description: 'npm publish --force (bypasses version checks)', severity: 'medium' },
|
|
93
|
+
{ name: 'npm-run-in-quotes-eval', regex: /\bnpm\s+run\s+\S+\s+--\s+--?eval=/, description: 'npm run … --eval= (code injection through script runner)', severity: 'medium' },
|
|
94
|
+
|
|
95
|
+
// ── docker / container escape ───────────────────────────────────────
|
|
96
|
+
{ name: 'docker-run-privileged', regex: /\bdocker\s+run\b[^\n]*--privileged\b[^\n]*(--|[^\n]*(-v|--volume)\s+\/:\/host|\/var\/run\/docker\.sock)/, description: 'docker run --privileged mounting host fs / docker socket', severity: 'critical' },
|
|
97
|
+
{ name: 'docker-socket-mount', regex: /-v\s+\/var\/run\/docker\.sock:\/var\/run\/docker\.sock/, description: 'host docker socket mount (escape vector)', severity: 'high' },
|
|
98
|
+
|
|
99
|
+
// ── firewall / networking flip ──────────────────────────────────────
|
|
100
|
+
{ name: 'iptables-flush', regex: /\biptables\s+-F(\s|$)/, description: 'iptables -F — flush all firewall rules', severity: 'high' },
|
|
101
|
+
{ name: 'ufw-disable', regex: /\bufw\s+disable\b/, description: 'ufw disable — firewall off', severity: 'high' },
|
|
102
|
+
|
|
103
|
+
// ── sudo bypass ────────────────────────────────────────────────────
|
|
104
|
+
{ name: 'sudo-nopasswd-write', regex: /\becho\s+[^\n]*NOPASSWD[^\n]*\|\s*sudo\s+tee\s+\/etc\/sudoers/, description: 'sudo NOPASSWD injection via tee', severity: 'critical' },
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
function match(command) {
|
|
108
|
+
const normalized = normalize(command);
|
|
109
|
+
const hexVariant = hexDecodedVariant(normalized);
|
|
110
|
+
for (const p of patterns) {
|
|
111
|
+
if (p.regex.test(normalized) || p.regex.test(hexVariant)) {
|
|
112
|
+
return { matched: true, pattern: p.name, description: p.description, severity: p.severity };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { matched: false };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { normalize, patterns, match, _INVISIBLE_RE: INVISIBLE_RE, _ANSI_RE: ANSI_RE };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* scripts/lib/glob-match.cjs — tiny dependency-free glob matcher.
|
|
4
|
+
* Supports: **, *, ?, and literal segments. Not a full minimatch implementation,
|
|
5
|
+
* but covers the patterns used in reference/protected-paths.default.json.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function globToRegex(glob) {
|
|
9
|
+
// Normalize separators
|
|
10
|
+
const g = glob.replace(/\\/g, '/');
|
|
11
|
+
let re = '^';
|
|
12
|
+
let i = 0;
|
|
13
|
+
while (i < g.length) {
|
|
14
|
+
const c = g[i];
|
|
15
|
+
if (c === '*' && g[i + 1] === '*') {
|
|
16
|
+
// `**` — match zero or more of ANY characters (including path separators).
|
|
17
|
+
// Consume a trailing `/` so `reference/**/foo` becomes `reference/.*foo`
|
|
18
|
+
// and also matches `reference/foo` (the empty-match case).
|
|
19
|
+
let j = i + 2;
|
|
20
|
+
if (g[j] === '/') j++;
|
|
21
|
+
re += '.*';
|
|
22
|
+
i = j;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (c === '*') {
|
|
26
|
+
// single-segment wildcard
|
|
27
|
+
re += '[^/]*';
|
|
28
|
+
i++;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (c === '?') {
|
|
32
|
+
re += '[^/]';
|
|
33
|
+
i++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if ('.+^$(){}[]|\\'.includes(c)) {
|
|
37
|
+
re += '\\' + c;
|
|
38
|
+
i++;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
re += c;
|
|
42
|
+
i++;
|
|
43
|
+
}
|
|
44
|
+
re += '$';
|
|
45
|
+
return new RegExp(re);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function matches(filepath, globList) {
|
|
49
|
+
const norm = String(filepath).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
50
|
+
for (const g of globList) {
|
|
51
|
+
const re = globToRegex(g);
|
|
52
|
+
if (re.test(norm)) return { matched: true, pattern: g };
|
|
53
|
+
}
|
|
54
|
+
return { matched: false };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { matches, globToRegex };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* scripts/lib/reference-registry.cjs — typed index over reference/*.md.
|
|
4
|
+
*
|
|
5
|
+
* Exports:
|
|
6
|
+
* list({type}) → Entry[] (filter by type; omit to return all)
|
|
7
|
+
* find(name) → Entry | null (exact-name lookup)
|
|
8
|
+
* validateRegistry({cwd}) → { ok, missingInRegistry, danglingInRegistry, duplicates }
|
|
9
|
+
* loadRegistry({cwd}) → Registry (read-through; caches at module scope)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
16
|
+
const DEFAULT_REGISTRY_PATH = path.join(REPO_ROOT, 'reference', 'registry.json');
|
|
17
|
+
|
|
18
|
+
let _cache = null;
|
|
19
|
+
let _cachePath = null;
|
|
20
|
+
|
|
21
|
+
function loadRegistry({ cwd } = {}) {
|
|
22
|
+
const p = cwd ? path.join(cwd, 'reference', 'registry.json') : DEFAULT_REGISTRY_PATH;
|
|
23
|
+
if (_cache && _cachePath === p) return _cache;
|
|
24
|
+
_cachePath = p;
|
|
25
|
+
_cache = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
26
|
+
return _cache;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function list({ type, cwd } = {}) {
|
|
30
|
+
const reg = loadRegistry({ cwd });
|
|
31
|
+
if (!type) return reg.entries.slice();
|
|
32
|
+
return reg.entries.filter(e => e.type === type);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function find(name, { cwd } = {}) {
|
|
36
|
+
const reg = loadRegistry({ cwd });
|
|
37
|
+
return reg.entries.find(e => e.name === name) || null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Walk reference/*.md + reference/*.json (excluding schemas/**, data/**) and
|
|
42
|
+
* compare to the registry. Returns:
|
|
43
|
+
* - missingInRegistry : files on disk not referenced by any entry
|
|
44
|
+
* - danglingInRegistry: entries whose paths do not exist on disk
|
|
45
|
+
* - duplicates : entries sharing the same `name` OR the same `path`
|
|
46
|
+
*/
|
|
47
|
+
function validateRegistry({ cwd } = {}) {
|
|
48
|
+
const root = cwd || REPO_ROOT;
|
|
49
|
+
const refDir = path.join(root, 'reference');
|
|
50
|
+
const reg = (() => {
|
|
51
|
+
try { return JSON.parse(fs.readFileSync(path.join(refDir, 'registry.json'), 'utf8')); }
|
|
52
|
+
catch { return { entries: [] }; }
|
|
53
|
+
})();
|
|
54
|
+
|
|
55
|
+
const onDisk = new Set();
|
|
56
|
+
for (const leaf of walk(refDir)) {
|
|
57
|
+
const rel = path.relative(root, leaf).replace(/\\/g, '/');
|
|
58
|
+
// Exclude registry itself, all .schema.json files (non-registerable),
|
|
59
|
+
// schemas tree, data tree, and non-md/non-json files.
|
|
60
|
+
if (rel === 'reference/registry.json') continue;
|
|
61
|
+
if (rel.endsWith('.schema.json')) continue;
|
|
62
|
+
if (rel.startsWith('reference/schemas/')) continue;
|
|
63
|
+
if (rel.startsWith('reference/data/')) continue;
|
|
64
|
+
if (!/\.(md|json)$/.test(rel)) continue;
|
|
65
|
+
onDisk.add(rel);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const registryPaths = new Set(reg.entries.map(e => e.path));
|
|
69
|
+
const missingInRegistry = [...onDisk].filter(p => !registryPaths.has(p)).sort();
|
|
70
|
+
const danglingInRegistry = reg.entries
|
|
71
|
+
.filter(e => !fs.existsSync(path.join(root, e.path)))
|
|
72
|
+
.map(e => ({ name: e.name, path: e.path }));
|
|
73
|
+
|
|
74
|
+
const nameCount = {}, pathCount = {};
|
|
75
|
+
for (const e of reg.entries) {
|
|
76
|
+
nameCount[e.name] = (nameCount[e.name] || 0) + 1;
|
|
77
|
+
pathCount[e.path] = (pathCount[e.path] || 0) + 1;
|
|
78
|
+
}
|
|
79
|
+
const duplicates = [];
|
|
80
|
+
for (const [k, v] of Object.entries(nameCount)) if (v > 1) duplicates.push({ kind: 'name', key: k, count: v });
|
|
81
|
+
for (const [k, v] of Object.entries(pathCount)) if (v > 1) duplicates.push({ kind: 'path', key: k, count: v });
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
ok: missingInRegistry.length === 0 && danglingInRegistry.length === 0 && duplicates.length === 0,
|
|
85
|
+
missingInRegistry,
|
|
86
|
+
danglingInRegistry,
|
|
87
|
+
duplicates,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function* walk(dir) {
|
|
92
|
+
let entries;
|
|
93
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
94
|
+
for (const e of entries) {
|
|
95
|
+
const full = path.join(dir, e.name);
|
|
96
|
+
if (e.isDirectory()) yield* walk(full);
|
|
97
|
+
else if (e.isFile()) yield full;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { list, find, validateRegistry, loadRegistry };
|
package/skills/pause/SKILL.md
CHANGED
|
@@ -5,6 +5,9 @@ argument-hint: "[context note]"
|
|
|
5
5
|
tools: Read, Write, AskUserQuestion
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
@reference/retrieval-contract.md
|
|
9
|
+
@reference/cycle-handoff-preamble.md
|
|
10
|
+
|
|
8
11
|
# /gdd:pause
|
|
9
12
|
|
|
10
13
|
Captures enough state that a killed or stopped session can resume cleanly via `/gdd:resume`.
|
package/skills/progress/SKILL.md
CHANGED
package/skills/reflect/SKILL.md
CHANGED
|
@@ -5,6 +5,8 @@ argument-hint: "[--dry-run] [--cycle <slug>]"
|
|
|
5
5
|
tools: Read, Write, Task
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
@reference/retrieval-contract.md
|
|
9
|
+
|
|
8
10
|
# /gdd:reflect
|
|
9
11
|
|
|
10
12
|
Run `design-reflector` on demand against the current (or specified) cycle. Produces `.design/reflections/<cycle-slug>.md` with numbered improvement proposals. Every proposal requires explicit user review — nothing is auto-applied.
|
package/skills/resume/SKILL.md
CHANGED
|
@@ -5,6 +5,9 @@ argument-hint: ""
|
|
|
5
5
|
tools: Read, Write, Bash, Glob
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
@reference/retrieval-contract.md
|
|
9
|
+
@reference/cycle-handoff-preamble.md
|
|
10
|
+
|
|
8
11
|
# /gdd:resume
|
|
9
12
|
|
|
10
13
|
Inverse of `/gdd:pause`. Reads the handoff file, prints a clear "you were here" summary, and routes to the next command.
|