godpowers 1.6.24 → 2.1.0
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/AGENTS.md +1 -1
- package/CHANGELOG.md +166 -0
- package/README.md +103 -8
- package/RELEASE.md +48 -50
- package/SKILL.md +9 -1
- package/agents/god-design-reviewer.md +6 -6
- package/agents/god-designer.md +1 -1
- package/agents/god-executor.md +23 -0
- package/agents/god-quality-reviewer.md +12 -1
- package/agents/god-spec-reviewer.md +10 -0
- package/bin/install.js +137 -655
- package/extensions/data-pack/manifest.yaml +1 -1
- package/extensions/data-pack/package.json +1 -1
- package/extensions/launch-pack/README.md +1 -1
- package/extensions/launch-pack/manifest.yaml +1 -1
- package/extensions/launch-pack/package.json +1 -1
- package/extensions/security-pack/manifest.yaml +1 -1
- package/extensions/security-pack/package.json +1 -1
- package/fixtures/quick-proof/manifest.json +19 -0
- package/fixtures/quick-proof/project/.godpowers/prep/INITIAL-FINDINGS.md +5 -0
- package/fixtures/quick-proof/project/.godpowers/state.json +69 -0
- package/fixtures/quick-proof/project/README.md +5 -0
- package/fixtures/quick-proof/project/package.json +6 -0
- package/lib/agent-browser-driver.js +13 -13
- package/lib/agent-cache.js +8 -1
- package/lib/agent-refs.js +161 -0
- package/lib/budget.js +25 -11
- package/lib/events.js +11 -4
- package/lib/extension-authoring.js +27 -0
- package/lib/feature-awareness.js +24 -0
- package/lib/fs-async.js +28 -0
- package/lib/installer-args.js +99 -0
- package/lib/installer-core.js +345 -0
- package/lib/installer-files.js +80 -0
- package/lib/installer-runtimes.js +112 -0
- package/lib/intent.js +111 -16
- package/lib/quick-proof.js +153 -0
- package/lib/release-surface-sync.js +8 -1
- package/lib/repo-surface-sync.js +9 -2
- package/lib/review-required.js +2 -1
- package/lib/router.js +23 -3
- package/lib/skill-surface.js +42 -0
- package/lib/state-lock.js +10 -0
- package/lib/state.js +101 -8
- package/lib/workflow-runner.js +42 -5
- package/package.json +7 -3
- package/references/HAVE-NOTS.md +4 -3
- package/references/orchestration/GOD-MODE-RUNBOOK.md +273 -0
- package/routing/god-arch.yaml +1 -1
- package/routing/god-build.yaml +1 -1
- package/skills/god-add-backlog.md +1 -1
- package/skills/god-agent-audit.md +2 -2
- package/skills/god-build.md +5 -3
- package/skills/god-context-scan.md +2 -3
- package/skills/god-design.md +2 -2
- package/skills/god-doctor.md +2 -2
- package/skills/god-extension-info.md +1 -1
- package/skills/god-help.md +4 -3
- package/skills/god-mode.md +10 -266
- package/skills/god-org-context.md +1 -1
- package/skills/god-repair.md +3 -3
- package/skills/god-review.md +9 -0
- package/skills/god-stories.md +1 -1
- package/skills/god-test-extension.md +1 -1
- package/skills/god-version.md +2 -2
|
@@ -11,7 +11,7 @@ Channel-specific launch strategists for Godpowers.
|
|
|
11
11
|
| `/god-indie-hackers` | god-indie-hackers-strategist | Numbers-first, mistakes, real questions |
|
|
12
12
|
| `/god-oss-release` | god-oss-release-strategist | README, SemVer, examples that run |
|
|
13
13
|
|
|
14
|
-
Plus 4 workflows and
|
|
14
|
+
Plus 4 workflows and 23 channel-specific have-nots.
|
|
15
15
|
|
|
16
16
|
## When to use
|
|
17
17
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "quick-proof",
|
|
3
|
+
"description": "Deterministic fixture for the Godpowers quick proof CLI command.",
|
|
4
|
+
"project": "quick-proof-saas",
|
|
5
|
+
"proves": [
|
|
6
|
+
"state on disk",
|
|
7
|
+
"missing artifacts",
|
|
8
|
+
"next command",
|
|
9
|
+
"host guarantees"
|
|
10
|
+
],
|
|
11
|
+
"expected": {
|
|
12
|
+
"state": "in progress",
|
|
13
|
+
"next": "/god-prd",
|
|
14
|
+
"missing": [
|
|
15
|
+
".godpowers/prd/PRD.md",
|
|
16
|
+
".godpowers/roadmap/ROADMAP.md"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Initial Findings
|
|
2
|
+
|
|
3
|
+
- [DECISION] This quick-proof fixture represents a project that has been initialized but does not have a PRD yet.
|
|
4
|
+
- [DECISION] The expected next command is `/god-prd`.
|
|
5
|
+
- [HYPOTHESIS] A first-time user can understand the Godpowers proof loop by seeing state, missing artifacts, host guarantees, and a next action together.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://godpowers.dev/schema/state.v1.json",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"project": {
|
|
5
|
+
"name": "quick-proof-saas",
|
|
6
|
+
"started": "2026-05-16T00:00:00.000Z"
|
|
7
|
+
},
|
|
8
|
+
"active-workstream": "main",
|
|
9
|
+
"tiers": {
|
|
10
|
+
"tier-0": {
|
|
11
|
+
"orchestration": {
|
|
12
|
+
"status": "done",
|
|
13
|
+
"updated": "2026-05-16T00:00:00.000Z"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"tier-1": {
|
|
17
|
+
"prd": {
|
|
18
|
+
"status": "pending"
|
|
19
|
+
},
|
|
20
|
+
"arch": {
|
|
21
|
+
"status": "pending"
|
|
22
|
+
},
|
|
23
|
+
"roadmap": {
|
|
24
|
+
"status": "pending"
|
|
25
|
+
},
|
|
26
|
+
"stack": {
|
|
27
|
+
"status": "pending"
|
|
28
|
+
},
|
|
29
|
+
"design": {
|
|
30
|
+
"status": "not-required",
|
|
31
|
+
"reason": "Fixture is focused on state and routing proof."
|
|
32
|
+
},
|
|
33
|
+
"product": {
|
|
34
|
+
"status": "not-required",
|
|
35
|
+
"reason": "Fixture is focused on state and routing proof."
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"tier-2": {
|
|
39
|
+
"repo": {
|
|
40
|
+
"status": "pending"
|
|
41
|
+
},
|
|
42
|
+
"build": {
|
|
43
|
+
"status": "pending"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"tier-3": {
|
|
47
|
+
"deploy": {
|
|
48
|
+
"status": "pending"
|
|
49
|
+
},
|
|
50
|
+
"observe": {
|
|
51
|
+
"status": "pending"
|
|
52
|
+
},
|
|
53
|
+
"launch": {
|
|
54
|
+
"status": "pending"
|
|
55
|
+
},
|
|
56
|
+
"harden": {
|
|
57
|
+
"status": "pending"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"lifecycle-phase": "in-arc",
|
|
62
|
+
"linkage": {
|
|
63
|
+
"coverage-pct": 0,
|
|
64
|
+
"orphan-count": 0,
|
|
65
|
+
"drift-count": 0,
|
|
66
|
+
"review-required-items": 0
|
|
67
|
+
},
|
|
68
|
+
"yolo-decisions": []
|
|
69
|
+
}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* isInstalled() -> bool
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
const {
|
|
33
|
+
const { execFileSync } = require('child_process');
|
|
34
34
|
const path = require('path');
|
|
35
35
|
|
|
36
36
|
/**
|
|
@@ -38,14 +38,14 @@ const path = require('path');
|
|
|
38
38
|
*/
|
|
39
39
|
function isInstalled() {
|
|
40
40
|
try {
|
|
41
|
-
|
|
41
|
+
execFileSync('agent-browser', ['--version'], {
|
|
42
42
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
43
43
|
timeout: 5000
|
|
44
44
|
});
|
|
45
45
|
return true;
|
|
46
46
|
} catch (e1) {
|
|
47
47
|
try {
|
|
48
|
-
|
|
48
|
+
execFileSync('npx', ['--no-install', 'agent-browser', '--version'], {
|
|
49
49
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
50
50
|
timeout: 5000
|
|
51
51
|
});
|
|
@@ -59,21 +59,21 @@ function isInstalled() {
|
|
|
59
59
|
/**
|
|
60
60
|
* Run an agent-browser CLI command and return stdout.
|
|
61
61
|
* Throws on non-zero exit. Times out after 30s default.
|
|
62
|
+
*
|
|
63
|
+
* Arguments are passed as an argv array with the shell disabled. URLs,
|
|
64
|
+
* selectors, and eval expressions can originate from project content
|
|
65
|
+
* (PRD/DESIGN acceptance criteria, CLI flags), so they must never be
|
|
66
|
+
* concatenated into a shell command string where metacharacters like
|
|
67
|
+
* `;`, `&&`, `$()`, or backticks could trigger command injection.
|
|
62
68
|
*/
|
|
63
69
|
function run(args, opts = {}) {
|
|
64
|
-
const cmd = Array.isArray(args) ? args : [args];
|
|
65
|
-
|
|
66
|
-
const argString = cmd.map(a => {
|
|
67
|
-
const s = String(a);
|
|
68
|
-
if (/\s/.test(s) && !s.startsWith('"')) return `"${s.replace(/"/g, '\\"')}"`;
|
|
69
|
-
return s;
|
|
70
|
-
}).join(' ');
|
|
71
|
-
const fullCmd = `agent-browser ${argString}`;
|
|
72
|
-
return execSync(fullCmd, {
|
|
70
|
+
const cmd = (Array.isArray(args) ? args : [args]).map(String);
|
|
71
|
+
return execFileSync('agent-browser', cmd, {
|
|
73
72
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
74
73
|
timeout: opts.timeout || 30000,
|
|
75
74
|
cwd: opts.cwd || process.cwd(),
|
|
76
|
-
encoding: 'utf8'
|
|
75
|
+
encoding: 'utf8',
|
|
76
|
+
shell: false
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
79
|
|
package/lib/agent-cache.js
CHANGED
|
@@ -139,7 +139,14 @@ function clear(projectRoot, opts = {}) {
|
|
|
139
139
|
const fpath = path.join(shardPath, fname);
|
|
140
140
|
let entry;
|
|
141
141
|
try { entry = JSON.parse(fs.readFileSync(fpath, 'utf8')); }
|
|
142
|
-
catch (e) {
|
|
142
|
+
catch (e) {
|
|
143
|
+
// Corrupt or non-JSON entry: prune it only on an explicit full clear.
|
|
144
|
+
// A narrow clear (by agent, expiry, or age) must not delete unrelated
|
|
145
|
+
// unparseable files it was never asked to touch.
|
|
146
|
+
if (opts.all) { fs.unlinkSync(fpath); removed++; }
|
|
147
|
+
else { kept++; }
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
143
150
|
let shouldRemove = false;
|
|
144
151
|
if (opts.all) shouldRemove = true;
|
|
145
152
|
else if (opts.agent && entry.agent === opts.agent) shouldRemove = true;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { isCompatible, parseManifest } = require('./extensions');
|
|
4
|
+
|
|
5
|
+
const AGENT_CONTRACT_VERSION = '1.0.0';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} AgentRef
|
|
9
|
+
* @property {string|null} agent Agent name without the range suffix.
|
|
10
|
+
* @property {string|null} range SemVer range declared after the final at sign.
|
|
11
|
+
* @property {string} raw Original workflow `uses` value.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {AgentRef & { contractVersion: string, valid: boolean, errors: string[] }} AgentRefValidation
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} ref
|
|
20
|
+
* @returns {AgentRef}
|
|
21
|
+
*/
|
|
22
|
+
function parseAgentRef(ref) {
|
|
23
|
+
if (!ref) return { agent: null, range: null, raw: ref };
|
|
24
|
+
const raw = String(ref).trim();
|
|
25
|
+
const at = raw.lastIndexOf('@');
|
|
26
|
+
if (at <= 0) {
|
|
27
|
+
return { agent: raw, range: null, raw };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
agent: raw.slice(0, at),
|
|
31
|
+
range: raw.slice(at + 1),
|
|
32
|
+
raw
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} ref
|
|
38
|
+
* @param {string} [contractVersion]
|
|
39
|
+
* @returns {AgentRefValidation}
|
|
40
|
+
*/
|
|
41
|
+
function validateAgentRef(ref, contractVersion = AGENT_CONTRACT_VERSION) {
|
|
42
|
+
const parsed = parseAgentRef(ref);
|
|
43
|
+
const errors = [];
|
|
44
|
+
|
|
45
|
+
if (!parsed.agent || !/^[a-z][a-z0-9-]*$/.test(parsed.agent)) {
|
|
46
|
+
errors.push(`invalid agent name in uses: ${ref}`);
|
|
47
|
+
}
|
|
48
|
+
if (!parsed.range) {
|
|
49
|
+
errors.push(`agent ref must include a semver range: ${ref}`);
|
|
50
|
+
} else if (!isCompatible(parsed.range, contractVersion)) {
|
|
51
|
+
errors.push(`agent ref ${ref} does not satisfy agent contract ${contractVersion}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { ...parsed, contractVersion, valid: errors.length === 0, errors };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} ref
|
|
59
|
+
* @param {string} [contractVersion]
|
|
60
|
+
* @returns {AgentRefValidation}
|
|
61
|
+
*/
|
|
62
|
+
function assertAgentRef(ref, contractVersion = AGENT_CONTRACT_VERSION) {
|
|
63
|
+
const result = validateAgentRef(ref, contractVersion);
|
|
64
|
+
if (!result.valid) {
|
|
65
|
+
throw new Error(result.errors.join('; '));
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @typedef {Object} ProseRef
|
|
72
|
+
* @property {string} token Unresolved god-* token (leading slash stripped).
|
|
73
|
+
* @property {string} file Repo-relative path where the token was found.
|
|
74
|
+
* @property {number} count Occurrences of that token in that file.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
const PROSE_TOKEN = /(?<![A-Za-z0-9_])\/?god-[a-z0-9]+(?:-[a-z0-9]+)*/g;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Collect the set of every god-* command/agent name that legitimately exists:
|
|
81
|
+
* core skills, core agents, and the skills/agents provided by first-party
|
|
82
|
+
* extension packs. {god, godpowers} are always valid (front door + binary).
|
|
83
|
+
*
|
|
84
|
+
* @param {string} rootDir Repository root.
|
|
85
|
+
* @returns {Set<string>}
|
|
86
|
+
*/
|
|
87
|
+
function knownGodNames(rootDir) {
|
|
88
|
+
const valid = new Set(['god', 'godpowers']);
|
|
89
|
+
|
|
90
|
+
for (const f of fs.readdirSync(path.join(rootDir, 'skills'))) {
|
|
91
|
+
if (f.endsWith('.md')) valid.add(f.replace(/\.md$/, ''));
|
|
92
|
+
}
|
|
93
|
+
for (const f of fs.readdirSync(path.join(rootDir, 'agents'))) {
|
|
94
|
+
if (/^god-.*\.md$/.test(f)) valid.add(f.replace(/\.md$/, ''));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const extDir = path.join(rootDir, 'extensions');
|
|
98
|
+
if (fs.existsSync(extDir)) {
|
|
99
|
+
for (const pack of fs.readdirSync(extDir)) {
|
|
100
|
+
const manifestPath = path.join(extDir, pack, 'manifest.yaml');
|
|
101
|
+
if (!fs.existsSync(manifestPath)) continue;
|
|
102
|
+
let provides = {};
|
|
103
|
+
try {
|
|
104
|
+
const parsed = parseManifest(fs.readFileSync(manifestPath, 'utf8'));
|
|
105
|
+
provides = (parsed && parsed.manifest && parsed.manifest.provides) || {};
|
|
106
|
+
} catch (e) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
for (const list of [provides.skills, provides.agents]) {
|
|
110
|
+
if (Array.isArray(list)) {
|
|
111
|
+
for (const name of list) valid.add(String(name).trim());
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return valid;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Scan skill and agent prose for `god-*` references that do not resolve to a
|
|
122
|
+
* real core skill, core agent, or extension-provided skill/agent. Catches
|
|
123
|
+
* phantom references that the workflow `uses:` validator cannot see because
|
|
124
|
+
* they live in markdown bodies rather than YAML.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} rootDir Repository root.
|
|
127
|
+
* @returns {ProseRef[]} Unresolved references (empty when everything resolves).
|
|
128
|
+
*/
|
|
129
|
+
function findUnresolvedProseRefs(rootDir) {
|
|
130
|
+
const valid = knownGodNames(rootDir);
|
|
131
|
+
const unresolved = [];
|
|
132
|
+
|
|
133
|
+
for (const dir of ['skills', 'agents']) {
|
|
134
|
+
const dirPath = path.join(rootDir, dir);
|
|
135
|
+
for (const f of fs.readdirSync(dirPath)) {
|
|
136
|
+
if (!f.endsWith('.md')) continue;
|
|
137
|
+
const text = fs.readFileSync(path.join(dirPath, f), 'utf8');
|
|
138
|
+
const counts = new Map();
|
|
139
|
+
let m;
|
|
140
|
+
while ((m = PROSE_TOKEN.exec(text)) !== null) {
|
|
141
|
+
const token = m[0].replace(/^\//, '');
|
|
142
|
+
if (valid.has(token)) continue;
|
|
143
|
+
counts.set(token, (counts.get(token) || 0) + 1);
|
|
144
|
+
}
|
|
145
|
+
for (const [token, count] of counts) {
|
|
146
|
+
unresolved.push({ token, file: `${dir}/${f}`, count });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return unresolved;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = {
|
|
155
|
+
AGENT_CONTRACT_VERSION,
|
|
156
|
+
parseAgentRef,
|
|
157
|
+
validateAgentRef,
|
|
158
|
+
assertAgentRef,
|
|
159
|
+
knownGodNames,
|
|
160
|
+
findUnresolvedProseRefs
|
|
161
|
+
};
|
package/lib/budget.js
CHANGED
|
@@ -75,19 +75,32 @@ function writeBlock(projectRoot, newBudgets) {
|
|
|
75
75
|
*/
|
|
76
76
|
function stripBudgets(raw) {
|
|
77
77
|
const lines = raw.split('\n');
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
78
|
+
const range = findTopLevelBlock(lines, 'budgets');
|
|
79
|
+
if (!range) return raw;
|
|
80
|
+
const out = lines.slice(0, range.start).concat(lines.slice(range.end));
|
|
81
|
+
return out.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function findTopLevelBlock(lines, key) {
|
|
85
|
+
const header = new RegExp(`^${key}\\s*:(?:\\s*(?:#.*)?)?$`);
|
|
86
|
+
let start = -1;
|
|
87
|
+
for (let i = 0; i < lines.length; i++) {
|
|
88
|
+
if (header.test(lines[i])) {
|
|
89
|
+
start = i;
|
|
90
|
+
break;
|
|
87
91
|
}
|
|
88
|
-
out.push(line);
|
|
89
92
|
}
|
|
90
|
-
|
|
93
|
+
if (start === -1) return null;
|
|
94
|
+
|
|
95
|
+
let end = lines.length;
|
|
96
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
97
|
+
if (!lines[i].trim()) continue;
|
|
98
|
+
if (/^\S/.test(lines[i])) {
|
|
99
|
+
end = i;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { start, end };
|
|
91
104
|
}
|
|
92
105
|
|
|
93
106
|
/**
|
|
@@ -211,5 +224,6 @@ module.exports = {
|
|
|
211
224
|
writeBlock,
|
|
212
225
|
stripBudgets,
|
|
213
226
|
renderBudgets,
|
|
227
|
+
findTopLevelBlock,
|
|
214
228
|
DEFAULTS
|
|
215
229
|
};
|
package/lib/events.js
CHANGED
|
@@ -161,10 +161,17 @@ function verifyChain(file) {
|
|
|
161
161
|
function readRun(projectRoot, runId) {
|
|
162
162
|
const file = eventsPath(projectRoot, runId);
|
|
163
163
|
if (!fs.existsSync(file)) return [];
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
const out = [];
|
|
165
|
+
for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
|
|
166
|
+
if (!line) continue;
|
|
167
|
+
try {
|
|
168
|
+
out.push(JSON.parse(line));
|
|
169
|
+
} catch (e) {
|
|
170
|
+
// Skip a torn or partial line (e.g. the process was killed mid-append).
|
|
171
|
+
// Hash-chain integrity is verified separately by verifyChain.
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
168
175
|
}
|
|
169
176
|
|
|
170
177
|
/**
|
|
@@ -26,6 +26,24 @@ function packageFolderName(name) {
|
|
|
26
26
|
return name.replace(/^@/, '').replace(/\//g, '-');
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function assertSafePackageName(value) {
|
|
30
|
+
if (!/^@?[a-z0-9][a-z0-9-]*(\/[a-z0-9][a-z0-9-]*)?$/.test(value)) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Invalid extension name "${value}": expected a package name like ` +
|
|
33
|
+
`"@scope/pack" or "pack" using lowercase letters, digits, and dashes.`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function assertSafeSegment(value, label) {
|
|
39
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(value)) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Invalid ${label} "${value}": use lowercase letters, digits, and dashes ` +
|
|
42
|
+
`only. Slashes, dots, and ".." are rejected to prevent path traversal.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
29
47
|
function scaffold(outputRoot, opts = {}) {
|
|
30
48
|
const name = opts.name || '@godpowers/custom-pack';
|
|
31
49
|
const version = opts.version || '0.1.0';
|
|
@@ -34,6 +52,15 @@ function scaffold(outputRoot, opts = {}) {
|
|
|
34
52
|
const workflow = opts.workflow || null;
|
|
35
53
|
const range = opts.godpowersRange || '>=1.6.0';
|
|
36
54
|
const folder = opts.folder || packageFolderName(name);
|
|
55
|
+
|
|
56
|
+
// Validate all user-supplied names before they reach a filesystem path, so a
|
|
57
|
+
// value like "../../escape" can never write outside the output folder.
|
|
58
|
+
assertSafePackageName(name);
|
|
59
|
+
assertSafeSegment(skill, 'skill');
|
|
60
|
+
if (agent) assertSafeSegment(agent, 'agent');
|
|
61
|
+
if (workflow) assertSafeSegment(workflow, 'workflow');
|
|
62
|
+
assertSafeSegment(folder, 'folder');
|
|
63
|
+
|
|
37
64
|
const root = path.join(outputRoot, folder);
|
|
38
65
|
const written = [];
|
|
39
66
|
|
package/lib/feature-awareness.js
CHANGED
|
@@ -83,6 +83,30 @@ const FEATURES = [
|
|
|
83
83
|
commands: ['/god-status', '/god-next'],
|
|
84
84
|
description: 'Report full, degraded, or unknown host guarantees in dashboard output.'
|
|
85
85
|
},
|
|
86
|
+
{
|
|
87
|
+
id: 'quick-proof',
|
|
88
|
+
since: '2.0.0',
|
|
89
|
+
commands: ['godpowers quick-proof'],
|
|
90
|
+
description: 'Render a shipped proof fixture with computed next action and host guarantees.'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'request-trace-review',
|
|
94
|
+
since: '2.0.1',
|
|
95
|
+
commands: ['/god-build', '/god-review', '/god-feature', '/god-refactor'],
|
|
96
|
+
description: 'Keep implementation diffs narrow by requiring request-trace evidence and reviewer checks for scope, simplicity, and surgicality.'
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'release-hardening',
|
|
100
|
+
since: '2.0.2',
|
|
101
|
+
commands: ['npm test', 'npm run lint', 'npm run release:check'],
|
|
102
|
+
description: 'Maintain release validation through a delegated test runner, static checks, parser coverage, and hardened package and runtime checks.'
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'maintenance-hardening',
|
|
106
|
+
since: '2.0.3',
|
|
107
|
+
commands: ['npm test', 'npm run lint', 'npm run release:check'],
|
|
108
|
+
description: 'Track installer decomposition, shared test harness adoption, executable agent refs, skill metadata sync, God Mode runbook delegation, and async runtime APIs.'
|
|
109
|
+
},
|
|
86
110
|
{
|
|
87
111
|
id: 'extension-authoring',
|
|
88
112
|
since: '1.6.22',
|
package/lib/fs-async.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const fs = require('fs/promises');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
async function exists(filePath) {
|
|
5
|
+
try {
|
|
6
|
+
await fs.access(filePath);
|
|
7
|
+
return true;
|
|
8
|
+
} catch (_) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function readJson(filePath) {
|
|
14
|
+
return JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function writeJson(filePath, value) {
|
|
18
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
19
|
+
await fs.writeFile(filePath, JSON.stringify(value, null, 2) + '\n');
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
exists,
|
|
25
|
+
readJson,
|
|
26
|
+
writeJson,
|
|
27
|
+
fs
|
|
28
|
+
};
|