godpowers 2.4.0 → 2.4.2
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/CHANGELOG.md +44 -0
- package/README.md +42 -10
- package/RELEASE.md +33 -34
- package/lib/README.md +1 -0
- package/lib/adoption-metrics.js +87 -0
- package/lib/agent-validator.js +2 -27
- package/lib/checkpoint.js +4 -19
- package/lib/context-budget.js +5 -34
- package/lib/design-spec.js +11 -13
- package/lib/extensions.js +12 -2
- package/lib/frontmatter.js +74 -0
- package/lib/installer-core.js +7 -52
- package/lib/intent.js +88 -6
- package/lib/pillars.js +2 -41
- package/lib/quick-proof.js +10 -1
- package/lib/recipes.js +10 -2
- package/lib/router.js +10 -2
- package/lib/skill-surface.js +2 -11
- package/lib/workflow-parser.js +12 -4
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.4.2] - 2026-06-09
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Added strict YAML diagnostics for the dependency-free parser, covering
|
|
14
|
+
skipped malformed lines, unsafe prototype-pollution keys, and legacy empty
|
|
15
|
+
array shorthand.
|
|
16
|
+
- Added shared markdown frontmatter parsing through `lib/frontmatter.js` and a
|
|
17
|
+
static check that blocks new inline parser drift.
|
|
18
|
+
- Added dev-only coverage tooling through `c8` and `npm run coverage`.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Routing, recipe, and workflow loaders now surface strict YAML warnings with
|
|
22
|
+
file and line context.
|
|
23
|
+
- Installer metadata, Pillars, skill validation, agent validation, checkpoint
|
|
24
|
+
reads, context budgets, skill surface metadata, and DESIGN.md parsing now use
|
|
25
|
+
the shared frontmatter helper.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Extension manifests now fail closed on malformed YAML lines and unsafe keys
|
|
29
|
+
instead of accepting partially parsed manifests.
|
|
30
|
+
- Removed stale root tarballs and `.DS_Store` package clutter before release.
|
|
31
|
+
|
|
32
|
+
## [2.4.1] - 2026-06-08
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- Added adoption-proof outcome metrics for Quick Proof and Adoption Canary
|
|
36
|
+
reports, covering commands to first signal, disk-state source, missing
|
|
37
|
+
artifacts, next command, host level, and host gaps.
|
|
38
|
+
- Added the First 10 Minute Proof case study as a repo-verifiable public proof
|
|
39
|
+
artifact before the first external repository canary.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- Updated README and Getting Started to lead with `--profile=core` and the
|
|
43
|
+
brief Quick Proof path before full autonomy.
|
|
44
|
+
- Updated Quick Proof, Adoption Canary, Reference, Roadmap, and Proof
|
|
45
|
+
Transcript docs to separate observable adoption evidence from broader product
|
|
46
|
+
claims.
|
|
47
|
+
- Added surface-discipline guidance so new public commands require adoption
|
|
48
|
+
evidence before expanding the command surface.
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
- Package guardrails now require the adoption metrics runtime helper so the
|
|
52
|
+
published package keeps Quick Proof and canary metrics available.
|
|
53
|
+
|
|
10
54
|
## [2.4.0] - 2026-06-08
|
|
11
55
|
|
|
12
56
|
### Added
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/aihxp/godpowers/actions/workflows/ci.yml)
|
|
4
4
|
[](LICENSE)
|
|
5
|
-
[](CHANGELOG.md)
|
|
6
6
|
[](https://www.npmjs.com/package/godpowers)
|
|
7
7
|
|
|
8
8
|
**Ship fast. Ship right. Ship everything. Ship accountably.**
|
|
@@ -13,18 +13,19 @@ tool** (Claude Code, Codex, Cursor, etc.) that orchestrate **specialist agents**
|
|
|
13
13
|
in fresh contexts to do the work.
|
|
14
14
|
|
|
15
15
|
Want the short proof first? Start with [Quick Proof](docs/quick-proof.md) to
|
|
16
|
-
run `npx godpowers quick-proof --project
|
|
17
|
-
starter command set, and understand runtime expectations before reading the
|
|
18
|
-
full reference.
|
|
16
|
+
run `npx godpowers quick-proof --project=. --brief`, see outcome metrics, pick
|
|
17
|
+
a starter command set, and understand runtime expectations before reading the
|
|
18
|
+
full reference. The [First 10 Minute Proof Case Study](docs/case-studies/first-10-minute-proof.md)
|
|
19
|
+
shows the same evidence as a before-and-after adoption story.
|
|
19
20
|
|
|
20
21
|
Godpowers makes AI coding accountable: every serious run should leave disk
|
|
21
22
|
state, artifacts, validation gates, host guarantees, and a next action. Code is
|
|
22
23
|
only one output. The project memory and proof trail matter too.
|
|
23
24
|
|
|
24
|
-
Version 2.4.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
Version 2.4.2 keeps the 2.4 command-family UX and hardens the release-facing
|
|
26
|
+
runtime: strict YAML diagnostics for routing, recipes, workflows, and extension
|
|
27
|
+
manifests; shared markdown frontmatter parsing; dev-only coverage tooling; and
|
|
28
|
+
clean package hygiene before publish.
|
|
28
29
|
|
|
29
30
|
Maintainer hardening continues on the 2.x line with small, audited public
|
|
30
31
|
surface updates when they close real workflow gaps. The 2.1.0 patch closes a command-injection vector in the
|
|
@@ -47,6 +48,20 @@ next command, why it is recommended, whether the project is ready, the first
|
|
|
47
48
|
blockers that need attention, and whether the current host can provide full,
|
|
48
49
|
degraded, or unknown runtime guarantees.
|
|
49
50
|
|
|
51
|
+
### Ten Minute Proof Path
|
|
52
|
+
|
|
53
|
+
Run this before deciding whether Godpowers is worth a full project arc:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx godpowers quick-proof --project=. --brief
|
|
57
|
+
npx godpowers status --project=. --brief
|
|
58
|
+
npx godpowers next --project=. --brief
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The first command should produce disk-state evidence, missing-artifact
|
|
62
|
+
visibility, a next command, host guarantees, and outcome metrics. The next two
|
|
63
|
+
commands show what Godpowers can infer from your current project.
|
|
64
|
+
|
|
50
65
|
It fuses four disciplines into one unified workflow:
|
|
51
66
|
|
|
52
67
|
- **Native project context** - every Godpowers project is a Pillars project:
|
|
@@ -80,7 +95,7 @@ should prove:
|
|
|
80
95
|
## Install
|
|
81
96
|
|
|
82
97
|
```bash
|
|
83
|
-
npx godpowers --claude --global
|
|
98
|
+
npx godpowers --claude --global --profile=core
|
|
84
99
|
```
|
|
85
100
|
|
|
86
101
|
Other targets: `--codex`, `--cursor`, `--windsurf`, `--opencode`, `--gemini`,
|
|
@@ -94,7 +109,8 @@ The installer copies:
|
|
|
94
109
|
- Codex agent metadata to `<runtime>/agents/*.toml`
|
|
95
110
|
- SessionStart hook (Claude Code only) to `<runtime>/hooks/`
|
|
96
111
|
|
|
97
|
-
Installer profiles keep the visible command surface calm
|
|
112
|
+
Installer profiles keep the visible command surface calm. Start with `core` or
|
|
113
|
+
`builder` unless you already know you need the full maintainer surface:
|
|
98
114
|
|
|
99
115
|
```bash
|
|
100
116
|
npx godpowers --claude --global --profile=core
|
|
@@ -207,6 +223,21 @@ commands remain direct shortcuts.
|
|
|
207
223
|
| Maintain project health | `/god-hygiene`, `/god-update-deps`, `/god-docs`, `/god-check-todos` |
|
|
208
224
|
| Extend Godpowers | `/god-extension-scaffold --name=@godpowers/my-pack --output=.`, `/god-test-extension`, `/god-extension-add`, `/god-extension-list` |
|
|
209
225
|
|
|
226
|
+
### Outcome Metrics
|
|
227
|
+
|
|
228
|
+
Godpowers reports adoption and run signals separately from narrative claims:
|
|
229
|
+
|
|
230
|
+
| Metric | Where it appears |
|
|
231
|
+
|---|---|
|
|
232
|
+
| Commands to first signal | `quick-proof` outcome metrics |
|
|
233
|
+
| Next command and reason | `quick-proof`, `status`, `next`, `/god-next` |
|
|
234
|
+
| Missing artifacts | dashboard planning visibility |
|
|
235
|
+
| Host gaps | host guarantee line |
|
|
236
|
+
| Run duration, pauses, retries, cost | `/god-metrics`, `/god-trace`, `/god-cost` |
|
|
237
|
+
|
|
238
|
+
New public command surface should be added only when existing families,
|
|
239
|
+
ladders, profiles, recipes, and docs cannot express a proven user need.
|
|
240
|
+
|
|
210
241
|
The same status engine is available from the installer CLI for humans, CI,
|
|
211
242
|
Codex, Claude, Cursor, Gemini, OpenCode, Windsurf, Antigravity, and any host
|
|
212
243
|
runtime that can execute Node:
|
|
@@ -509,6 +540,7 @@ Pi. T3 Code inherits from the underlying agent (Codex / Claude / OpenCode).
|
|
|
509
540
|
|
|
510
541
|
- [Getting Started](docs/getting-started.md)
|
|
511
542
|
- [Quick Proof](docs/quick-proof.md)
|
|
543
|
+
- [First 10 Minute Proof Case Study](docs/case-studies/first-10-minute-proof.md)
|
|
512
544
|
- [Concepts](docs/concepts.md)
|
|
513
545
|
- [Command reference (all 112 skills + 40 agents)](docs/reference.md)
|
|
514
546
|
- [Feature awareness](docs/feature-awareness.md)
|
package/RELEASE.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
# Godpowers 2.4.
|
|
1
|
+
# Godpowers 2.4.2 Release
|
|
2
2
|
|
|
3
3
|
> Status: Ready for package verification
|
|
4
|
-
> Date: 2026-06-
|
|
4
|
+
> Date: 2026-06-09
|
|
5
5
|
|
|
6
|
-
Godpowers 2.4.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
Godpowers 2.4.2 is a release-hardening patch for the 2.4 line. It keeps the
|
|
7
|
+
2.4 command-family UX intact while making parser failures, frontmatter
|
|
8
|
+
metadata, coverage visibility, and package hygiene more accountable before
|
|
9
|
+
publish.
|
|
10
10
|
|
|
11
11
|
## What's in this release
|
|
12
12
|
|
|
@@ -17,43 +17,42 @@ and auditable workflow helper groups.
|
|
|
17
17
|
|
|
18
18
|
## Highlights
|
|
19
19
|
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
flows, and clarify workstream versus suite collaboration paths.
|
|
34
|
-
- Extension journey docs now describe current extension-pack-required flows
|
|
35
|
-
instead of old release annotations.
|
|
36
|
-
- Release guardrails now require the command-family runtime files, helper group
|
|
37
|
-
runtime files, and command-family regression test.
|
|
20
|
+
- YAML parsing now exposes strict diagnostics for malformed skipped lines and
|
|
21
|
+
unsafe prototype-pollution keys while preserving default legacy reads.
|
|
22
|
+
- Routing, recipe, and workflow YAML loaders now surface warnings with file and
|
|
23
|
+
line context.
|
|
24
|
+
- Extension manifests now fail closed on malformed YAML lines and unsafe keys.
|
|
25
|
+
- Markdown frontmatter parsing is centralized in `lib/frontmatter.js` and
|
|
26
|
+
enforced by `scripts/static-check.js`.
|
|
27
|
+
- Installer metadata, Pillars, skill validation, agent validation,
|
|
28
|
+
checkpoints, context budgets, skill surface metadata, and DESIGN.md parsing
|
|
29
|
+
now share the same frontmatter path.
|
|
30
|
+
- `npm run coverage` is available through dev-only `c8` tooling.
|
|
31
|
+
- Stale root package tarballs and `.DS_Store` clutter were removed before
|
|
32
|
+
release.
|
|
38
33
|
|
|
39
34
|
## Validation
|
|
40
35
|
|
|
41
|
-
- `
|
|
42
|
-
- `
|
|
43
|
-
- `
|
|
44
|
-
- `
|
|
36
|
+
- `node scripts/test-yaml-parser.js` green
|
|
37
|
+
- `node scripts/test-frontmatter.js` green
|
|
38
|
+
- `node scripts/test-extensions.js` green
|
|
39
|
+
- `node scripts/test-router.js` green
|
|
40
|
+
- `node scripts/test-recipes.js` green
|
|
41
|
+
- `node scripts/test-install-smoke.js` green
|
|
42
|
+
- `npm run release:check` required before publish
|
|
43
|
+
- `npm pack` creates a local `godpowers-2.4.2.tgz` tarball for package
|
|
45
44
|
inspection
|
|
46
45
|
|
|
47
46
|
## Upgrade
|
|
48
47
|
|
|
49
|
-
- `npm install -g godpowers@2.4.
|
|
48
|
+
- `npm install -g godpowers@2.4.2` or `npx godpowers@2.4.2`
|
|
50
49
|
- Re-run `/god-context` in each project to refresh installed runtime metadata
|
|
51
|
-
- No breaking changes
|
|
52
|
-
|
|
50
|
+
- No breaking changes for valid `.godpowers/` state. Invalid extension
|
|
51
|
+
manifests that were previously partially accepted now fail with parse errors.
|
|
53
52
|
|
|
54
53
|
## Notes
|
|
55
54
|
|
|
56
|
-
- GitHub release creation for `v2.4.
|
|
55
|
+
- GitHub release creation for `v2.4.2`
|
|
57
56
|
- The tag should match the npm package version
|
|
58
|
-
- The `v2.4.
|
|
59
|
-
`godpowers@2.4.
|
|
57
|
+
- The `v2.4.2` tag should point to the release commit that matches the npm
|
|
58
|
+
`godpowers@2.4.2` package.
|
package/lib/README.md
CHANGED
|
@@ -11,6 +11,7 @@ package-level integrations.
|
|
|
11
11
|
| `state.js` | Read, initialize, validate, and write `.godpowers/state.json`. |
|
|
12
12
|
| `state-lock.js` | Coordinate state writes with a lock file. |
|
|
13
13
|
| `intent.js` | Read and validate `intent.yaml` from project roots or `.godpowers/`. |
|
|
14
|
+
| `frontmatter.js` | Parse shared markdown YAML frontmatter for skills, agents, Pillars, checkpoints, and design specs. |
|
|
14
15
|
| `checkpoint.js` | Create and inspect resumable checkpoint artifacts. |
|
|
15
16
|
| `feature-awareness.js` | Detect and refresh existing-project awareness after runtime upgrades. |
|
|
16
17
|
| `code-intelligence.js` | Detect optional `ast-grep`, `sg`, and LSP tooling for structural search, rewrite, and diagnostics guidance. |
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adoption proof metrics.
|
|
3
|
+
*
|
|
4
|
+
* These metrics keep first-user trust claims tied to observable CLI signals
|
|
5
|
+
* instead of narrative confidence.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function countMissingPlanning(planning = {}) {
|
|
9
|
+
return Object.values(planning)
|
|
10
|
+
.filter(item => item && item.status === 'missing')
|
|
11
|
+
.length;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function fromQuickProof(proof) {
|
|
15
|
+
const dashboard = proof.dashboard || {};
|
|
16
|
+
const progress = dashboard.progress || {};
|
|
17
|
+
const planning = dashboard.planning || {};
|
|
18
|
+
const next = dashboard.next || {};
|
|
19
|
+
const host = proof.host || {};
|
|
20
|
+
const hostGaps = Array.isArray(host.gaps) ? host.gaps : [];
|
|
21
|
+
const completed = Number.isFinite(progress.completed) ? progress.completed : 0;
|
|
22
|
+
const total = Number.isFinite(progress.total) ? progress.total : 0;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
commandsToFirstSignal: 1,
|
|
26
|
+
stateSource: proof.statePath || 'unknown',
|
|
27
|
+
trackedStepsComplete: completed,
|
|
28
|
+
trackedStepsTotal: total,
|
|
29
|
+
missingPlanningArtifacts: countMissingPlanning(planning),
|
|
30
|
+
nextCommand: next.command || 'describe the next intent',
|
|
31
|
+
nextReason: next.reason || 'No route was computed.',
|
|
32
|
+
hostLevel: host.level || 'unknown',
|
|
33
|
+
hostGapCount: hostGaps.length,
|
|
34
|
+
proofSignals: [
|
|
35
|
+
'disk state',
|
|
36
|
+
'missing artifact',
|
|
37
|
+
'next command',
|
|
38
|
+
'host guarantee'
|
|
39
|
+
]
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function render(metrics) {
|
|
44
|
+
return [
|
|
45
|
+
` Commands to first signal: ${metrics.commandsToFirstSignal}`,
|
|
46
|
+
` State source: ${metrics.stateSource}`,
|
|
47
|
+
` Tracked steps: ${metrics.trackedStepsComplete} of ${metrics.trackedStepsTotal}`,
|
|
48
|
+
` Missing planning artifacts: ${metrics.missingPlanningArtifacts}`,
|
|
49
|
+
` Next command: ${metrics.nextCommand}`,
|
|
50
|
+
` Host level: ${metrics.hostLevel}`,
|
|
51
|
+
` Host gaps: ${metrics.hostGapCount}`
|
|
52
|
+
].join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function canaryMetrics(outputs = {}) {
|
|
56
|
+
const quickProof = outputs.quickProof || '';
|
|
57
|
+
const status = outputs.status || '';
|
|
58
|
+
const next = outputs.next || '';
|
|
59
|
+
return {
|
|
60
|
+
cliSignalsCaptured: ['quick-proof', 'status', 'next']
|
|
61
|
+
.filter((name) => outputs[camelSignal(name)] && outputs[camelSignal(name)].trim()).length,
|
|
62
|
+
quickProofHasNext: /\bNext:\s+\S+/.test(quickProof),
|
|
63
|
+
statusHasDashboard: /Godpowers Dashboard|Current status:/.test(status),
|
|
64
|
+
nextHasRecommendation: /Suggested next command:|Recommended:|Next:/.test(next)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function camelSignal(name) {
|
|
69
|
+
if (name === 'quick-proof') return 'quickProof';
|
|
70
|
+
return name;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderCanary(metrics) {
|
|
74
|
+
return [
|
|
75
|
+
`- [DECISION] CLI signals captured: ${metrics.cliSignalsCaptured} of 3.`,
|
|
76
|
+
`- [DECISION] Quick Proof reported a next action: ${metrics.quickProofHasNext ? 'yes' : 'no'}.`,
|
|
77
|
+
`- [DECISION] Status rendered a dashboard signal: ${metrics.statusHasDashboard ? 'yes' : 'no'}.`,
|
|
78
|
+
`- [DECISION] Next rendered a recommendation signal: ${metrics.nextHasRecommendation ? 'yes' : 'no'}.`
|
|
79
|
+
].join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
fromQuickProof,
|
|
84
|
+
render,
|
|
85
|
+
canaryMetrics,
|
|
86
|
+
renderCanary
|
|
87
|
+
};
|
package/lib/agent-validator.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
const fs = require('fs');
|
|
20
20
|
const path = require('path');
|
|
21
|
+
const frontmatter = require('./frontmatter');
|
|
21
22
|
|
|
22
23
|
const REQUIRED_FRONTMATTER = ['name', 'description'];
|
|
23
24
|
const RECOMMENDED_FRONTMATTER = ['tools'];
|
|
@@ -37,33 +38,7 @@ function parseAgentFile(filePath) {
|
|
|
37
38
|
raw
|
|
38
39
|
};
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
if (raw.startsWith('---')) {
|
|
42
|
-
const end = raw.indexOf('\n---', 3);
|
|
43
|
-
if (end > 0) {
|
|
44
|
-
const fmBlock = raw.slice(3, end).trim();
|
|
45
|
-
// Parse simple YAML (key: value, key: |, etc.)
|
|
46
|
-
let currentMultiline = null;
|
|
47
|
-
for (const line of fmBlock.split('\n')) {
|
|
48
|
-
if (currentMultiline) {
|
|
49
|
-
if (line.startsWith(' ') || line.startsWith('\t')) {
|
|
50
|
-
result.frontmatter[currentMultiline] += '\n' + line.trim();
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
currentMultiline = null;
|
|
54
|
-
}
|
|
55
|
-
const m = line.match(/^([\w-]+):\s*(.*)$/);
|
|
56
|
-
if (m) {
|
|
57
|
-
if (m[2] === '|' || m[2] === '>') {
|
|
58
|
-
currentMultiline = m[1];
|
|
59
|
-
result.frontmatter[m[1]] = '';
|
|
60
|
-
} else {
|
|
61
|
-
result.frontmatter[m[1]] = m[2].trim();
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
41
|
+
result.frontmatter = frontmatter.parse(raw, { strict: true, source: filePath });
|
|
67
42
|
|
|
68
43
|
// Sections: lines starting with # / ## / ###
|
|
69
44
|
const lines = raw.split('\n');
|
package/lib/checkpoint.js
CHANGED
|
@@ -50,6 +50,7 @@ const fs = require('fs');
|
|
|
50
50
|
const path = require('path');
|
|
51
51
|
const crypto = require('crypto');
|
|
52
52
|
const atomic = require('./atomic-write');
|
|
53
|
+
const frontmatterLib = require('./frontmatter');
|
|
53
54
|
|
|
54
55
|
const MAX_ACTIONS = 20;
|
|
55
56
|
const MAX_FACTS = 10;
|
|
@@ -74,25 +75,9 @@ function read(projectRoot) {
|
|
|
74
75
|
if (!fs.existsSync(file)) return null;
|
|
75
76
|
const raw = fs.readFileSync(file, 'utf8');
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (raw.startsWith('---')) {
|
|
81
|
-
const end = raw.indexOf('\n---', 3);
|
|
82
|
-
if (end > 0) {
|
|
83
|
-
const fmText = raw.slice(3, end).trim();
|
|
84
|
-
body = raw.slice(end + 4).trim();
|
|
85
|
-
for (const line of fmText.split('\n')) {
|
|
86
|
-
const m = line.match(/^([\w-]+):\s*(.*)$/);
|
|
87
|
-
if (m) {
|
|
88
|
-
let v = m[2].trim();
|
|
89
|
-
if (v === 'true') v = true;
|
|
90
|
-
else if (v === 'false') v = false;
|
|
91
|
-
frontmatter[m[1]] = v;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
78
|
+
const parsed = frontmatterLib.split(raw, { strict: true, source: file });
|
|
79
|
+
const frontmatter = parsed.frontmatter || {};
|
|
80
|
+
const body = parsed.body.trim();
|
|
96
81
|
|
|
97
82
|
// Section parse for actions and facts
|
|
98
83
|
const actions = parseList(body, 'Last actions');
|
package/lib/context-budget.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
const fs = require('fs');
|
|
27
27
|
const path = require('path');
|
|
28
|
+
const frontmatter = require('./frontmatter');
|
|
28
29
|
|
|
29
30
|
const BYTES_PER_TOKEN = 4; // rough English text heuristic
|
|
30
31
|
const DEFAULT_MAX_TOKENS = 80_000; // per-agent default; ~half of a 200K window
|
|
@@ -52,44 +53,14 @@ function parseAgentBudget(agentPath) {
|
|
|
52
53
|
return { required: [], optional: [], maxTokens: null };
|
|
53
54
|
}
|
|
54
55
|
const raw = fs.readFileSync(agentPath, 'utf8');
|
|
55
|
-
|
|
56
|
-
if (raw.startsWith('---')) {
|
|
57
|
-
const end = raw.indexOf('\n---', 3);
|
|
58
|
-
if (end > 0) fmText = raw.slice(3, end);
|
|
59
|
-
}
|
|
60
|
-
|
|
56
|
+
const metadata = frontmatter.parse(raw, { strict: true, source: agentPath });
|
|
61
57
|
const out = { required: [], optional: [], maxTokens: null };
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const line = lines[i].trim();
|
|
66
|
-
if (line.startsWith('required-context:')) {
|
|
67
|
-
out.required = parseListInline(line.slice('required-context:'.length), lines, i);
|
|
68
|
-
} else if (line.startsWith('optional-context:')) {
|
|
69
|
-
out.optional = parseListInline(line.slice('optional-context:'.length), lines, i);
|
|
70
|
-
} else if (line.startsWith('max-tokens:')) {
|
|
71
|
-
const n = parseInt(line.slice('max-tokens:'.length).trim(), 10);
|
|
72
|
-
if (Number.isFinite(n)) out.maxTokens = n;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
58
|
+
if (Array.isArray(metadata['required-context'])) out.required = metadata['required-context'];
|
|
59
|
+
if (Array.isArray(metadata['optional-context'])) out.optional = metadata['optional-context'];
|
|
60
|
+
if (Number.isFinite(metadata['max-tokens'])) out.maxTokens = metadata['max-tokens'];
|
|
75
61
|
return out;
|
|
76
62
|
}
|
|
77
63
|
|
|
78
|
-
function parseListInline(rest, lines, idx) {
|
|
79
|
-
const r = rest.trim();
|
|
80
|
-
if (r.startsWith('[') && r.endsWith(']')) {
|
|
81
|
-
return r.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
|
|
82
|
-
}
|
|
83
|
-
// Block list: subsequent lines starting with '- '
|
|
84
|
-
const items = [];
|
|
85
|
-
for (let j = idx + 1; j < lines.length; j++) {
|
|
86
|
-
const l = lines[j];
|
|
87
|
-
if (!l.startsWith(' - ')) break;
|
|
88
|
-
items.push(l.slice(4).trim().replace(/^["']|["']$/g, ''));
|
|
89
|
-
}
|
|
90
|
-
return items;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
64
|
/**
|
|
94
65
|
* Compute total bytes + estimated tokens for a list of file paths.
|
|
95
66
|
* Missing files contribute 0; not an error.
|
package/lib/design-spec.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* lint(content) -> { findings, valid } // run all checks
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
const
|
|
18
|
+
const frontmatter = require('./frontmatter');
|
|
19
19
|
|
|
20
20
|
const VALID_SECTIONS = [
|
|
21
21
|
'Overview', 'Brand & Style',
|
|
@@ -48,23 +48,21 @@ const COMPONENT_PROPS = [
|
|
|
48
48
|
* Parse a DESIGN.md file: separate YAML frontmatter from markdown body.
|
|
49
49
|
*/
|
|
50
50
|
function parse(content) {
|
|
51
|
-
const errors = [];
|
|
52
51
|
if (!content.startsWith('---')) {
|
|
53
52
|
return { frontmatter: null, body: content, errors: ['Missing YAML frontmatter (file must start with `---`).'] };
|
|
54
53
|
}
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
54
|
+
const parsed = frontmatter.split(content, { strict: true, source: 'DESIGN.md' });
|
|
55
|
+
if (!parsed.frontmatter) {
|
|
57
56
|
return { frontmatter: null, body: content, errors: ['Frontmatter not closed (missing closing `---`).'] };
|
|
58
57
|
}
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
frontmatter
|
|
64
|
-
|
|
65
|
-
errors
|
|
66
|
-
}
|
|
67
|
-
return { frontmatter, body, errors };
|
|
58
|
+
const errors = parsed.diagnostics.map((diagnostic) =>
|
|
59
|
+
`Frontmatter YAML warning: ${diagnostic.message} on line ${diagnostic.line}`
|
|
60
|
+
);
|
|
61
|
+
return {
|
|
62
|
+
frontmatter: errors.length > 0 ? null : parsed.frontmatter,
|
|
63
|
+
body: parsed.body.trim(),
|
|
64
|
+
errors
|
|
65
|
+
};
|
|
68
66
|
}
|
|
69
67
|
|
|
70
68
|
/**
|
package/lib/extensions.js
CHANGED
|
@@ -41,8 +41,18 @@ function parseManifest(yamlText) {
|
|
|
41
41
|
return { manifest: null, errors: ['empty manifest'] };
|
|
42
42
|
}
|
|
43
43
|
try {
|
|
44
|
-
const
|
|
45
|
-
|
|
44
|
+
const parsed = intentLib.parseSimpleYamlWithDiagnostics(yamlText, {
|
|
45
|
+
strict: true,
|
|
46
|
+
source: 'manifest.yaml',
|
|
47
|
+
unsafeKeySeverity: 'error'
|
|
48
|
+
});
|
|
49
|
+
if (parsed.diagnostics.length > 0) {
|
|
50
|
+
return {
|
|
51
|
+
manifest: null,
|
|
52
|
+
errors: parsed.diagnostics.map(intentLib.formatDiagnostic)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return { manifest: parsed.data, errors: [] };
|
|
46
56
|
} catch (e) {
|
|
47
57
|
return { manifest: null, errors: ['parse error: ' + e.message] };
|
|
48
58
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared YAML frontmatter helpers for markdown-backed Godpowers contracts.
|
|
3
|
+
*
|
|
4
|
+
* Frontmatter uses the same dependency-free YAML subset as intent, routing,
|
|
5
|
+
* recipes, workflows, and extension manifests.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { parseSimpleYamlWithDiagnostics } = require('./intent');
|
|
9
|
+
|
|
10
|
+
function hasOpeningFence(text) {
|
|
11
|
+
return typeof text === 'string' && text.startsWith('---');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function split(text, opts = {}) {
|
|
15
|
+
const source = opts.source || null;
|
|
16
|
+
if (!hasOpeningFence(text)) {
|
|
17
|
+
return {
|
|
18
|
+
frontmatter: null,
|
|
19
|
+
body: text,
|
|
20
|
+
rawFrontmatter: '',
|
|
21
|
+
diagnostics: opts.require ? [{
|
|
22
|
+
severity: 'warning',
|
|
23
|
+
line: 1,
|
|
24
|
+
source,
|
|
25
|
+
message: 'Missing YAML frontmatter fence'
|
|
26
|
+
}] : []
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const end = text.indexOf('\n---', 3);
|
|
31
|
+
if (end === -1) {
|
|
32
|
+
return {
|
|
33
|
+
frontmatter: null,
|
|
34
|
+
body: text,
|
|
35
|
+
rawFrontmatter: '',
|
|
36
|
+
diagnostics: [{
|
|
37
|
+
severity: 'warning',
|
|
38
|
+
line: 1,
|
|
39
|
+
source,
|
|
40
|
+
message: 'YAML frontmatter fence is not closed'
|
|
41
|
+
}]
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const rawFrontmatter = text.slice(3, end).trim();
|
|
46
|
+
const parsed = parseSimpleYamlWithDiagnostics(rawFrontmatter, {
|
|
47
|
+
strict: opts.strict === true,
|
|
48
|
+
source,
|
|
49
|
+
unsafeKeySeverity: opts.unsafeKeySeverity || 'warning',
|
|
50
|
+
onDiagnostic: opts.onDiagnostic
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
frontmatter: parsed.data,
|
|
55
|
+
body: text.slice(end + 4).trimStart(),
|
|
56
|
+
rawFrontmatter,
|
|
57
|
+
diagnostics: parsed.diagnostics
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parse(text, opts = {}) {
|
|
62
|
+
const result = split(text, opts);
|
|
63
|
+
return result.frontmatter || {};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function strip(text) {
|
|
67
|
+
return split(text).body.trim();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
split,
|
|
72
|
+
parse,
|
|
73
|
+
strip
|
|
74
|
+
};
|
package/lib/installer-core.js
CHANGED
|
@@ -5,6 +5,7 @@ const { ensureDir, copyRecursive, copyRuntimeBundle } = require('./installer-fil
|
|
|
5
5
|
const { resolveRuntime } = require('./installer-runtimes');
|
|
6
6
|
const { selectedSkillNames, normalizeProfiles } = require('./install-profiles');
|
|
7
7
|
const identity = require('./package-identity');
|
|
8
|
+
const frontmatter = require('./frontmatter');
|
|
8
9
|
|
|
9
10
|
const VERSION = identity.PACKAGE_VERSION;
|
|
10
11
|
|
|
@@ -46,52 +47,6 @@ function installSkillFile(srcFile, skillsDest, runtimeKey, targetName = null) {
|
|
|
46
47
|
fs.copyFileSync(srcFile, path.join(skillsDest, `${baseName}.md`));
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
function parseAgentFrontmatter(content) {
|
|
50
|
-
const fallback = { name: null, description: null };
|
|
51
|
-
if (!content.startsWith('---\n')) return fallback;
|
|
52
|
-
|
|
53
|
-
const end = content.indexOf('\n---', 4);
|
|
54
|
-
if (end === -1) return fallback;
|
|
55
|
-
|
|
56
|
-
const lines = content.slice(4, end).split('\n');
|
|
57
|
-
const parsed = { ...fallback };
|
|
58
|
-
|
|
59
|
-
for (let i = 0; i < lines.length; i++) {
|
|
60
|
-
const line = lines[i];
|
|
61
|
-
const nameMatch = line.match(/^name:\s*(.+)\s*$/);
|
|
62
|
-
if (nameMatch) {
|
|
63
|
-
parsed.name = nameMatch[1].replace(/^["']|["']$/g, '');
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (line === 'description: |') {
|
|
68
|
-
const desc = [];
|
|
69
|
-
i++;
|
|
70
|
-
while (i < lines.length && /^ {2}/.test(lines[i])) {
|
|
71
|
-
desc.push(lines[i].slice(2));
|
|
72
|
-
i++;
|
|
73
|
-
}
|
|
74
|
-
i--;
|
|
75
|
-
parsed.description = desc.join('\n').trim();
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const descMatch = line.match(/^description:\s*(.+)\s*$/);
|
|
80
|
-
if (descMatch) {
|
|
81
|
-
parsed.description = descMatch[1].replace(/^["']|["']$/g, '');
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return parsed;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function stripFrontmatter(content) {
|
|
89
|
-
if (!content.startsWith('---\n')) return content.trim();
|
|
90
|
-
const end = content.indexOf('\n---', 4);
|
|
91
|
-
if (end === -1) return content.trim();
|
|
92
|
-
return content.slice(end + 4).trim();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
50
|
function tomlString(value) {
|
|
96
51
|
return JSON.stringify(value || '');
|
|
97
52
|
}
|
|
@@ -102,10 +57,10 @@ function tomlLiteral(value) {
|
|
|
102
57
|
|
|
103
58
|
function writeCodexAgentToml(srcFile, agentsDest) {
|
|
104
59
|
const content = fs.readFileSync(srcFile, 'utf8');
|
|
105
|
-
const
|
|
106
|
-
const name =
|
|
107
|
-
const description =
|
|
108
|
-
const instructions =
|
|
60
|
+
const metadata = frontmatter.parse(content, { strict: true, source: srcFile });
|
|
61
|
+
const name = metadata.name || path.basename(srcFile, '.md');
|
|
62
|
+
const description = metadata.description || `Godpowers specialist agent: ${name}.`;
|
|
63
|
+
const instructions = frontmatter.strip(content);
|
|
109
64
|
const toml = [
|
|
110
65
|
`name = ${tomlString(name)}`,
|
|
111
66
|
`description = ${tomlString(description)}`,
|
|
@@ -371,8 +326,8 @@ module.exports = {
|
|
|
371
326
|
uninstallForRuntime,
|
|
372
327
|
countInstalledSurface: countProfileSurface,
|
|
373
328
|
installSkillFile,
|
|
374
|
-
parseAgentFrontmatter,
|
|
375
|
-
stripFrontmatter,
|
|
329
|
+
parseAgentFrontmatter: frontmatter.parse,
|
|
330
|
+
stripFrontmatter: frontmatter.strip,
|
|
376
331
|
writeCodexAgentToml,
|
|
377
332
|
removeSkillEntry,
|
|
378
333
|
pruneGodpowersSkills,
|
package/lib/intent.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Note: this is a minimal YAML reader, intentionally avoiding a YAML
|
|
7
7
|
* dependency. Handles the subset of YAML our intent files use.
|
|
8
|
+
* Strict callers can collect diagnostics for skipped lines and unsafe keys.
|
|
8
9
|
* For complex YAML, agents read the file directly.
|
|
9
10
|
*/
|
|
10
11
|
|
|
@@ -66,15 +67,59 @@ function isUnsafeKey(key) {
|
|
|
66
67
|
* Parse a simple YAML subset. Just enough for intent.yaml structure.
|
|
67
68
|
* Real-world: replace with `yaml` npm package when we add deps.
|
|
68
69
|
*/
|
|
69
|
-
function
|
|
70
|
+
function createDiagnostic(severity, line, message, source) {
|
|
71
|
+
return {
|
|
72
|
+
severity,
|
|
73
|
+
line,
|
|
74
|
+
source: source || null,
|
|
75
|
+
message
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatDiagnostic(diagnostic) {
|
|
80
|
+
const source = diagnostic.source ? `${diagnostic.source}:` : '';
|
|
81
|
+
return `${source}${diagnostic.line}: ${diagnostic.message}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function diagnosticsError(diagnostics, label = 'YAML diagnostics') {
|
|
85
|
+
const error = new Error(`${label}:\n - ${diagnostics.map(formatDiagnostic).join('\n - ')}`);
|
|
86
|
+
error.diagnostics = diagnostics;
|
|
87
|
+
return error;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseSimpleYaml(content, opts = {}) {
|
|
91
|
+
const result = parseSimpleYamlWithDiagnostics(content, opts);
|
|
92
|
+
if (opts.throwOnDiagnostics && result.diagnostics.length > 0) {
|
|
93
|
+
throw diagnosticsError(result.diagnostics, opts.errorLabel);
|
|
94
|
+
}
|
|
95
|
+
return result.data;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseSimpleYamlWithDiagnostics(content, opts = {}) {
|
|
70
99
|
const lines = content.split('\n');
|
|
71
100
|
const result = {};
|
|
72
101
|
const stack = [{ obj: result, indent: -1, key: null, isArray: false, parent: null }];
|
|
102
|
+
const diagnostics = [];
|
|
103
|
+
const strict = opts.strict === true;
|
|
104
|
+
const source = opts.source || null;
|
|
105
|
+
const unsafeKeySeverity = opts.unsafeKeySeverity || 'warning';
|
|
106
|
+
|
|
107
|
+
function record(severity, lineNumber, message) {
|
|
108
|
+
const diagnostic = createDiagnostic(severity, lineNumber, message, source);
|
|
109
|
+
diagnostics.push(diagnostic);
|
|
110
|
+
if (typeof opts.onDiagnostic === 'function') opts.onDiagnostic(diagnostic);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function recordSkipped(lineNumber, rawLine, reason) {
|
|
114
|
+
if (!strict) return;
|
|
115
|
+
record('warning', lineNumber, `${reason}: ${rawLine.trim()}`);
|
|
116
|
+
}
|
|
73
117
|
|
|
74
118
|
for (let i = 0; i < lines.length; i++) {
|
|
75
119
|
let line = lines[i];
|
|
76
120
|
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
77
121
|
line = stripInlineComment(line);
|
|
122
|
+
if (!line.trim()) continue;
|
|
78
123
|
|
|
79
124
|
const indent = line.length - line.trimStart().length;
|
|
80
125
|
const trimmed = line.trim();
|
|
@@ -84,6 +129,16 @@ function parseSimpleYaml(content) {
|
|
|
84
129
|
stack.pop();
|
|
85
130
|
}
|
|
86
131
|
const parent = stack[stack.length - 1].obj;
|
|
132
|
+
|
|
133
|
+
if (trimmed === '[]') {
|
|
134
|
+
const current = stack[stack.length - 1];
|
|
135
|
+
if (current.parent && current.key && Object.keys(current.obj).length === 0) {
|
|
136
|
+
current.parent[current.key] = [];
|
|
137
|
+
stack.pop();
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
87
142
|
// List item: "- key: value" or "- value"
|
|
88
143
|
if (trimmed.startsWith('- ')) {
|
|
89
144
|
const rest = trimmed.slice(2);
|
|
@@ -102,7 +157,14 @@ function parseSimpleYaml(content) {
|
|
|
102
157
|
} else {
|
|
103
158
|
// List of objects: "- key: value"
|
|
104
159
|
const itemKey = rest.slice(0, restColonIdx).trim();
|
|
105
|
-
if (
|
|
160
|
+
if (!itemKey) {
|
|
161
|
+
recordSkipped(i + 1, lines[i], 'Missing list item key');
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (isUnsafeKey(itemKey)) {
|
|
165
|
+
record(unsafeKeySeverity, i + 1, `Unsafe YAML key rejected: ${itemKey}`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
106
168
|
const itemVal = rest.slice(restColonIdx + 1).trim();
|
|
107
169
|
const newObj = {};
|
|
108
170
|
if (itemVal) {
|
|
@@ -122,9 +184,19 @@ function parseSimpleYaml(content) {
|
|
|
122
184
|
}
|
|
123
185
|
|
|
124
186
|
const colonIdx = findUnquotedColon(trimmed);
|
|
125
|
-
if (colonIdx === -1)
|
|
187
|
+
if (colonIdx === -1) {
|
|
188
|
+
recordSkipped(i + 1, lines[i], 'Unparseable YAML line skipped');
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
126
191
|
const key = trimmed.slice(0, colonIdx).trim();
|
|
127
|
-
if (
|
|
192
|
+
if (!key) {
|
|
193
|
+
recordSkipped(i + 1, lines[i], 'Missing YAML key');
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (isUnsafeKey(key)) {
|
|
197
|
+
record(unsafeKeySeverity, i + 1, `Unsafe YAML key rejected: ${key}`);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
128
200
|
const valueStr = trimmed.slice(colonIdx + 1).trim();
|
|
129
201
|
|
|
130
202
|
if (!valueStr) {
|
|
@@ -140,7 +212,7 @@ function parseSimpleYaml(content) {
|
|
|
140
212
|
}
|
|
141
213
|
}
|
|
142
214
|
|
|
143
|
-
return cleanArrays(result);
|
|
215
|
+
return { data: cleanArrays(result), diagnostics };
|
|
144
216
|
}
|
|
145
217
|
|
|
146
218
|
function readBlockScalar(lines, startIndex, parentIndent, folded) {
|
|
@@ -301,4 +373,14 @@ function validate(intent) {
|
|
|
301
373
|
return errors;
|
|
302
374
|
}
|
|
303
375
|
|
|
304
|
-
module.exports = {
|
|
376
|
+
module.exports = {
|
|
377
|
+
read,
|
|
378
|
+
readAsync,
|
|
379
|
+
get,
|
|
380
|
+
validate,
|
|
381
|
+
intentPath,
|
|
382
|
+
parseSimpleYaml,
|
|
383
|
+
parseSimpleYamlWithDiagnostics,
|
|
384
|
+
diagnosticsError,
|
|
385
|
+
formatDiagnostic
|
|
386
|
+
};
|
package/lib/pillars.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
|
+
const frontmatterLib = require('./frontmatter');
|
|
11
12
|
|
|
12
13
|
const PILLARS_FENCE_BEGIN = '<!-- pillars:begin -->';
|
|
13
14
|
const PILLARS_FENCE_END = '<!-- pillars:end -->';
|
|
@@ -119,47 +120,7 @@ function stripQuotes(value) {
|
|
|
119
120
|
return String(value).trim().replace(/^['"]|['"]$/g, '');
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
|
|
123
|
-
const trimmed = value.trim();
|
|
124
|
-
if (trimmed === '[]') return [];
|
|
125
|
-
if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) return null;
|
|
126
|
-
const body = trimmed.slice(1, -1).trim();
|
|
127
|
-
if (!body) return [];
|
|
128
|
-
return body.split(',').map(part => stripQuotes(part)).filter(Boolean);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function parseScalar(value) {
|
|
132
|
-
const trimmed = value.trim();
|
|
133
|
-
if (trimmed === 'true') return true;
|
|
134
|
-
if (trimmed === 'false') return false;
|
|
135
|
-
const list = parseInlineList(trimmed);
|
|
136
|
-
if (list) return list;
|
|
137
|
-
return stripQuotes(trimmed);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function parseFrontmatter(raw) {
|
|
141
|
-
if (!raw.startsWith('---\n')) return null;
|
|
142
|
-
const end = raw.indexOf('\n---', 4);
|
|
143
|
-
if (end === -1) return null;
|
|
144
|
-
|
|
145
|
-
const frontmatter = {};
|
|
146
|
-
const lines = raw.slice(4, end).split('\n');
|
|
147
|
-
let currentKey = null;
|
|
148
|
-
for (const line of lines) {
|
|
149
|
-
const match = line.match(/^([\w-]+):\s*(.*)$/);
|
|
150
|
-
if (match) {
|
|
151
|
-
currentKey = match[1];
|
|
152
|
-
frontmatter[currentKey] = parseScalar(match[2]);
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
const itemMatch = line.match(/^\s*-\s*(.+)$/);
|
|
156
|
-
if (currentKey && itemMatch) {
|
|
157
|
-
if (!Array.isArray(frontmatter[currentKey])) frontmatter[currentKey] = [];
|
|
158
|
-
frontmatter[currentKey].push(stripQuotes(itemMatch[1]));
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return frontmatter;
|
|
162
|
-
}
|
|
123
|
+
const parseFrontmatter = (raw) => frontmatterLib.parse(raw, { strict: true });
|
|
163
124
|
|
|
164
125
|
function walkMarkdown(dir) {
|
|
165
126
|
if (!fs.existsSync(dir)) return [];
|
package/lib/quick-proof.js
CHANGED
|
@@ -10,6 +10,7 @@ const path = require('path');
|
|
|
10
10
|
|
|
11
11
|
const dashboard = require('./dashboard');
|
|
12
12
|
const hostCapabilities = require('./host-capabilities');
|
|
13
|
+
const adoptionMetrics = require('./adoption-metrics');
|
|
13
14
|
|
|
14
15
|
const FIXTURE_ROOT = path.join(__dirname, '..', 'fixtures', 'quick-proof', 'project');
|
|
15
16
|
const MANIFEST_PATH = path.join(__dirname, '..', 'fixtures', 'quick-proof', 'manifest.json');
|
|
@@ -85,6 +86,8 @@ function compute(projectRoot = process.cwd(), opts = {}) {
|
|
|
85
86
|
]
|
|
86
87
|
};
|
|
87
88
|
|
|
89
|
+
proof.metrics = adoptionMetrics.fromQuickProof(proof);
|
|
90
|
+
|
|
88
91
|
return proof;
|
|
89
92
|
}
|
|
90
93
|
|
|
@@ -111,7 +114,10 @@ function render(proof, opts = {}) {
|
|
|
111
114
|
` State on disk: ${proof.statePath}`,
|
|
112
115
|
` Fixture: ${proof.fixturePath}`,
|
|
113
116
|
` PRD: ${planning.prd ? planning.prd.status : 'unknown'}`,
|
|
114
|
-
` Roadmap: ${planning.roadmap ? planning.roadmap.status : 'unknown'}
|
|
117
|
+
` Roadmap: ${planning.roadmap ? planning.roadmap.status : 'unknown'}`,
|
|
118
|
+
'',
|
|
119
|
+
'Outcome metrics:',
|
|
120
|
+
adoptionMetrics.render(proof.metrics)
|
|
115
121
|
].join('\n');
|
|
116
122
|
}
|
|
117
123
|
|
|
@@ -138,6 +144,9 @@ function render(proof, opts = {}) {
|
|
|
138
144
|
'Evidence:',
|
|
139
145
|
...proof.evidence.map((item, index) => ` ${index + 1}. ${item.label}: ${item.value}`),
|
|
140
146
|
'',
|
|
147
|
+
'Outcome metrics:',
|
|
148
|
+
adoptionMetrics.render(proof.metrics),
|
|
149
|
+
'',
|
|
141
150
|
'Try it on the fixture:',
|
|
142
151
|
` npx godpowers status --project=${proof.fixtureRoot} --brief`,
|
|
143
152
|
` npx godpowers next --project=${proof.fixtureRoot} --brief`,
|
package/lib/recipes.js
CHANGED
|
@@ -8,13 +8,17 @@
|
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
|
-
const { parseSimpleYaml } = require('./intent');
|
|
11
|
+
const { parseSimpleYaml, formatDiagnostic } = require('./intent');
|
|
12
12
|
const state = require('./state');
|
|
13
13
|
|
|
14
14
|
const RECIPES_DIR = path.join(__dirname, '..', 'routing', 'recipes');
|
|
15
15
|
|
|
16
16
|
let _cache = null;
|
|
17
17
|
|
|
18
|
+
function warnYamlDiagnostic(diagnostic) {
|
|
19
|
+
console.warn(`[godpowers] YAML warning ${formatDiagnostic(diagnostic)}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
18
22
|
/**
|
|
19
23
|
* Load all recipe definitions.
|
|
20
24
|
*/
|
|
@@ -26,7 +30,11 @@ function loadAll() {
|
|
|
26
30
|
for (const file of fs.readdirSync(RECIPES_DIR)) {
|
|
27
31
|
if (!file.endsWith('.yaml') && !file.endsWith('.yml')) continue;
|
|
28
32
|
const content = fs.readFileSync(path.join(RECIPES_DIR, file), 'utf8');
|
|
29
|
-
const parsed = parseSimpleYaml(content
|
|
33
|
+
const parsed = parseSimpleYaml(content, {
|
|
34
|
+
strict: true,
|
|
35
|
+
source: path.join('routing', 'recipes', file),
|
|
36
|
+
onDiagnostic: warnYamlDiagnostic
|
|
37
|
+
});
|
|
30
38
|
if (parsed.metadata && parsed.metadata.name) {
|
|
31
39
|
result.push(parsed);
|
|
32
40
|
}
|
package/lib/router.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
|
-
const { parseSimpleYaml } = require('./intent');
|
|
13
|
+
const { parseSimpleYaml, formatDiagnostic } = require('./intent');
|
|
14
14
|
const state = require('./state');
|
|
15
15
|
const commandFamilies = require('./command-families');
|
|
16
16
|
|
|
@@ -24,6 +24,10 @@ const HARDEN_FINDINGS = '.godpowers/harden/FINDINGS.md';
|
|
|
24
24
|
|
|
25
25
|
let _cache = null;
|
|
26
26
|
|
|
27
|
+
function warnYamlDiagnostic(diagnostic) {
|
|
28
|
+
console.warn(`[godpowers] YAML warning ${formatDiagnostic(diagnostic)}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
27
31
|
/**
|
|
28
32
|
* Load all routing files into a map keyed by command.
|
|
29
33
|
*/
|
|
@@ -35,7 +39,11 @@ function loadAll() {
|
|
|
35
39
|
for (const file of fs.readdirSync(ROUTING_DIR)) {
|
|
36
40
|
if (!file.endsWith('.yaml')) continue;
|
|
37
41
|
const content = fs.readFileSync(path.join(ROUTING_DIR, file), 'utf8');
|
|
38
|
-
const parsed = parseSimpleYaml(content
|
|
42
|
+
const parsed = parseSimpleYaml(content, {
|
|
43
|
+
strict: true,
|
|
44
|
+
source: path.join('routing', file),
|
|
45
|
+
onDiagnostic: warnYamlDiagnostic
|
|
46
|
+
});
|
|
39
47
|
if (parsed.metadata && parsed.metadata.command) {
|
|
40
48
|
result[parsed.metadata.command] = parsed;
|
|
41
49
|
}
|
package/lib/skill-surface.js
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const frontmatter = require('./frontmatter');
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
if (!text.startsWith('---\n')) return {};
|
|
6
|
-
const end = text.indexOf('\n---', 4);
|
|
7
|
-
if (end === -1) return {};
|
|
8
|
-
const out = {};
|
|
9
|
-
for (const line of text.slice(4, end).split('\n')) {
|
|
10
|
-
const match = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/);
|
|
11
|
-
if (match) out[match[1]] = match[2].replace(/^["']|["']$/g, '');
|
|
12
|
-
}
|
|
13
|
-
return out;
|
|
14
|
-
}
|
|
5
|
+
const parseFrontmatter = (text) => frontmatter.parse(text, { strict: true });
|
|
15
6
|
|
|
16
7
|
function listSkills(rootDir = path.join(__dirname, '..', 'skills')) {
|
|
17
8
|
return fs.readdirSync(rootDir)
|
package/lib/workflow-parser.js
CHANGED
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
|
-
const { parseSimpleYaml } = require('./intent');
|
|
10
|
+
const { parseSimpleYaml, formatDiagnostic } = require('./intent');
|
|
11
|
+
|
|
12
|
+
function warnYamlDiagnostic(diagnostic) {
|
|
13
|
+
console.warn(`[godpowers] YAML warning ${formatDiagnostic(diagnostic)}`);
|
|
14
|
+
}
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* Parse a workflow YAML file into a structured object.
|
|
@@ -17,14 +21,18 @@ function parseFile(filePath) {
|
|
|
17
21
|
throw new Error(`Workflow not found: ${filePath}`);
|
|
18
22
|
}
|
|
19
23
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
20
|
-
return parse(content);
|
|
24
|
+
return parse(content, filePath);
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
/**
|
|
24
28
|
* Parse YAML content into a workflow object.
|
|
25
29
|
*/
|
|
26
|
-
function parse(yamlContent) {
|
|
27
|
-
const parsed = parseSimpleYaml(yamlContent
|
|
30
|
+
function parse(yamlContent, source = 'workflow.yaml') {
|
|
31
|
+
const parsed = parseSimpleYaml(yamlContent, {
|
|
32
|
+
strict: true,
|
|
33
|
+
source,
|
|
34
|
+
onDiagnostic: warnYamlDiagnostic
|
|
35
|
+
});
|
|
28
36
|
validate(parsed);
|
|
29
37
|
return parsed;
|
|
30
38
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "godpowers",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.2",
|
|
4
4
|
"description": "AI-powered development system: 112 slash commands and 40 specialist agents that take a project from raw idea to hardened production. Runs inside Claude Code, Codex, Cursor, Windsurf, Gemini, and 10+ other AI coding tools.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"godpowers": "./bin/install.js"
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"test:linter": "node scripts/test-artifact-linter.js",
|
|
23
23
|
"test:diff": "node scripts/test-artifact-diff.js",
|
|
24
24
|
"test:e2e": "node tests/integration/full-arc.test.js",
|
|
25
|
+
"coverage": "c8 --reporter=text --reporter=lcov node scripts/run-tests.js",
|
|
25
26
|
"test:audit": "npm audit --omit=dev && git diff --check && npm run test:surface",
|
|
26
27
|
"pack:check": "node scripts/check-package-contents.js",
|
|
27
28
|
"release:check": "npm test && npm run test:audit && npm run pack:check",
|
|
@@ -86,5 +87,8 @@
|
|
|
86
87
|
"AGENTS.md",
|
|
87
88
|
"CHANGELOG.md",
|
|
88
89
|
"LICENSE"
|
|
89
|
-
]
|
|
90
|
+
],
|
|
91
|
+
"devDependencies": {
|
|
92
|
+
"c8": "^11.0.0"
|
|
93
|
+
}
|
|
90
94
|
}
|