@deepseek-insight/dsi 0.5.24 → 0.5.25
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/skills/dsi-adr/SKILL.md +3 -3
- package/.agents/skills/dsi-ov-setup/SKILL.md +6 -1
- package/.agents/skills/dsi-skill-shelf/SKILL.md +46 -0
- package/.agents/skills/dsi-skill-shelf/shelf.mjs +511 -0
- package/.agents/skills/dsi-spec/SKILL.md +4 -4
- package/.agents/skills/dsi-spec/references/tasks-json-template.md +1 -1
- package/build/client/_app/immutable/assets/0.DoqT6ojN.css +2 -0
- package/build/client/_app/immutable/assets/2.DqAZaZ6x.css +1 -0
- package/build/client/_app/immutable/chunks/C1BIbywb.js +1 -0
- package/build/client/_app/immutable/chunks/CN_lPpCU.js +1 -0
- package/build/client/_app/immutable/chunks/{xNyHdy4Z.js → DEg6k2po.js} +1 -1
- package/build/client/_app/immutable/chunks/{BFAx3hqQ.js → QV2fW_C7.js} +2 -2
- package/build/client/_app/immutable/entry/{app.BfVt8pR9.js → app.CkwTkkSM.js} +2 -2
- package/build/client/_app/immutable/entry/start.DxUGjIjy.js +1 -0
- package/build/client/_app/immutable/nodes/1.8K25GLCS.js +1 -0
- package/build/client/_app/immutable/nodes/2.CZgpKf5S.js +171 -0
- package/build/client/_app/version.json +1 -1
- package/build/handler.js +4 -4
- package/build/index.js +4 -4
- package/build/server/chunks/chunks/engine.js-DzJkajy7.js +90 -0
- package/build/server/chunks/chunks/{internal.js-nJTGYhdX.js → internal.js-1R9PHQNf.js} +3 -3
- package/build/server/chunks/chunks/{state.js-D0-EDcsi.js → state.js-vRL4xZQO.js} +163 -3
- package/build/server/chunks/entries/endpoints/api/skills/install/_server.ts.js-Dvoz59-b.js +47 -0
- package/build/server/chunks/entries/endpoints/api/skills/reload/_server.ts.js-fUUcvsn3.js +33 -0
- package/build/server/chunks/entries/endpoints/api/skills/snapshot/_server.ts.js-C4AvWpBK.js +37 -0
- package/build/server/chunks/entries/endpoints/api/skills/uninstall/_server.ts.js-qqFlorKD.js +47 -0
- package/build/server/chunks/entries/endpoints/api/skills/voice/_server.ts.js-yiGBLufB.js +72 -0
- package/build/server/chunks/entries/pages/_error.svelte.js-CtYKjJrf.js +43 -0
- package/build/server/chunks/entries/pages/{_page.svelte.js-D63btYnK.js → _page.svelte.js-CZV29jqi.js} +9625 -3397
- package/build/server/chunks/{handler-DiaR2DmJ.js → handler-BcFykRDA.js} +3 -3
- package/build/server/chunks/{index.js-trwV04zl.js → index.js-CYc5jGBw.js} +2 -2
- package/build/server/chunks/{manifest.js-B2sb3fMn.js → manifest.js-CgPn3jEA.js} +40 -5
- package/build/server/chunks/nodes/{0.js-CcvNYltc.js → 0.js-DO4F8XMq.js} +3 -3
- package/build/server/chunks/nodes/1.js-B3lmNFSp.js +9 -0
- package/build/server/chunks/nodes/{2.js-CEsSSgY6.js → 2.js-DVa5udLN.js} +4 -4
- package/package.json +1 -1
- package/template/.dsi/prompts.sqlite +0 -0
- package/template/.dsi/resources/skills-reff.md +8 -0
- package/build/client/_app/immutable/assets/0.C4pk9RsQ.css +0 -2
- package/build/client/_app/immutable/assets/2.BpERqkk_.css +0 -1
- package/build/client/_app/immutable/chunks/BjB44r24.js +0 -1
- package/build/client/_app/immutable/chunks/as89y6ti.js +0 -1
- package/build/client/_app/immutable/entry/start.BOYIplSn.js +0 -1
- package/build/client/_app/immutable/nodes/1.BZ_iRXtk.js +0 -1
- package/build/client/_app/immutable/nodes/2.BADJoHBB.js +0 -171
- package/build/server/chunks/entries/pages/_error.svelte.js-BTQyW5Df.js +0 -203
- package/build/server/chunks/nodes/1.js-2FULYexO.js +0 -9
- package/template/.dsi/prompts.sqlite-shm +0 -0
- package/template/.dsi/prompts.sqlite-wal +0 -0
- /package/build/client/_app/immutable/nodes/{0.BBoMIehN.js → 0.DlNmLZ8S.js} +0 -0
|
@@ -14,9 +14,9 @@ hard core: the self-audit in §5 is not optional.
|
|
|
14
14
|
|
|
15
15
|
Exemplars, in authority order (paths relative to the DSI repo root):
|
|
16
16
|
|
|
17
|
-
1. `dev/architectural-decission/2026-08-27 - The Lineage Pin — Sub-Agents Below Their Spawner, Swap-Proof.md` — the gold standard: D1–D7, sequence + flowchart + state homes + control matrix, glossary, amendments log.
|
|
18
|
-
2. `dev/architectural-decission/2026-08-24 - The Panel Floor — Many Conversations, One Page.md` — decision set with honest give-ups.
|
|
19
|
-
3. `dev/architectural-decission/2026-08-29 - The Prompt Macro — Bang Runs the Shelf, Line by Line.md` — deep format on a composer-scale feature.
|
|
17
|
+
1. `dev/architectural-decission/2026-08/2026-08-27 - The Lineage Pin — Sub-Agents Below Their Spawner, Swap-Proof.md` — the gold standard: D1–D7, sequence + flowchart + state homes + control matrix, glossary, amendments log.
|
|
18
|
+
2. `dev/architectural-decission/2026-08/2026-08-24 - The Panel Floor — Many Conversations, One Page.md` — decision set with honest give-ups.
|
|
19
|
+
3. `dev/architectural-decission/2026-08/2026-08-29 - The Prompt Macro — Bang Runs the Shelf, Line by Line.md` — deep format on a composer-scale feature.
|
|
20
20
|
|
|
21
21
|
Read the exemplar closest to your subject before drafting a new ADR kind.
|
|
22
22
|
|
|
@@ -89,7 +89,12 @@ Sandbox warning: server startup acquires a PID lock at
|
|
|
89
89
|
dies with `PermissionError` / exit 3 at once. Start the server with full file
|
|
90
90
|
access (same as step 2's CLI housekeeping).
|
|
91
91
|
|
|
92
|
-
For persistence: launchd/
|
|
92
|
+
For persistence: launchd/systemd — never rely on an agent shell outliving the
|
|
93
|
+
session. In a harness session, never start the daemon with `nohup … &` inside a
|
|
94
|
+
**foreground** bash call: the harness reaps the call's process group when the
|
|
95
|
+
call returns and the daemon dies seconds later, health-ok inside the call but
|
|
96
|
+
connection-refused on the next one (RCA 2026-09-21 — The Server That Slept).
|
|
97
|
+
Run it as a harness background job instead. Verify:
|
|
93
98
|
|
|
94
99
|
```sh
|
|
95
100
|
curl -s http://127.0.0.1:1933/health # {"status":"ok","healthy":true,...}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# dsi-skill-shelf
|
|
2
|
+
|
|
3
|
+
Install and uninstall agent skills from the SKR reference (`~/.dsi/resources/skills-reff.md`). Implements the ADR 'The Skill Shelf' (dev/architectural-decission/2026-09/, 2026-09-20).
|
|
4
|
+
|
|
5
|
+
## Engine
|
|
6
|
+
|
|
7
|
+
`shelf.mjs` — plain Node, JSON on stdout, every payload carries `v: 1`.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# build snapshot (reuses existing unless --reload)
|
|
11
|
+
node shelf.mjs refresh [--reload] [--skr PATH] [--cache PATH] [--skills-dir PATH] [--fixture-root PATH]
|
|
12
|
+
|
|
13
|
+
# signed install / uninstall
|
|
14
|
+
node shelf.mjs apply install 1.1 3.1-3.3 # numbers resolved by the caller; engine takes explicit targets
|
|
15
|
+
node shelf.mjs apply install 2.30 --allow-unstable # in-progress tier is opt-in (D6)
|
|
16
|
+
node shelf.mjs apply uninstall <skill-id> # refuses unsigned folders (D5)
|
|
17
|
+
|
|
18
|
+
node shelf.mjs snapshot-status # does the cache exist?
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Rules (do not violate)
|
|
22
|
+
|
|
23
|
+
- **No signature, no uninstall.** The engine hard-refuses to remove any folder without a valid `.dsi-provenance.json` (D5). Never work around this with rm.
|
|
24
|
+
- **No folder without signature.** A failed install cleans up its own folder (D4).
|
|
25
|
+
- **Snapshot-first (D3):** `refresh` reuses an existing cache; only `--reload` rebuilds. Never delete the cache to force a rebuild — pass `--reload`.
|
|
26
|
+
- **Unstable tiers (D6):** in-progress/deprecated skills install only with explicit `--allow-unstable`.
|
|
27
|
+
- **Collisions:** an existing folder aborts the install unless `--force`.
|
|
28
|
+
- Tests MUST use `--fixture-root` + `--cache` + `--skills-dir` overrides — never touch the real home directories from a test.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## Translate at install (The Shelf Voice ADR, D4)
|
|
32
|
+
|
|
33
|
+
An install SHOULD carry a voice: pass `--voice '<json>'` with the skill's name,
|
|
34
|
+
description and whenToUse TRANSLATED BY YOU (the installing agent) into the
|
|
35
|
+
operator's locales (en + zh/id/es as appropriate):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
node shelf.mjs apply install 1.2 --voice '{"zh":{"name":"技能","description":"…","whenToUse":"…"},"id":{…}}'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Rules: translate faithfully, never invent behavior; keep `name` short; an
|
|
42
|
+
invalid payload degrades the install to en-only (never a blocker); `en` is
|
|
43
|
+
always written as the scaffold. Uninstall removes the voice with the folder.
|
|
44
|
+
## Shelf numbering
|
|
45
|
+
|
|
46
|
+
1.x pstack (47), 2.x mattpocock (38: engineering 2.1-2.18, productivity 2.19-2.25, misc 2.26-2.29, in-progress 2.30-2.38), 3.x superpowers (15).
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// dsi-skill-shelf engine — The Skill Shelf (ADR 2026-09-20, D1-D6)
|
|
3
|
+
// Plain Node ESM, no dependencies. All output is JSON on stdout: { v: 1, ok, ... }
|
|
4
|
+
|
|
5
|
+
import { createHash } from 'node:crypto'
|
|
6
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
7
|
+
import { execFileSync } from 'node:child_process'
|
|
8
|
+
import { tmpdir } from 'node:os'
|
|
9
|
+
import { basename, dirname, join, resolve, sep } from 'node:path'
|
|
10
|
+
import { homedir } from 'node:os'
|
|
11
|
+
|
|
12
|
+
const CACHE_VERSION = 1
|
|
13
|
+
const SIGNATURE_NAME = '.dsi-provenance.json'
|
|
14
|
+
const UNSTABLE_TIERS = new Set(['in-progress', 'deprecated'])
|
|
15
|
+
// locales the shelf can voice (mirrors project.inlang settings.json)
|
|
16
|
+
const SHELF_LOCALES = ['en', 'zh', 'id', 'es']
|
|
17
|
+
|
|
18
|
+
// tier display order (ADR D6: in-progress is last, numbers 2.30-2.38)
|
|
19
|
+
const TIER_RANK = { engineering: 0, productivity: 1, misc: 2, 'in-progress': 3 }
|
|
20
|
+
function tierSortKey(sk) { return (sk.tier !== null && sk.tier in TIER_RANK ? TIER_RANK[sk.tier] : 0) + ':' + sk.path }
|
|
21
|
+
|
|
22
|
+
// source name -> UPSTREAM OVERRIDE (2026-09-21 redesign: the SKR is the
|
|
23
|
+
// registry — every github source enumerates from its own URL by default;
|
|
24
|
+
// a row here only OVERRIDES the derivation, e.g. pstack's nested layout).
|
|
25
|
+
// single: true (2026-09-22) = the repo hosts exactly ONE skill at its
|
|
26
|
+
// canonical root (prefix 'skill', entry SKILL.md or SKILL.src.md; the id
|
|
27
|
+
// comes from the frontmatter name). impeccable ships one skill mirrored
|
|
28
|
+
// into ~20 host dirs — prefixless enumeration would yield duplicate rows.
|
|
29
|
+
const SOURCE_SPECS = {
|
|
30
|
+
pstack: { repo: 'cursor/plugins', prefix: 'pstack/skills', tiered: false },
|
|
31
|
+
mattpocock: { repo: 'mattpocock/skills', prefix: 'skills', tiered: true },
|
|
32
|
+
superpowers: { repo: 'obra/superpowers', prefix: 'skills', tiered: false },
|
|
33
|
+
'web-quality-skills': { repo: 'addyosmani/web-quality-skills', prefix: 'skills', tiered: false },
|
|
34
|
+
impeccable: { repo: 'pbakaus/impeccable', prefix: 'skill', tiered: false, single: true }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Derive the enumeration spec from the SKR line itself: the github URL
|
|
38
|
+
// names the repo, an optional tail hints the prefix — either the classic
|
|
39
|
+
// /tree/<branch>/<path> or (2026-09-22, single-skill repos) a bare path
|
|
40
|
+
// like .../repo/skill. A prefix that IS 'skill' or ends in '/skill'
|
|
41
|
+
// marks single-skill mode. SOURCE_SPECS wins when a row exists.
|
|
42
|
+
const SINGLE_SKILL_RE = /(^|\/)skill$/
|
|
43
|
+
function specFor(src) {
|
|
44
|
+
if (SOURCE_SPECS[src.name]) return SOURCE_SPECS[src.name]
|
|
45
|
+
// src.url = the raw SKR line; src.repo = a snapshot source (skrRepo's
|
|
46
|
+
// output). Both carry a github URL the regex can read.
|
|
47
|
+
const url = src.url || src.repo || ''
|
|
48
|
+
const m = url.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/[^/]+(?:\/(.*))?|\/(.+))?$/)
|
|
49
|
+
if (!m) return null
|
|
50
|
+
const prefix = m[3] || m[4] || ''
|
|
51
|
+
return { repo: m[1] + '/' + m[2], prefix, tiered: false, single: SINGLE_SKILL_RE.test(prefix) }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Skill entry files the shelf can enumerate (2026-09-22): the classic
|
|
55
|
+
// SKILL.md and the single-skill repos' authored SKILL.src.md.
|
|
56
|
+
const ENTRY_FILES = ['SKILL.md', 'SKILL.src.md']
|
|
57
|
+
function frontmatterName(text) {
|
|
58
|
+
const fm = text.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
59
|
+
const m = fm && fm[1].match(/^name:\s*(.+)$/m)
|
|
60
|
+
return m ? m[1].trim() : null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseArgs(argv) {
|
|
64
|
+
const args = { _: [] }
|
|
65
|
+
for (let i = 0; i < argv.length; i++) {
|
|
66
|
+
const a = argv[i]
|
|
67
|
+
if (a === '--reload') args.reload = true
|
|
68
|
+
else if (a === '--allow-unstable') args.allowUnstable = true
|
|
69
|
+
else if (a === '--force') args.force = true
|
|
70
|
+
else if (a.startsWith('--')) args[a.slice(2)] = argv[++i]
|
|
71
|
+
else args._.push(a)
|
|
72
|
+
}
|
|
73
|
+
return args
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function fail(errors) {
|
|
77
|
+
console.log(JSON.stringify({ v: CACHE_VERSION, ok: false, errors }))
|
|
78
|
+
process.exit(1)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --- SKR parsing (ADR D2: [name](repo-url) - [author](linkedin)) ---
|
|
82
|
+
export function parseSkr(text) {
|
|
83
|
+
const sources = []
|
|
84
|
+
for (const line of text.split('\n')) {
|
|
85
|
+
const m = line.match(/^\d+\.\s*\[(.+?)\]\((.+?)\)\s*-\s*\[(.+?)\]\((.+)\)\s*$/)
|
|
86
|
+
if (!m) continue
|
|
87
|
+
const name = m[1].trim().toLowerCase()
|
|
88
|
+
sources.push({ name, url: m[2].trim(), author: m[3].trim(), repo: SOURCE_SPECS[name] ? SOURCE_SPECS[name].repo : null })
|
|
89
|
+
}
|
|
90
|
+
return sources
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- enumeration ---
|
|
94
|
+
|
|
95
|
+
function walkLocal(root) {
|
|
96
|
+
const out = []
|
|
97
|
+
const walk = (dir) => {
|
|
98
|
+
for (const entry of readdirSync(dir)) {
|
|
99
|
+
const p = join(dir, entry)
|
|
100
|
+
if (statSync(p).isDirectory()) walk(p)
|
|
101
|
+
else if (entry === 'SKILL.md') out.push(dirname(p))
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
walk(root)
|
|
105
|
+
return out
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function enumLocal(src, fixtureRoot) {
|
|
109
|
+
// fixture tree mirrors the upstream layout under <fixtureRoot>/<sourceName>/
|
|
110
|
+
const spec = specFor(src)
|
|
111
|
+
const root = join(fixtureRoot, src.name)
|
|
112
|
+
if (!spec || !existsSync(root)) return []
|
|
113
|
+
if (spec.single) {
|
|
114
|
+
// Single-skill repo: one entry at <prefix>/, id from the frontmatter
|
|
115
|
+
// name (falls back to the folder name when the entry carries none).
|
|
116
|
+
const dir = join(root, spec.prefix)
|
|
117
|
+
const entry = ENTRY_FILES.find((f) => existsSync(join(dir, f)))
|
|
118
|
+
if (!entry) return []
|
|
119
|
+
let id = basename(spec.prefix)
|
|
120
|
+
try { id = frontmatterName(readFileSync(join(dir, entry), 'utf8')) || id } catch { /* keep folder name */ }
|
|
121
|
+
return [{ id, path: spec.prefix, tier: null, entry }]
|
|
122
|
+
}
|
|
123
|
+
const prefix = spec.prefix
|
|
124
|
+
const tiered = spec.tiered
|
|
125
|
+
const skills = walkLocal(root).map((dir) => {
|
|
126
|
+
const rel = dir.slice(root.length + sep.length)
|
|
127
|
+
const parts = rel.split(sep)
|
|
128
|
+
const id = parts[parts.length - 1]
|
|
129
|
+
let tier = null
|
|
130
|
+
if (tiered && parts.length > 2) tier = parts[1]
|
|
131
|
+
else if (tiered && parts.length === 2) tier = parts[0]
|
|
132
|
+
return { id, path: rel, tier: tier && UNSTABLE_TIERS.has(tier) ? tier : null }
|
|
133
|
+
})
|
|
134
|
+
skills.sort((a, b) => tierSortKey(a).localeCompare(tierSortKey(b)))
|
|
135
|
+
return skills
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// GitHub API auth (2026-09-22): unauthenticated api.github.com allows
|
|
139
|
+
// only 60 req/hr per IP — one exhausted window aborts the whole refresh
|
|
140
|
+
// (observed: HTTP 403 on cursor/plugins). Prefer GITHUB_TOKEN / GH_TOKEN
|
|
141
|
+
// or the gh CLI credential when present (5000 req/hr).
|
|
142
|
+
function ghAuthHeaders() {
|
|
143
|
+
const headers = { 'user-agent': 'dsi-skill-shelf' }
|
|
144
|
+
let token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || ''
|
|
145
|
+
if (!token) {
|
|
146
|
+
try { token = execFileSync('gh', ['auth', 'token'], { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim() } catch { /* no gh CLI */ }
|
|
147
|
+
}
|
|
148
|
+
if (token) headers.authorization = 'Bearer ' + token
|
|
149
|
+
return headers
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// One trees call per source; failure is DATA ({ skills: [], error }),
|
|
153
|
+
// never an exception — one dead source must not abort the whole snapshot.
|
|
154
|
+
async function fetchTree(repo) {
|
|
155
|
+
try {
|
|
156
|
+
const res = await fetch('https://api.github.com/repos/' + repo + '/git/trees/HEAD?recursive=1', { headers: ghAuthHeaders() })
|
|
157
|
+
if (!res.ok) {
|
|
158
|
+
const hint = res.status === 403 ? ' (api.github.com rate limited? set GITHUB_TOKEN or run: gh auth login)' : ''
|
|
159
|
+
return { tree: null, error: 'github trees ' + repo + ' -> HTTP ' + res.status + hint }
|
|
160
|
+
}
|
|
161
|
+
return { tree: (await res.json()).tree || [], error: null }
|
|
162
|
+
} catch (e) {
|
|
163
|
+
return { tree: null, error: 'github trees ' + repo + ' -> ' + String(e.message || e) }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function enumGitHub(src) {
|
|
168
|
+
const spec = specFor(src)
|
|
169
|
+
if (!spec) return { skills: [], error: null }
|
|
170
|
+
const repo = spec.repo
|
|
171
|
+
if (spec.single) {
|
|
172
|
+
// Single-skill repo (2026-09-22): exactly one skill at <prefix>/, so
|
|
173
|
+
// pick the entry file directly instead of scanning every SKILL.md
|
|
174
|
+
// blob (impeccable mirrors its one skill into ~20 host dirs — a tree
|
|
175
|
+
// scan would enumerate duplicates). The id is the frontmatter name.
|
|
176
|
+
const { tree, error } = await fetchTree(repo)
|
|
177
|
+
if (error) return { skills: [], error }
|
|
178
|
+
const entry = ENTRY_FILES.find((f) => tree.some((n) => n.type === 'blob' && n.path === spec.prefix + '/' + f))
|
|
179
|
+
if (!entry) return { skills: [], error: null }
|
|
180
|
+
let id = basename(spec.prefix)
|
|
181
|
+
try {
|
|
182
|
+
const raw = await fetch('https://raw.githubusercontent.com/' + repo + '/HEAD/' + spec.prefix + '/' + entry, { headers: { 'user-agent': 'dsi-skill-shelf' } })
|
|
183
|
+
if (raw.ok) id = frontmatterName(await raw.text()) || id
|
|
184
|
+
} catch { /* keep folder name */ }
|
|
185
|
+
return { skills: [{ id, path: spec.prefix, tier: null, entry }], error: null }
|
|
186
|
+
}
|
|
187
|
+
const { tree, error } = await fetchTree(repo)
|
|
188
|
+
if (error) return { skills: [], error }
|
|
189
|
+
const skills = []
|
|
190
|
+
for (const node of tree) {
|
|
191
|
+
if (node.type !== 'blob') continue
|
|
192
|
+
const parts = node.path.split('/')
|
|
193
|
+
if (!ENTRY_FILES.includes(parts[parts.length - 1])) continue
|
|
194
|
+
if (spec.prefix && !node.path.startsWith(spec.prefix + '/')) continue
|
|
195
|
+
const dirParts = parts.slice(0, -1)
|
|
196
|
+
const id = dirParts[dirParts.length - 1]
|
|
197
|
+
if (!id || id === 'deprecated') continue
|
|
198
|
+
let tier = null
|
|
199
|
+
if (spec.tiered && dirParts.length > 2) tier = dirParts[1]
|
|
200
|
+
else if (spec.tiered && dirParts.length === 2) tier = dirParts[0]
|
|
201
|
+
skills.push({ id, path: dirParts.join('/'), tier: tier && UNSTABLE_TIERS.has(tier) ? tier : null })
|
|
202
|
+
}
|
|
203
|
+
skills.sort((a, b) => tierSortKey(a).localeCompare(tierSortKey(b)))
|
|
204
|
+
return { skills, error: null }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// --- signatures / reconciliation (ADR D5: signature is the uninstall authority) ---
|
|
208
|
+
|
|
209
|
+
export function readSignatures(skillsDir) {
|
|
210
|
+
const signed = new Map()
|
|
211
|
+
if (!existsSync(skillsDir)) return signed
|
|
212
|
+
for (const entry of readdirSync(skillsDir)) {
|
|
213
|
+
const sigPath = join(skillsDir, entry, SIGNATURE_NAME)
|
|
214
|
+
if (!existsSync(sigPath)) continue
|
|
215
|
+
try {
|
|
216
|
+
const sig = JSON.parse(readFileSync(sigPath, 'utf8'))
|
|
217
|
+
if (sig && sig.skillId === entry && sig.v === CACHE_VERSION) signed.set(entry, sig)
|
|
218
|
+
} catch { /* unreadable signature is not authority */ }
|
|
219
|
+
}
|
|
220
|
+
return signed
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Skill overview (card UI, 2026-09-22): prefer the SKILL.md
|
|
224
|
+
// frontmatter `description:` line, else the first paragraph under an
|
|
225
|
+
// `## Overview` heading. Installed skills read from disk; uninstalled
|
|
226
|
+
// ones fetch the raw SKILL.md from the source repo (raw.githubusercontent
|
|
227
|
+
// is outside the API rate limit) so the card never lies about a missing
|
|
228
|
+
// overview. Parse failure still yields null — the honest fallback.
|
|
229
|
+
const OVERVIEW_MAX = 280
|
|
230
|
+
function parseOverview(text) {
|
|
231
|
+
let out = null
|
|
232
|
+
const fm = text.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
233
|
+
if (fm) {
|
|
234
|
+
const desc = fm[1].match(/^description:\s*(.+)$/m)
|
|
235
|
+
if (desc) out = desc[1].trim()
|
|
236
|
+
}
|
|
237
|
+
if (!out) {
|
|
238
|
+
const ov = text.match(/^##\s*Overview\s*\r?\n([\s\S]*?)(?=\n##\s|$)/m)
|
|
239
|
+
if (ov) out = ov[1].trim()
|
|
240
|
+
}
|
|
241
|
+
if (!out) return null
|
|
242
|
+
return out.length > OVERVIEW_MAX ? out.slice(0, OVERVIEW_MAX - 1).trimEnd() + '\u2026' : out
|
|
243
|
+
}
|
|
244
|
+
function readOverview(skillsDir, id, entry) {
|
|
245
|
+
const files = entry ? [entry] : ENTRY_FILES
|
|
246
|
+
for (const f of files) {
|
|
247
|
+
const mdPath = join(skillsDir, id, f)
|
|
248
|
+
if (!existsSync(mdPath)) continue
|
|
249
|
+
try {
|
|
250
|
+
return parseOverview(readFileSync(mdPath, 'utf8'))
|
|
251
|
+
} catch { return null }
|
|
252
|
+
}
|
|
253
|
+
return null
|
|
254
|
+
}
|
|
255
|
+
async function fetchOverview(repo, relPath, entry) {
|
|
256
|
+
const files = entry ? [entry] : ENTRY_FILES
|
|
257
|
+
for (const f of files) {
|
|
258
|
+
try {
|
|
259
|
+
const res = await fetch('https://raw.githubusercontent.com/' + repo + '/HEAD/' + relPath + '/' + f, { headers: { 'user-agent': 'dsi-skill-shelf' } })
|
|
260
|
+
if (!res.ok) continue
|
|
261
|
+
const out = parseOverview(await res.text())
|
|
262
|
+
if (out) return out
|
|
263
|
+
} catch { /* try next entry file */ }
|
|
264
|
+
}
|
|
265
|
+
return null
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function buildSnapshot(sourcesRaw, enums, skillsDir) {
|
|
269
|
+
const signed = readSignatures(skillsDir)
|
|
270
|
+
const generatedAt = new Date().toISOString()
|
|
271
|
+
const warnings = []
|
|
272
|
+
const sources = []
|
|
273
|
+
for (let si = 0; si < sourcesRaw.length; si++) { const src = sourcesRaw[si]
|
|
274
|
+
// Registry-gap warning (2026-09-21, revised): enumeration derives
|
|
275
|
+
// from the SKR URL itself, so an empty list is a REAL anomaly and
|
|
276
|
+
// the snapshot says why.
|
|
277
|
+
const spec = specFor(src)
|
|
278
|
+
const enumed = enums[si] || { skills: [], error: null }
|
|
279
|
+
if (!spec) {
|
|
280
|
+
warnings.push({ code: 'source-no-repo', source: src.name, detail: 'source "' + src.name + '" has no github repo URL in the SKR - cannot enumerate' })
|
|
281
|
+
} else if (enumed.error) {
|
|
282
|
+
// Enumeration failure (rate limit, network) degrades to a warning —
|
|
283
|
+
// the rest of the shelf still lands in the snapshot (2026-09-22).
|
|
284
|
+
warnings.push({ code: 'source-enum-failed', source: src.name, detail: enumed.error })
|
|
285
|
+
} else if (enumed.skills.length === 0) {
|
|
286
|
+
warnings.push({ code: 'source-empty', source: src.name, detail: 'source "' + src.name + '" enumerated zero skills' + (spec.prefix ? ' under prefix "' + spec.prefix + '"' : '') + ' - upstream layout may have moved' })
|
|
287
|
+
}
|
|
288
|
+
const skills = []
|
|
289
|
+
for (let ki = 0; ki < enumed.skills.length; ki++) {
|
|
290
|
+
const sk = enumed.skills[ki]
|
|
291
|
+
const installed = existsSync(join(skillsDir, sk.id))
|
|
292
|
+
// Overview: disk first, then the source repo's raw SKILL.md for
|
|
293
|
+
// uninstalled rows (sequential — gentle on raw.githubusercontent).
|
|
294
|
+
let overview = readOverview(skillsDir, sk.id, sk.entry)
|
|
295
|
+
if (!overview && spec) overview = await fetchOverview(spec.repo, sk.path, sk.entry)
|
|
296
|
+
skills.push({
|
|
297
|
+
n: (si + 1) + '.' + (ki + 1),
|
|
298
|
+
id: sk.id,
|
|
299
|
+
path: sk.path,
|
|
300
|
+
tier: sk.tier,
|
|
301
|
+
installed,
|
|
302
|
+
signed: signed.has(sk.id),
|
|
303
|
+
installedFrom: signed.has(sk.id) ? signed.get(sk.id).source : null,
|
|
304
|
+
overview
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
sources.push({ id: src.name, name: src.name, author: src.author, repo: skrRepo(src), skills })
|
|
308
|
+
}
|
|
309
|
+
return { v: CACHE_VERSION, generatedAt, sources, warnings }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function skrRepo(src) {
|
|
313
|
+
const spec = specFor(src)
|
|
314
|
+
return spec ? 'https://github.com/' + spec.repo + (spec.prefix ? '/tree/main/' + spec.prefix : '') : src.url
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// --- refresh ---
|
|
318
|
+
|
|
319
|
+
async function cmdRefresh(args) {
|
|
320
|
+
const skrPath = resolve(args.skr || join(homedir(), '.dsi/resources/skills-reff.md'))
|
|
321
|
+
const cachePath = resolve(args.cache || join(homedir(), '.dsi/resources/skr-cache.json'))
|
|
322
|
+
const skillsDir = resolve(args['skills-dir'] || join(homedir(), '.agents/skills'))
|
|
323
|
+
if (!args.reload && existsSync(cachePath)) {
|
|
324
|
+
return { snapshot: JSON.parse(readFileSync(cachePath, 'utf8')), reused: true }
|
|
325
|
+
}
|
|
326
|
+
const sourcesRaw = parseSkr(readFileSync(skrPath, 'utf8'))
|
|
327
|
+
if (sourcesRaw.length === 0) fail(['SKR parsed to zero sources: ' + skrPath])
|
|
328
|
+
const fixtureRoot = args['fixture-root'] ? resolve(args['fixture-root']) : null
|
|
329
|
+
const enums = []
|
|
330
|
+
for (const src of sourcesRaw) {
|
|
331
|
+
enums.push(fixtureRoot ? { skills: enumLocal(src, fixtureRoot), error: null } : await enumGitHub(src))
|
|
332
|
+
}
|
|
333
|
+
const snapshot = await buildSnapshot(sourcesRaw, enums, skillsDir)
|
|
334
|
+
mkdirSync(dirname(cachePath), { recursive: true })
|
|
335
|
+
writeFileSync(cachePath, JSON.stringify(snapshot, null, 2))
|
|
336
|
+
return { snapshot, reused: false }
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// --- signed apply (ADR D4 + D5) ---
|
|
340
|
+
|
|
341
|
+
function findSkill(snapshot, n) {
|
|
342
|
+
for (const src of snapshot.sources) {
|
|
343
|
+
const sk = src.skills.find((s) => s.n === n || s.id === n)
|
|
344
|
+
if (sk) return { source: src, skill: sk }
|
|
345
|
+
}
|
|
346
|
+
return null
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function copyDirLocal(fixtureRoot, sourceName, relPath, dest) {
|
|
350
|
+
const srcDir = join(fixtureRoot, sourceName, relPath)
|
|
351
|
+
if (!existsSync(srcDir)) throw new Error('fixture skill dir missing: ' + srcDir)
|
|
352
|
+
cpSync(srcDir, dest, { recursive: true })
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function copyDirGitHub(source, relPath, dest) {
|
|
356
|
+
const spec = specFor(source)
|
|
357
|
+
if (!spec) throw new Error('source "' + source.id + '" has no github repo URL - cannot install')
|
|
358
|
+
const tmp = mkdtempSync(join(tmpdir(), 'shelf-clone-'))
|
|
359
|
+
try {
|
|
360
|
+
execFileSync('git', ['clone', '--depth', '1', '--filter=blob:none', '--sparse', 'https://github.com/' + spec.repo, tmp], { stdio: 'pipe' })
|
|
361
|
+
execFileSync('git', ['-C', tmp, 'sparse-checkout', 'set', relPath], { stdio: 'pipe' })
|
|
362
|
+
const srcDir = join(tmp, relPath)
|
|
363
|
+
if (!existsSync(srcDir)) throw new Error('upstream skill dir missing after clone: ' + relPath)
|
|
364
|
+
cpSync(srcDir, dest, { recursive: true })
|
|
365
|
+
} finally {
|
|
366
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function hashSkillFile(skillDir) {
|
|
371
|
+
// Single-skill repos author SKILL.src.md (2026-09-22): hash whichever
|
|
372
|
+
// entry file the installed skill actually carries.
|
|
373
|
+
const entry = ENTRY_FILES.find((f) => existsSync(join(skillDir, f)))
|
|
374
|
+
if (!entry) throw new Error('no SKILL.md / SKILL.src.md in installed skill dir: ' + skillDir)
|
|
375
|
+
return 'sha256:' + createHash('sha256').update(readFileSync(join(skillDir, entry))).digest('hex')
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function cmdApply(args) {
|
|
379
|
+
// args._ still carries the 'apply' command token from main()
|
|
380
|
+
const action = args._[1]
|
|
381
|
+
const targets = args._.slice(2)
|
|
382
|
+
const cachePath = resolve(args.cache || join(homedir(), '.dsi/resources/skr-cache.json'))
|
|
383
|
+
const skillsDir = resolve(args['skills-dir'] || join(homedir(), '.agents/skills'))
|
|
384
|
+
if (targets.length === 0) return fail(['apply ' + action + ' needs at least one target'])
|
|
385
|
+
if (!existsSync(cachePath)) return fail(['snapshot missing, run refresh first: ' + cachePath])
|
|
386
|
+
const snapshot = JSON.parse(readFileSync(cachePath, 'utf8'))
|
|
387
|
+
const results = []
|
|
388
|
+
if (action === 'install') {
|
|
389
|
+
const fixtureRoot = args['fixture-root'] ? resolve(args['fixture-root']) : null
|
|
390
|
+
for (const n of targets) {
|
|
391
|
+
const hit = findSkill(snapshot, n)
|
|
392
|
+
if (!hit) { results.push({ n, ok: false, error: 'not on shelf: ' + n }); continue }
|
|
393
|
+
const { source, skill } = hit
|
|
394
|
+
if (skill.tier && UNSTABLE_TIERS.has(skill.tier) && !args.allowUnstable) {
|
|
395
|
+
results.push({ n, id: skill.id, ok: false, error: 'unstable tier (' + skill.tier + ') requires --allow-unstable' })
|
|
396
|
+
continue
|
|
397
|
+
}
|
|
398
|
+
const dest = join(skillsDir, skill.id)
|
|
399
|
+
if (existsSync(dest) && !args.force) {
|
|
400
|
+
results.push({ n, id: skill.id, ok: false, error: 'collision at ' + dest + ' (use --force)' })
|
|
401
|
+
continue
|
|
402
|
+
}
|
|
403
|
+
mkdirSync(skillsDir, { recursive: true })
|
|
404
|
+
try {
|
|
405
|
+
if (existsSync(dest)) rmSync(dest, { recursive: true, force: true })
|
|
406
|
+
if (fixtureRoot) copyDirLocal(fixtureRoot, source.id, skill.path, dest)
|
|
407
|
+
else copyDirGitHub(source, skill.path, dest)
|
|
408
|
+
// Single-skill repos author SKILL.src.md (2026-09-22), but the
|
|
409
|
+
// DSH host skill provider only scans SKILL.md — without it the
|
|
410
|
+
// skill installs fine yet never appears in the host catalog and
|
|
411
|
+
// cannot be invoked. Materialize SKILL.md from the authored src.
|
|
412
|
+
const mdPath = join(dest, 'SKILL.md')
|
|
413
|
+
if (!existsSync(mdPath) && existsSync(join(dest, 'SKILL.src.md'))) {
|
|
414
|
+
copyFileSync(join(dest, 'SKILL.src.md'), mdPath)
|
|
415
|
+
}
|
|
416
|
+
const sig = {
|
|
417
|
+
v: CACHE_VERSION, source: source.id, n: skill.n, skillId: skill.id,
|
|
418
|
+
upstreamPath: source.id + '/' + skill.path,
|
|
419
|
+
installedAt: new Date().toISOString(),
|
|
420
|
+
contentHash: hashSkillFile(dest)
|
|
421
|
+
}
|
|
422
|
+
writeFileSync(join(dest, SIGNATURE_NAME), JSON.stringify(sig, null, 2))
|
|
423
|
+
// Voice sidecars (The Shelf Voice ADR D3/D4): .dsi-voice/<locale>.json
|
|
424
|
+
// travels with the skill. en is an ALWAYS-written scaffold; operator
|
|
425
|
+
// locales come from the --voice JSON payload authored by the
|
|
426
|
+
// installing agent. An invalid payload NEVER blocks the install -
|
|
427
|
+
// it degrades to en-only (D4) and says so on the result item.
|
|
428
|
+
let voiceError = null
|
|
429
|
+
const voice = { en: { name: skill.id, description: '', whenToUse: '' } }
|
|
430
|
+
if (args.voice) {
|
|
431
|
+
try {
|
|
432
|
+
const payload = JSON.parse(args.voice)
|
|
433
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) throw new Error('voice payload must be an object')
|
|
434
|
+
for (const [loc, entry] of Object.entries(payload)) {
|
|
435
|
+
if (!SHELF_LOCALES.includes(loc)) throw new Error('unsupported locale: ' + loc)
|
|
436
|
+
if (!entry || typeof entry !== 'object' || typeof entry.name !== 'string' || !entry.name.trim() || typeof entry.description !== 'string') throw new Error('bad voice entry for ' + loc)
|
|
437
|
+
voice[loc] = { name: entry.name, description: entry.description, whenToUse: String(entry.whenToUse ?? '') }
|
|
438
|
+
}
|
|
439
|
+
} catch (e) {
|
|
440
|
+
voiceError = String(e.message || e)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const voiceDir = join(dest, '.dsi-voice')
|
|
444
|
+
mkdirSync(voiceDir, { recursive: true })
|
|
445
|
+
for (const [loc, entry] of Object.entries(voice)) {
|
|
446
|
+
writeFileSync(join(voiceDir, loc + '.json'), JSON.stringify({ v: CACHE_VERSION, ...entry }, null, 2))
|
|
447
|
+
}
|
|
448
|
+
results.push({ n, id: skill.id, ok: true, installed: true, voice: Object.keys(voice), ...(voiceError ? { voiceError } : {}) })
|
|
449
|
+
} catch (e) {
|
|
450
|
+
rmSync(dest, { recursive: true, force: true }) // D4: no folder without signature
|
|
451
|
+
results.push({ n, id: skill.id, ok: false, error: String(e.message || e) })
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
else if (action === 'uninstall') {
|
|
456
|
+
for (const id of targets) {
|
|
457
|
+
const dir = join(skillsDir, id)
|
|
458
|
+
const sigPath = join(dir, SIGNATURE_NAME)
|
|
459
|
+
if (!existsSync(dir)) { results.push({ id, ok: false, error: 'not installed: ' + id }); continue }
|
|
460
|
+
if (!existsSync(sigPath)) { results.push({ id, ok: false, error: 'unsigned - not uninstallable via shelf (D5)' }); continue }
|
|
461
|
+
try {
|
|
462
|
+
const sig = JSON.parse(readFileSync(sigPath, 'utf8'))
|
|
463
|
+
if (sig.skillId !== id) { results.push({ id, ok: false, error: 'signature id mismatch' }); continue }
|
|
464
|
+
rmSync(dir, { recursive: true, force: true })
|
|
465
|
+
results.push({ id, ok: true, uninstalled: true })
|
|
466
|
+
} catch (e) {
|
|
467
|
+
results.push({ id, ok: false, error: String(e.message || e) })
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} else {
|
|
471
|
+
return fail(['unknown apply action: ' + action])
|
|
472
|
+
}
|
|
473
|
+
// Flip the snapshot flags for successful results (ADR sequence step:
|
|
474
|
+
// "flip installed / installedFrom") - without this the panel's next
|
|
475
|
+
// refetch shows stale state and the uninstall affordance never appears.
|
|
476
|
+
if (results.some((r) => r.ok)) {
|
|
477
|
+
for (const r of results) {
|
|
478
|
+
if (!r.ok) continue
|
|
479
|
+
for (const src of snapshot.sources) {
|
|
480
|
+
const sk = src.skills.find((s) => s.id === (r.id ?? ''))
|
|
481
|
+
if (!sk) continue
|
|
482
|
+
if (action === 'install') { sk.installed = true; sk.signed = true; sk.installedFrom = src.id }
|
|
483
|
+
else { sk.installed = false; sk.signed = false; sk.installedFrom = null }
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
writeFileSync(cachePath, JSON.stringify(snapshot, null, 2))
|
|
487
|
+
}
|
|
488
|
+
const ok = results.every((r) => r.ok)
|
|
489
|
+
return { ok, results }
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// --- main ---
|
|
493
|
+
|
|
494
|
+
async function main() {
|
|
495
|
+
const args = parseArgs(process.argv.slice(2))
|
|
496
|
+
const command = args._[0]
|
|
497
|
+
if (command === 'refresh') {
|
|
498
|
+
const { snapshot, reused } = await cmdRefresh(args)
|
|
499
|
+
console.log(JSON.stringify({ v: CACHE_VERSION, ok: true, reused, snapshot }))
|
|
500
|
+
} else if (command === 'apply') {
|
|
501
|
+
const out = await cmdApply(args)
|
|
502
|
+
console.log(JSON.stringify({ v: CACHE_VERSION, ...out }))
|
|
503
|
+
} else if (command === 'snapshot-status') {
|
|
504
|
+
const cachePath = resolve(args.cache || join(homedir(), '.dsi/resources/skr-cache.json'))
|
|
505
|
+
console.log(JSON.stringify({ v: CACHE_VERSION, ok: true, present: existsSync(cachePath), path: cachePath }))
|
|
506
|
+
} else {
|
|
507
|
+
fail(['unknown command: ' + command + ' (use refresh | apply | snapshot-status)'])
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
main().catch((e) => fail([String(e && e.stack || e)]))
|
|
@@ -159,7 +159,7 @@ Answer these questions from codebase evidence (not guessing):
|
|
|
159
159
|
|
|
160
160
|
### Document 1: PRD
|
|
161
161
|
|
|
162
|
-
Save to: `{project-path}/dev/specs/{TODAY} - {Feature Name}/PRD.md`
|
|
162
|
+
Save to: `{project-path}/dev/specs/{YYYY-MM}/{TODAY} - {Feature Name}/PRD.md`
|
|
163
163
|
|
|
164
164
|
**Contents:**
|
|
165
165
|
1. Current architecture — how it works today (with mermaid flowchart + sequence diagram)
|
|
@@ -183,7 +183,7 @@ Save to: `{project-path}/dev/specs/{TODAY} - {Feature Name}/PRD.md`
|
|
|
183
183
|
|
|
184
184
|
### Document 2: Tasks
|
|
185
185
|
|
|
186
|
-
Save to: `{project-path}/dev/specs/{TODAY} - {Feature Name}/Tasks.md`
|
|
186
|
+
Save to: `{project-path}/dev/specs/{YYYY-MM}/{TODAY} - {Feature Name}/Tasks.md`
|
|
187
187
|
|
|
188
188
|
**Contents:**
|
|
189
189
|
1. Execution checklist — `⛔ Behavioral Commitments` as PUBLIC checklists (not private anti-patterns)
|
|
@@ -200,13 +200,13 @@ Save to: `{project-path}/dev/specs/{TODAY} - {Feature Name}/Tasks.md`
|
|
|
200
200
|
|
|
201
201
|
### Document 3: Karpathy Context
|
|
202
202
|
|
|
203
|
-
Save to: `{project-path}/dev/specs/{TODAY} - {Feature Name}/karpathy-context.md`
|
|
203
|
+
Save to: `{project-path}/dev/specs/{YYYY-MM}/{TODAY} - {Feature Name}/karpathy-context.md`
|
|
204
204
|
|
|
205
205
|
**Contents:** All 3 layers as structured markdown. This is the internal context file the task-executor loads when executing.
|
|
206
206
|
|
|
207
207
|
### Document 4: Tasks Machine Twin (Tasks.json)
|
|
208
208
|
|
|
209
|
-
Save to: `{project-path}/dev/specs/{TODAY} - {Feature Name}/Tasks.json`
|
|
209
|
+
Save to: `{project-path}/dev/specs/{YYYY-MM}/{TODAY} - {Feature Name}/Tasks.json`
|
|
210
210
|
|
|
211
211
|
The binding machine contract — the orchestration register flow maps these keys one-to-one into SQLite. **Rules:**
|
|
212
212
|
|
|
@@ -13,7 +13,7 @@ This file is the single source of truth for the JSON shape. When the orchestrati
|
|
|
13
13
|
"$schema": "openclaw-insight/tasks-twin/v1",
|
|
14
14
|
"featureName": "Orchestration Zoom Artifact Fix",
|
|
15
15
|
"featureSlug": "orchestration-zoom-artifact-fix",
|
|
16
|
-
"specPath": "dev/specs/2026-08-15 - Orchestration Zoom Artifact Fix",
|
|
16
|
+
"specPath": "dev/specs/2026-08/2026-08-15 - Orchestration Zoom Artifact Fix",
|
|
17
17
|
"waves": [
|
|
18
18
|
{
|
|
19
19
|
"waveNumber": 1,
|