@xenonbyte/xsk 0.1.0 → 0.1.1
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/README.md +44 -24
- package/README.zh-CN.md +44 -24
- package/bin/xsk.js +10 -1
- package/lib/adapters/opencode.js +7 -1
- package/lib/capability.js +29 -2
- package/lib/install.js +96 -7
- package/lib/manifest.js +21 -1
- package/lib/skills.js +16 -2
- package/lib/status.js +33 -4
- package/lib/uninstall.js +88 -10
- package/package.json +1 -1
- package/skills/archive-req/SKILL.md +6 -6
- package/skills/check/SKILL.md +1 -1
- package/skills/consume-point/SKILL.md +43 -0
- package/skills/point/SKILL.md +68 -0
- package/skills/skill-scaffold/SKILL.md +5 -2
- package/skills/write-req/SKILL.md +8 -6
- package/templates/fragments/archive-req.behavior.md +3 -3
- package/templates/fragments/archive-req.output.md +1 -1
- package/templates/fragments/archive-req.purpose.md +1 -1
- package/templates/fragments/check.behavior.md +1 -1
- package/templates/fragments/consume-point.behavior.md +13 -0
- package/templates/fragments/consume-point.output.md +1 -0
- package/templates/fragments/consume-point.purpose.md +1 -0
- package/templates/fragments/consume-point.triggers.md +5 -0
- package/templates/fragments/point.behavior.md +35 -0
- package/templates/fragments/point.output.md +1 -0
- package/templates/fragments/point.purpose.md +3 -0
- package/templates/fragments/point.triggers.md +6 -0
- package/templates/fragments/skill-scaffold.behavior.md +5 -2
- package/templates/fragments/write-req.behavior.md +6 -4
- package/templates/fragments/write-req.purpose.md +1 -1
package/lib/status.js
CHANGED
|
@@ -5,7 +5,8 @@ const path = require('node:path');
|
|
|
5
5
|
const os = require('node:os');
|
|
6
6
|
|
|
7
7
|
const { read, validate, defaultXskRoot, isSafePath, isInsideDir, validateOperationalSemantics } = require('./manifest');
|
|
8
|
-
const { ALL_PLATFORMS, MARKER, rootFor } = require('./install');
|
|
8
|
+
const { ALL_PLATFORMS, MARKER, rootFor, commandsRootFor } = require('./install');
|
|
9
|
+
const { contentSha256 } = require('./content-hash');
|
|
9
10
|
|
|
10
11
|
const STATES = ['not-installed', 'ok', 'drift', 'invalid'];
|
|
11
12
|
|
|
@@ -14,12 +15,30 @@ function expectedInstalledPathType(p) {
|
|
|
14
15
|
if (base === 'SKILL.md' || base === MARKER) {
|
|
15
16
|
return 'file';
|
|
16
17
|
}
|
|
18
|
+
// A flat command file (commandsRoot/<name>.md) is an expected regular file, so
|
|
19
|
+
// a missing or no-longer-a-file command path reports drift.
|
|
20
|
+
if (base.endsWith('.md')) {
|
|
21
|
+
return 'file';
|
|
22
|
+
}
|
|
17
23
|
return 'directory';
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
function recordedPathEntries(manifest) {
|
|
27
|
+
const installedHashes = new Map(
|
|
28
|
+
Array.isArray(manifest.installed_hashes)
|
|
29
|
+
? manifest.installed_hashes.map((h) => [h.target, h.sha256])
|
|
30
|
+
: [],
|
|
31
|
+
);
|
|
21
32
|
const installed = Array.isArray(manifest.installed_paths)
|
|
22
|
-
? manifest.installed_paths.map((p) =>
|
|
33
|
+
? manifest.installed_paths.map((p) => {
|
|
34
|
+
const expectedType = expectedInstalledPathType(p);
|
|
35
|
+
const entry = { path: p, expectedType };
|
|
36
|
+
const expectedSha256 = installedHashes.get(p);
|
|
37
|
+
if (expectedType === 'file' && expectedSha256) {
|
|
38
|
+
entry.expectedSha256 = expectedSha256;
|
|
39
|
+
}
|
|
40
|
+
return entry;
|
|
41
|
+
})
|
|
23
42
|
: [];
|
|
24
43
|
const backups = Array.isArray(manifest.backups)
|
|
25
44
|
? manifest.backups.map((b) => ({ path: b.backup, target: b.target, expectedType: 'file', kind: 'backup' }))
|
|
@@ -43,6 +62,14 @@ function pathType(p) {
|
|
|
43
62
|
}
|
|
44
63
|
}
|
|
45
64
|
|
|
65
|
+
function matchesExpectedSha256(entry) {
|
|
66
|
+
try {
|
|
67
|
+
return contentSha256(fs.readFileSync(entry.path)) === entry.expectedSha256;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
46
73
|
function recordedPathHasDrift(entry, options) {
|
|
47
74
|
const opts = options || {};
|
|
48
75
|
const exists = opts.exists || ((p) => fs.existsSync(p));
|
|
@@ -51,6 +78,7 @@ function recordedPathHasDrift(entry, options) {
|
|
|
51
78
|
if (!safe(entry.path, entry)) return true;
|
|
52
79
|
if (!exists(entry.path, entry)) return true;
|
|
53
80
|
if (typeOf && typeOf(entry.path, entry) !== entry.expectedType) return true;
|
|
81
|
+
if (entry.expectedSha256 && !matchesExpectedSha256(entry)) return true;
|
|
54
82
|
return false;
|
|
55
83
|
}
|
|
56
84
|
|
|
@@ -104,7 +132,8 @@ function computeStatus(options) {
|
|
|
104
132
|
let skillsRoot = null;
|
|
105
133
|
if (validate(manifest, { expectedPlatform: platform })) {
|
|
106
134
|
skillsRoot = rootFor(platform, opts.platformRoots);
|
|
107
|
-
const
|
|
135
|
+
const commandsRoot = commandsRootFor(platform, opts.platformCommandsRoots);
|
|
136
|
+
const semantics = validateOperationalSemantics({ platform, skillsRoot, commandsRoot, manifest });
|
|
108
137
|
if (!semantics.valid) {
|
|
109
138
|
result.platforms[platform] = { state: 'invalid', reason: semantics.reason };
|
|
110
139
|
continue;
|
|
@@ -158,7 +187,7 @@ function render(result, options) {
|
|
|
158
187
|
line += ` v${entry.version}`;
|
|
159
188
|
}
|
|
160
189
|
if (entry.missing && entry.missing.length) {
|
|
161
|
-
line += `; ${entry.missing.length} recorded path(s)
|
|
190
|
+
line += `; ${entry.missing.length} recorded path(s) drifted`;
|
|
162
191
|
}
|
|
163
192
|
if (entry.reason) {
|
|
164
193
|
line += ` - ${entry.reason}`;
|
package/lib/uninstall.js
CHANGED
|
@@ -13,7 +13,9 @@ const {
|
|
|
13
13
|
removeManifest,
|
|
14
14
|
manifestPath,
|
|
15
15
|
atomicWriteFile,
|
|
16
|
+
assertSafePath,
|
|
16
17
|
isInsideDir,
|
|
18
|
+
isSafePath,
|
|
17
19
|
} = require('./manifest');
|
|
18
20
|
const { get } = require('./skills');
|
|
19
21
|
const { buildSkill } = require('./generator');
|
|
@@ -124,10 +126,25 @@ function capturePathState(targetPath) {
|
|
|
124
126
|
}
|
|
125
127
|
}
|
|
126
128
|
|
|
129
|
+
// Rollback restore re-runs the same path-safety walk install/uninstall use
|
|
130
|
+
// everywhere else. The capture-to-restore window is fully synchronous, but a
|
|
131
|
+
// guarded delete/create here keeps the documented "no symlink traversal or
|
|
132
|
+
// removal" invariant true even if an ancestor were swapped under us: it fails
|
|
133
|
+
// closed (rollback reports an error) rather than following a symlink out of the
|
|
134
|
+
// owned roots. allowNonDirectoryTarget lets the target itself be a regular file
|
|
135
|
+
// (the common case); a symlinked target or any symlinked ancestor still throws.
|
|
127
136
|
function removePathForRestore(targetPath) {
|
|
137
|
+
assertSafePath(targetPath, 'rollback restore target', { allowNonDirectoryTarget: true });
|
|
128
138
|
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
129
139
|
}
|
|
130
140
|
|
|
141
|
+
function ensureRestoreParent(targetPath) {
|
|
142
|
+
const dir = path.dirname(targetPath);
|
|
143
|
+
assertSafePath(dir, 'rollback restore dir');
|
|
144
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
145
|
+
assertSafePath(dir, 'rollback restore dir');
|
|
146
|
+
}
|
|
147
|
+
|
|
131
148
|
function restorePathState(targetPath, state) {
|
|
132
149
|
if (!state.exists) {
|
|
133
150
|
removePathForRestore(targetPath);
|
|
@@ -137,6 +154,7 @@ function restorePathState(targetPath, state) {
|
|
|
137
154
|
if (fs.existsSync(targetPath) && !fs.lstatSync(targetPath).isDirectory()) {
|
|
138
155
|
removePathForRestore(targetPath);
|
|
139
156
|
}
|
|
157
|
+
assertSafePath(targetPath, 'rollback restore dir');
|
|
140
158
|
fs.mkdirSync(targetPath, { recursive: true, mode: state.mode });
|
|
141
159
|
try {
|
|
142
160
|
fs.chmodSync(targetPath, state.mode);
|
|
@@ -146,12 +164,17 @@ function restorePathState(targetPath, state) {
|
|
|
146
164
|
return;
|
|
147
165
|
}
|
|
148
166
|
removePathForRestore(targetPath);
|
|
149
|
-
|
|
167
|
+
ensureRestoreParent(targetPath);
|
|
150
168
|
if (state.type === 'symlink') {
|
|
169
|
+
assertSafePath(targetPath, 'rollback restore symlink', { allowNonDirectoryTarget: true });
|
|
151
170
|
fs.symlinkSync(state.link, targetPath);
|
|
152
171
|
return;
|
|
153
172
|
}
|
|
154
173
|
if (state.type === 'file') {
|
|
174
|
+
// writeFileSync follows a symlink at targetPath; re-assert it is not one
|
|
175
|
+
// (removePathForRestore cleared it, so this is normally an ENOENT no-op)
|
|
176
|
+
// before writing, then preserve the captured mode via writeFileSync+chmod.
|
|
177
|
+
assertSafePath(targetPath, 'rollback restore file', { allowNonDirectoryTarget: true });
|
|
155
178
|
fs.writeFileSync(targetPath, state.content, { mode: state.mode });
|
|
156
179
|
try {
|
|
157
180
|
fs.chmodSync(targetPath, state.mode);
|
|
@@ -178,7 +201,9 @@ function createRollbackJournal() {
|
|
|
178
201
|
};
|
|
179
202
|
}
|
|
180
203
|
|
|
181
|
-
function uninstallPlatform({ platform, xskRoot, skillsRoot }) {
|
|
204
|
+
function uninstallPlatform({ platform, xskRoot, skillsRoot, commandsRoot }) {
|
|
205
|
+
// Lazy require avoids a load-time cycle (install.js requires uninstall.js).
|
|
206
|
+
const { isCommandFilePath } = require('./install');
|
|
182
207
|
let manifest;
|
|
183
208
|
try {
|
|
184
209
|
manifest = read(platform, { xskRoot });
|
|
@@ -226,7 +251,7 @@ function uninstallPlatform({ platform, xskRoot, skillsRoot }) {
|
|
|
226
251
|
};
|
|
227
252
|
}
|
|
228
253
|
|
|
229
|
-
const sem = validateOperationalSemantics({ platform, skillsRoot, manifest });
|
|
254
|
+
const sem = validateOperationalSemantics({ platform, skillsRoot, commandsRoot, manifest });
|
|
230
255
|
if (!sem.valid) {
|
|
231
256
|
return {
|
|
232
257
|
platform,
|
|
@@ -243,7 +268,12 @@ function uninstallPlatform({ platform, xskRoot, skillsRoot }) {
|
|
|
243
268
|
}
|
|
244
269
|
|
|
245
270
|
const backups = manifest.backups || [];
|
|
246
|
-
|
|
271
|
+
// Command files are flat, markerless `.md` files outside skillsRoot. Filter
|
|
272
|
+
// them out before classifyPaths / ownedDirs / validateBackupTargets so a flat
|
|
273
|
+
// command path is never synthesized into a <cmd>.md/SKILL.md skill triple.
|
|
274
|
+
const commandPaths = manifest.installed_paths.filter((p) => isCommandFilePath(p));
|
|
275
|
+
const skillInstalledPaths = manifest.installed_paths.filter((p) => !isCommandFilePath(p));
|
|
276
|
+
const skillDirs = classifyPaths(skillInstalledPaths);
|
|
247
277
|
const backupTargets = validateBackupTargets(backups, skillDirs, skillsRoot);
|
|
248
278
|
if (!backupTargets.valid) {
|
|
249
279
|
return {
|
|
@@ -274,8 +304,9 @@ function uninstallPlatform({ platform, xskRoot, skillsRoot }) {
|
|
|
274
304
|
let error = null;
|
|
275
305
|
const manifestPaths = new Set(manifest.installed_paths);
|
|
276
306
|
|
|
307
|
+
const retainedCommands = [];
|
|
277
308
|
const ownedDirs = new Set(
|
|
278
|
-
|
|
309
|
+
skillInstalledPaths.filter((p) => {
|
|
279
310
|
const base = path.basename(p);
|
|
280
311
|
return base !== 'SKILL.md' && base !== MARKER;
|
|
281
312
|
}),
|
|
@@ -493,13 +524,57 @@ function uninstallPlatform({ platform, xskRoot, skillsRoot }) {
|
|
|
493
524
|
}
|
|
494
525
|
}
|
|
495
526
|
|
|
496
|
-
|
|
527
|
+
// Dedicated command-file pass: remove a hash-matched (owned) command file,
|
|
528
|
+
// retain a hash-mismatched (user-edited) one, and prune command files for
|
|
529
|
+
// skills no longer installed. Command files carry no marker and no backup, so
|
|
530
|
+
// ownership rests solely on the recorded installed_hashes content match.
|
|
531
|
+
for (const commandPath of commandPaths) {
|
|
532
|
+
if (!isSafePath(commandPath, 'command file', { allowNonDirectoryTarget: true })) {
|
|
533
|
+
refused.push(commandPath);
|
|
534
|
+
retainedCommands.push(commandPath);
|
|
535
|
+
partial = true;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
let stat = null;
|
|
539
|
+
try {
|
|
540
|
+
stat = fs.lstatSync(commandPath);
|
|
541
|
+
} catch (e) {
|
|
542
|
+
if (e && (e.code === 'ENOENT' || e.code === 'ENOTDIR')) {
|
|
543
|
+
stat = null;
|
|
544
|
+
} else {
|
|
545
|
+
throw e;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (stat === null) {
|
|
549
|
+
// Already gone: drop it from the manifest (nothing to remove or retain).
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (!stat.isFile()) {
|
|
553
|
+
refused.push(commandPath);
|
|
554
|
+
retainedCommands.push(commandPath);
|
|
555
|
+
partial = true;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
const recordedHash = installedHashes.get(commandPath);
|
|
559
|
+
const onDisk = fs.readFileSync(commandPath);
|
|
560
|
+
const owned = Boolean(recordedHash) && contentSha256(onDisk) === recordedHash;
|
|
561
|
+
if (!owned) {
|
|
562
|
+
retainedCommands.push(commandPath);
|
|
563
|
+
partial = true;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
rollbackJournal.capture(commandPath);
|
|
567
|
+
fs.rmSync(commandPath, { force: true });
|
|
568
|
+
removed.push(commandPath);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const retainedPaths = [...retainedDirs, ...retainedFiles, ...retainedMarkers, ...retainedCommands];
|
|
497
572
|
let narrowed = null;
|
|
498
573
|
if (retainedPaths.length > 0 || retainedBackups.length > 0) {
|
|
499
574
|
narrowed = create(platform, manifest.version);
|
|
500
575
|
narrowed.installed_paths = retainedPaths;
|
|
501
576
|
narrowed.backups = retainedBackups;
|
|
502
|
-
narrowed.installed_hashes = retainedInstalledHashes(manifest, retainedFiles);
|
|
577
|
+
narrowed.installed_hashes = retainedInstalledHashes(manifest, [...retainedFiles, ...retainedCommands]);
|
|
503
578
|
try {
|
|
504
579
|
rollbackJournal.capture(manifestPath(platform, { xskRoot }));
|
|
505
580
|
write(platform, narrowed, { xskRoot });
|
|
@@ -532,7 +607,7 @@ function uninstallPlatform({ platform, xskRoot, skillsRoot }) {
|
|
|
532
607
|
platform,
|
|
533
608
|
removed,
|
|
534
609
|
restored,
|
|
535
|
-
retained: retainedFiles,
|
|
610
|
+
retained: [...retainedFiles, ...retainedCommands],
|
|
536
611
|
skipped,
|
|
537
612
|
refused,
|
|
538
613
|
partial,
|
|
@@ -549,13 +624,14 @@ function uninstall(options) {
|
|
|
549
624
|
const opts = options || {};
|
|
550
625
|
const platforms = opts.platforms || ['claude', 'codex', 'opencode', 'gemini'];
|
|
551
626
|
const xskRoot = opts.xskRoot || defaultXskRoot();
|
|
552
|
-
const { rootFor } = require('./install');
|
|
627
|
+
const { rootFor, commandsRootFor } = require('./install');
|
|
553
628
|
|
|
554
629
|
const summary = { platforms: {} };
|
|
555
630
|
let exitCode = 0;
|
|
556
631
|
for (const platform of platforms) {
|
|
557
632
|
const skillsRoot = rootFor(platform, opts.platformRoots);
|
|
558
|
-
const
|
|
633
|
+
const commandsRoot = commandsRootFor(platform, opts.platformCommandsRoots);
|
|
634
|
+
const res = uninstallPlatform({ platform, xskRoot, skillsRoot, commandsRoot });
|
|
559
635
|
summary.platforms[platform] = res;
|
|
560
636
|
if (res.exitCode > exitCode) {
|
|
561
637
|
exitCode = res.exitCode;
|
|
@@ -570,6 +646,8 @@ module.exports = {
|
|
|
570
646
|
uninstallPlatform,
|
|
571
647
|
classifyPaths,
|
|
572
648
|
generatedContentFor,
|
|
649
|
+
removePathForRestore,
|
|
650
|
+
restorePathState,
|
|
573
651
|
isSymlink,
|
|
574
652
|
safeBackupForSkill,
|
|
575
653
|
PARTIAL_EXIT,
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: xsk-archive-req
|
|
3
|
-
description: Archive the active requirement document into requirements/archive/ and leave zero active docs.
|
|
3
|
+
description: Archive the active requirement document into .xsk/requirements/archive/ and leave zero active docs.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# xsk-archive-req
|
|
7
7
|
|
|
8
|
-
Archive the active requirement document into
|
|
8
|
+
Archive the active requirement document into `.xsk/requirements/archive/`. After it runs, zero active requirement docs remain.
|
|
9
9
|
|
|
10
10
|
## When to use
|
|
11
11
|
|
|
@@ -17,17 +17,17 @@ Match the intent, not the exact words. Common cues:
|
|
|
17
17
|
|
|
18
18
|
## How it works
|
|
19
19
|
|
|
20
|
-
1. Scan
|
|
20
|
+
1. Scan `.xsk/requirements/*.md` (excluding `.xsk/requirements/archive/`) for the single document whose frontmatter has `status: active`. If more than one exists, stop, list the offending paths, and report the broken invariant for the user to resolve.
|
|
21
21
|
2. If there is no active document, refuse with a one-line reason and stop. Do not archive anything.
|
|
22
22
|
3. Read the active doc slug and validate it against `^[a-z0-9]+(-[a-z0-9]+)*$`. If the slug is missing/invalid, stop with the reason before any write.
|
|
23
|
-
4. Check the archive target
|
|
24
|
-
5. Write the fully-updated archived content to
|
|
23
|
+
4. Check the archive target `.xsk/requirements/archive/<slug>.md`. If it already exists, stop, ask the user how to proceed, and leave it unchanged, writing nothing.
|
|
24
|
+
5. Write the fully-updated archived content to `.xsk/requirements/archive/<slug>.md`: set `status: archived`, add `archived_at: <ISO date>`, and preserve every other frontmatter field and the entire body unchanged.
|
|
25
25
|
6. Confirm it landed as written, then remove the source active doc.
|
|
26
26
|
7. After archiving, confirm zero active documents remain.
|
|
27
27
|
|
|
28
28
|
## Output
|
|
29
29
|
|
|
30
|
-
Either a one-line refusal that names the missing/invalid slug, a stop-and-ask response when
|
|
30
|
+
Either a one-line refusal that names the missing/invalid slug, a stop-and-ask response when `.xsk/requirements/archive/<slug>.md` already exists and it was left unchanged, writing nothing, or the archived path (`.xsk/requirements/archive/<slug>.md`) plus confirmation that it landed before the source active doc was removed and that no active requirement docs remain.
|
|
31
31
|
|
|
32
32
|
## Conventions shared across xsk skills
|
|
33
33
|
|
package/skills/check/SKILL.md
CHANGED
|
@@ -45,7 +45,7 @@ Match the intent, not the exact words. Common cues:
|
|
|
45
45
|
|
|
46
46
|
**6. Gate every finding on evidence.** A HIGH or CRITICAL finding needs three things: the exact file and line, the concrete trigger that produces the bad outcome, and why existing guards do not already prevent it. Missing any one, downgrade it or drop it. Do not pad the report with low-confidence noise that trains the reader to ignore the real findings.
|
|
47
47
|
|
|
48
|
-
**7. Route the fixes.**
|
|
48
|
+
**7. Route the fixes. Review-only by default.** Do not modify files during a review. List the safe, risk-free mechanical fixes (typos, missing imports, obvious style) separately from the findings, and apply them only when the user explicitly asks for fixes. Batch behavior-changing fixes (added null checks, new error handling) into one confirmation block instead of asking one at a time. Leave architecture and security tradeoffs for the user to decide, and mark informational notes as advisory.
|
|
49
49
|
|
|
50
50
|
**8. Verify before claiming done.** Run the project's own verification (its tests, lint, type check, build, or syntax check) and read the output. For a bug fix, a regression test that fails on the old code must exist before the fix counts as done. If no verification command is available, say so plainly and call it a gap. Never present unverified work as passing.
|
|
51
51
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: xsk-consume-point
|
|
3
|
+
description: Fold selected .xsk/points into one .xsk/requirements doc via xsk-write-req, archiving consumed points write-before-remove and leaving unconsumed points active.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# xsk-consume-point
|
|
7
|
+
|
|
8
|
+
Fold one or more researched point documents from `.xsk/points/` into a single requirement document via `xsk-write-req`. Points that land in the requirement are archived as `consumed`; the rest stay active or are dropped only on user confirmation.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
Match the intent, not the exact words. Common cues:
|
|
13
|
+
|
|
14
|
+
- "把这些 point 变成需求", "消费 point", "把调研结果写成需求"
|
|
15
|
+
- "consume points", "turn points into a requirement", "fold points into a req"
|
|
16
|
+
- any request to convert accumulated point documents into a structured requirement document
|
|
17
|
+
|
|
18
|
+
## How it works
|
|
19
|
+
|
|
20
|
+
**1. Scan and list available points.** Read all `.md` files under `.xsk/points/` excluding `.xsk/points/archive/`. For each, extract `slug`, `status`, and the `## Aspect` line. Display the list with status, highlighting `ready` entries as eligible. If no files exist, or all are already archived, stop with a one-line reason and do not proceed. If no unarchived point has `status: ready`, stop with a one-line reason, leave every point unchanged, and do not proceed.
|
|
21
|
+
|
|
22
|
+
**2. Have the user select.** Ask the user which `ready` points to fold into the requirement. Only points with `status: ready` may be selected. If the user selects or names any `researching` or otherwise non-ready point, stop without writing or archiving anything and say it must be completed by `xsk-point` first. If the user selects none, stop without writing anything.
|
|
23
|
+
|
|
24
|
+
**3. Guard the single-active requirement.** Scan `.xsk/requirements/*.md` (excluding `.xsk/requirements/archive/`) for frontmatter `status: active`. If one exists, prompt the user: append the selected points to the existing active requirement, or abort. If the user aborts, leave `.xsk/requirements/` and `.xsk/points/` entirely unchanged and stop. If more than one active requirement exists, stop, list the offending paths, and report the broken invariant for the user to resolve.
|
|
25
|
+
|
|
26
|
+
**4. Hand off to xsk-write-req.** Pass the selected `ready` points to `xsk-write-req` as the input need. Before the handoff, re-read each selected point and confirm `status: ready`; if any selected point is no longer ready, stop, archive nothing, and leave all points unchanged. For each selected point, supply the `## Aspect` and `## Landed plan` as the core input, with `## Research` as supporting context. Do not duplicate or modify the `xsk-write-req` behavior; invoke it by reference. If `xsk-write-req` stops for any reason (blocking decision, user abort, audit failure), archive nothing and leave all points unchanged.
|
|
27
|
+
|
|
28
|
+
**5. Archive folded points as consumed (write-before-remove).** After the requirement lands successfully, for each selected point that was folded in: set `status: consumed`, add `consumed_at: <ISO date>` and `consumed_by: .xsk/requirements/<slug>.md` to the frontmatter. Before writing, check the archive target `.xsk/points/archive/<slug>.md`: if it already exists, stop, ask the user how to proceed, and leave that point's source and archive unchanged rather than overwriting. Otherwise write the updated content to `.xsk/points/archive/<slug>.md`, confirm it landed, then remove `.xsk/points/<slug>.md`. Complete write-before-remove for every folded point before reporting done.
|
|
29
|
+
|
|
30
|
+
**6. Leave excluded and deferred points active.** Points not selected, or points the user deferred, stay in `.xsk/points/` unchanged with their current status.
|
|
31
|
+
|
|
32
|
+
**7. Drop consume-rejected points only on user confirmation.** If the user decides a point should be discarded rather than deferred, confirm before writing. On confirmation, set `status: dropped`, add `dropped_at: <ISO date>` and a one-line `dropped_reason`. Before writing, check the archive target `.xsk/points/archive/<slug>.md`: if it already exists, stop, ask the user how to proceed, and leave it unchanged, writing nothing. Otherwise write to `.xsk/points/archive/<slug>.md` (write-before-remove), then remove the source file.
|
|
33
|
+
|
|
34
|
+
## Output
|
|
35
|
+
|
|
36
|
+
The path of the produced requirement document (or the existing active doc if appended), plus a summary of which points were archived as `consumed` and which remain active or were dropped.
|
|
37
|
+
|
|
38
|
+
## Conventions shared across xsk skills
|
|
39
|
+
|
|
40
|
+
- Triggers are matched by intent, not by exact wording. The phrases listed under "When to use" are cues, not a required incantation.
|
|
41
|
+
- Write in natural, direct prose. No formulaic openers, no filler conclusions, no restating the request before you answer it.
|
|
42
|
+
- When a decision would change the implementation, surface it as a short question and let the user decide. Do not pick silently.
|
|
43
|
+
- These are instruction skills. They shape how work is approached, not what the agent is technically capable of.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: xsk-point
|
|
3
|
+
description: Research one aspect into .xsk/points/ to a decision-complete landed plan using xsk-think discipline, then persist and manage it as a named point document.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# xsk-point
|
|
7
|
+
|
|
8
|
+
Research one aspect of the current project to a decision-complete landed plan and persist it as a point document in `.xsk/points/`. The result is a concrete, slug-named file an implementer can consume without re-doing the research.
|
|
9
|
+
|
|
10
|
+
It is research-and-persist only. It writes no code, no scaffolding, and no requirement document.
|
|
11
|
+
|
|
12
|
+
## When to use
|
|
13
|
+
|
|
14
|
+
Match the intent, not the exact words. Common cues:
|
|
15
|
+
|
|
16
|
+
- "研究一下", "调研一下", "先搞清楚", "这块需要研究"
|
|
17
|
+
- "spike this", "research this", "figure out the approach for", "investigate"
|
|
18
|
+
- any request to explore and nail down one aspect before writing a requirement or starting implementation
|
|
19
|
+
- a slug argument that names an existing point (triggers a refinement of that point, not a new one)
|
|
20
|
+
|
|
21
|
+
## How it works
|
|
22
|
+
|
|
23
|
+
**1. Ground in the current project.** Read the relevant code, config, and docs before forming any opinion. Never rely on memory for project-specific facts. Identify the one aspect under investigation and state it as a single sentence.
|
|
24
|
+
|
|
25
|
+
**2. Research with xsk-think's discipline.** Apply the same depth-matching, option-weighing, and premise-collapse discipline that `xsk-think` uses: match depth to the problem (lightweight, evaluation, or triage), declare premise collapse explicitly if one assumption invalidates everything, and reach a decision-complete landed plan. Do not stop at "it depends" - resolve the options and commit to one. If a blocking decision can only be resolved by the user, surface it as a single question before writing anything.
|
|
26
|
+
|
|
27
|
+
**3. Re-invoke-by-slug to refine.** If the user supplies a slug and `.xsk/points/<slug>.md` already exists, load it, treat the existing `## Research` and `## Landed plan` as prior context, and refine in place rather than starting over. Update the file with the refined content; do not create a duplicate.
|
|
28
|
+
|
|
29
|
+
**4. Persist the point document.** Write `.xsk/points/<slug>.md` where `<slug>` is generated from the aspect title using the pattern `^[a-z0-9]+(-[a-z0-9]+)*$`. Set `status: researching` while work is in progress; promote to `status: ready` only when the `## Landed plan` is decision-complete and the user confirms.
|
|
30
|
+
|
|
31
|
+
Point document schema:
|
|
32
|
+
|
|
33
|
+
```markdown
|
|
34
|
+
---
|
|
35
|
+
status: researching | ready
|
|
36
|
+
slug: <slug>
|
|
37
|
+
created_at: <ISO date>
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
# <title>
|
|
41
|
+
|
|
42
|
+
## Aspect
|
|
43
|
+
|
|
44
|
+
One-sentence statement of the aspect under investigation.
|
|
45
|
+
|
|
46
|
+
## Research
|
|
47
|
+
|
|
48
|
+
Evidence gathered, options considered, and tradeoffs weighed. Grounded in project reality.
|
|
49
|
+
|
|
50
|
+
## Landed plan
|
|
51
|
+
|
|
52
|
+
The concrete, decision-complete outcome. No "TBD". No open forks.
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**5. Ensure the gitignore line.** Ensure `.xsk/.gitignore` contains the line `points/archive/` (relative to `.xsk/`). Create `.xsk/` and `.xsk/.gitignore` if absent; append the line only if it is missing; never overwrite an existing `.xsk/.gitignore`.
|
|
56
|
+
|
|
57
|
+
**6. Drop only on user confirmation.** If the user asks to drop a point, confirm before writing. On confirmation, set `status: dropped`, add `dropped_at: <ISO date>` and a one-line `dropped_reason` to the frontmatter. Before writing, check the archive target `.xsk/points/archive/<slug>.md`: if it already exists, stop, ask the user how to proceed, and leave everything unchanged, writing nothing. Otherwise move the file to `.xsk/points/archive/<slug>.md` using write-before-remove (write the archive copy first, confirm it landed, then remove the source).
|
|
58
|
+
|
|
59
|
+
## Output
|
|
60
|
+
|
|
61
|
+
The path of the point document and its current `status`: active points (`researching`, `ready`) are at `.xsk/points/<slug>.md`; dropped points are at `.xsk/points/archive/<slug>.md`.
|
|
62
|
+
|
|
63
|
+
## Conventions shared across xsk skills
|
|
64
|
+
|
|
65
|
+
- Triggers are matched by intent, not by exact wording. The phrases listed under "When to use" are cues, not a required incantation.
|
|
66
|
+
- Write in natural, direct prose. No formulaic openers, no filler conclusions, no restating the request before you answer it.
|
|
67
|
+
- When a decision would change the implementation, surface it as a short question and let the user decide. Do not pick silently.
|
|
68
|
+
- These are instruction skills. They shape how work is approached, not what the agent is technically capable of.
|
|
@@ -32,11 +32,14 @@ Match the intent, not the exact words. Common cues:
|
|
|
32
32
|
- Unknown options fail loud.
|
|
33
33
|
- `status` validates manifest **shape**, not just parse success.
|
|
34
34
|
- Removed or renamed commands leave no stale references (grep-clean across CLI, help, README, generated text, `AGENTS.md`, `CLAUDE.md`).
|
|
35
|
-
- Four platforms
|
|
36
|
-
-
|
|
35
|
+
- Four platforms covered: Claude Code, Codex, opencode, Gemini.
|
|
36
|
+
- User-invocable on every platform, not merely present. Installing a skill must make it invocable on each target platform. Platforms that do not auto-expose skill files as slash commands need a verified platform-specific invocation artifact carrying the skill body and the platform's argument placeholder so invocation arguments are not dropped. This project currently implements that artifact for opencode as a command file; do not claim it for another platform until adapter, install, uninstall, status, docs, and tests all cover that platform. Claude exposes skill files directly. Verify per platform rather than assuming.
|
|
37
|
+
- Manifest-backed install safety: owned-only removal, ownership markers, atomic writes, symlink refusal, and content-hash modification detection. The manifest records a hash per owned file; a previously generated install is recognized by that hash even without the marker (markerless detection), so a user-edited owned file is detected and refused or rolled back rather than silently overwritten.
|
|
37
38
|
- `install` is uninstall-first: a reinstall resets the previously-owned files (pruning skills no longer installed) before regenerating, so no manual `uninstall` is needed. It still refuses to overwrite a user-edited owned file and rolls back instead of destroying it.
|
|
39
|
+
- Built from source: skills are generated from a single per-skill source, not hand-maintained per platform or per file. The reference composes each skill from per-section fragments (`purpose`, `triggers`, `behavior`, `output`) plus a shared common body and a template, with one registry listing the skills. The build is deterministic, and the committed packed skill output stays byte-for-byte in sync with the generator, enforced by a test. The golden snapshot below is the masked form of that output.
|
|
38
40
|
- A golden snapshot of the generated skill shell, masking embedded `shared/` body.
|
|
39
41
|
- A bilingual README (`README.md` plus `README.zh-CN.md`) with identical headings, English literals preserved, and content-pinning tests.
|
|
42
|
+
- Test coverage spans the install surface, not only generation and docs: install, uninstall, uninstall-first reset, transactional rollback, and the safety refusals (a user-edited owned file, symlinked paths) are exercised by executable tests, alongside the golden snapshot, README parity, and self-conformance tests.
|
|
40
43
|
|
|
41
44
|
### Self-conformance
|
|
42
45
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: xsk-write-req
|
|
3
|
-
description: Turn plain-language needs into a grounded requirement document in requirements/, with a self-audit gate before it is finalized.
|
|
3
|
+
description: Turn plain-language needs into a grounded requirement document in .xsk/requirements/, with a self-audit gate before it is finalized.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# xsk-write-req
|
|
7
7
|
|
|
8
|
-
Convert plain-language ("白话") needs into a compliant requirement document in
|
|
8
|
+
Convert plain-language ("白话") needs into a compliant requirement document in `.xsk/requirements/`, grounded in the current project's actual code. The output is a doc an implementer can act on without guessing, not a transcript of the request.
|
|
9
9
|
|
|
10
10
|
## When to use
|
|
11
11
|
|
|
@@ -19,16 +19,17 @@ Match the intent, not the exact words. Common cues:
|
|
|
19
19
|
|
|
20
20
|
**1. Read the project first.** `grep` and `read` the current project structure, config files (`package.json` and the like), and relevant code to ground the requirement in reality. Never quote defaults from memory.
|
|
21
21
|
|
|
22
|
-
**2. Locate or create the active requirement doc.** Scan
|
|
22
|
+
**2. Locate or create the active requirement doc.** Scan `.xsk/requirements/*.md` for frontmatter `status: active`. There is **at most one** active doc at a time. If more than one exists, stop, list the offending paths, and report the broken invariant for the user to resolve. If one exists, lock onto it and append or refine. If none exists, create `.xsk/requirements/<slug>.md` with `status: active` and a slug generated from the need.
|
|
23
23
|
|
|
24
|
-
**3. Ensure the directory convention.**
|
|
24
|
+
**3. Ensure the directory convention.** Ensure `.xsk/.gitignore` contains the line `requirements/archive/` (create `.xsk/` and `.xsk/.gitignore` if absent; append only if the line is missing; never overwrite an existing `.xsk/.gitignore`).
|
|
25
25
|
|
|
26
26
|
**4. Convert fuzzy into concrete.** Match the document structure to the input richness:
|
|
27
27
|
|
|
28
28
|
- A minimal one-liner becomes a concise requirement: Goal, Scope, Acceptance. Do not force a Background section that would be filler.
|
|
29
29
|
- Richer input becomes Background, Goal, Scope (in and out), Requirements, Open Questions, Checkpoints.
|
|
30
|
+
- Open Questions holds only non-blocking items, and each one names its disposition: escalate it to the user to decide (step 5), or defer it to a named owner at a named checkpoint. A choice that would fork the implementation is never parked here. Resolve it inline, or raise it as a decision point.
|
|
30
31
|
|
|
31
|
-
**5. Decision points go to the user.** When a genuine technical or scoping choice would change the implementation, stop and ask the user to decide
|
|
32
|
+
**5. Decision points go to the user, resolved one at a time.** When a genuine technical or scoping choice would change the implementation, stop and ask the user to decide: surface the options and the tradeoffs, let them pick, do not pick silently. When several genuine decisions exist, take them in dependency order rather than all at once. Surface the most upstream open one first, fold the answer in, then re-derive what remains before surfacing the next. A resolved decision often removes or reshapes the forks beneath it, so never surface a fork whose options still depend on an open decision.
|
|
32
33
|
|
|
33
34
|
**6. Brainstorm the genuinely open-ended.** Where the need is open-ended, run a short exploration with the user before writing.
|
|
34
35
|
|
|
@@ -38,8 +39,9 @@ Match the intent, not the exact words. Common cues:
|
|
|
38
39
|
|
|
39
40
|
- **Conflict check:** verify no internal contradictions (Goal versus Scope, in-scope versus out-of-scope, Requirements versus Open Questions, any two statements that cannot both hold). Resolve every conflict, or surface it to the user. A finalized doc contains zero conflicts.
|
|
40
41
|
- **Ambiguity check:** verify no undefined terms, unstated assumptions, or vague qualifiers ("fast", "supported", "as needed") that would force the implementer to guess. Tighten each to a concrete, testable statement. A finalized doc leaves no ambiguity that blocks implementation.
|
|
42
|
+
- **Open-questions check:** verify every Open Question is non-blocking and names a disposition (escalated to the user, or deferred to a named owner at a named checkpoint). Resolve or raise anything that would block or fork implementation before finalizing. A finalized doc parks no blocking or unowned question.
|
|
41
43
|
- **Checkpoint gates:** ensure the requirement defines the verification points where downstream implementation must pause and confirm against the requirement (acceptance criteria, integration gates, review gates). If it lacks them, add them before finalizing.
|
|
42
|
-
|
|
44
|
+
The audit is a loop, not a single pass. If it surfaces issues only the user can resolve, take them one at a time in dependency order (as in step 5), then re-run the checks before finalizing. Repeat until one full pass finds zero conflicts, zero blocking ambiguity, and nothing left for the user to resolve. Do not write a contradictory or under-specified doc.
|
|
43
45
|
|
|
44
46
|
The document frontmatter:
|
|
45
47
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
1. Scan
|
|
1
|
+
1. Scan `.xsk/requirements/*.md` (excluding `.xsk/requirements/archive/`) for the single document whose frontmatter has `status: active`. If more than one exists, stop, list the offending paths, and report the broken invariant for the user to resolve.
|
|
2
2
|
2. If there is no active document, refuse with a one-line reason and stop. Do not archive anything.
|
|
3
3
|
3. Read the active doc slug and validate it against `^[a-z0-9]+(-[a-z0-9]+)*$`. If the slug is missing/invalid, stop with the reason before any write.
|
|
4
|
-
4. Check the archive target
|
|
5
|
-
5. Write the fully-updated archived content to
|
|
4
|
+
4. Check the archive target `.xsk/requirements/archive/<slug>.md`. If it already exists, stop, ask the user how to proceed, and leave it unchanged, writing nothing.
|
|
5
|
+
5. Write the fully-updated archived content to `.xsk/requirements/archive/<slug>.md`: set `status: archived`, add `archived_at: <ISO date>`, and preserve every other frontmatter field and the entire body unchanged.
|
|
6
6
|
6. Confirm it landed as written, then remove the source active doc.
|
|
7
7
|
7. After archiving, confirm zero active documents remain.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Either a one-line refusal that names the missing/invalid slug, a stop-and-ask response when
|
|
1
|
+
Either a one-line refusal that names the missing/invalid slug, a stop-and-ask response when `.xsk/requirements/archive/<slug>.md` already exists and it was left unchanged, writing nothing, or the archived path (`.xsk/requirements/archive/<slug>.md`) plus confirmation that it landed before the source active doc was removed and that no active requirement docs remain.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Archive the active requirement document into
|
|
1
|
+
Archive the active requirement document into `.xsk/requirements/archive/`. After it runs, zero active requirement docs remain.
|
|
@@ -24,6 +24,6 @@
|
|
|
24
24
|
|
|
25
25
|
**6. Gate every finding on evidence.** A HIGH or CRITICAL finding needs three things: the exact file and line, the concrete trigger that produces the bad outcome, and why existing guards do not already prevent it. Missing any one, downgrade it or drop it. Do not pad the report with low-confidence noise that trains the reader to ignore the real findings.
|
|
26
26
|
|
|
27
|
-
**7. Route the fixes.**
|
|
27
|
+
**7. Route the fixes. Review-only by default.** Do not modify files during a review. List the safe, risk-free mechanical fixes (typos, missing imports, obvious style) separately from the findings, and apply them only when the user explicitly asks for fixes. Batch behavior-changing fixes (added null checks, new error handling) into one confirmation block instead of asking one at a time. Leave architecture and security tradeoffs for the user to decide, and mark informational notes as advisory.
|
|
28
28
|
|
|
29
29
|
**8. Verify before claiming done.** Run the project's own verification (its tests, lint, type check, build, or syntax check) and read the output. For a bug fix, a regression test that fails on the old code must exist before the fix counts as done. If no verification command is available, say so plainly and call it a gap. Never present unverified work as passing.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
**1. Scan and list available points.** Read all `.md` files under `.xsk/points/` excluding `.xsk/points/archive/`. For each, extract `slug`, `status`, and the `## Aspect` line. Display the list with status, highlighting `ready` entries as eligible. If no files exist, or all are already archived, stop with a one-line reason and do not proceed. If no unarchived point has `status: ready`, stop with a one-line reason, leave every point unchanged, and do not proceed.
|
|
2
|
+
|
|
3
|
+
**2. Have the user select.** Ask the user which `ready` points to fold into the requirement. Only points with `status: ready` may be selected. If the user selects or names any `researching` or otherwise non-ready point, stop without writing or archiving anything and say it must be completed by `xsk-point` first. If the user selects none, stop without writing anything.
|
|
4
|
+
|
|
5
|
+
**3. Guard the single-active requirement.** Scan `.xsk/requirements/*.md` (excluding `.xsk/requirements/archive/`) for frontmatter `status: active`. If one exists, prompt the user: append the selected points to the existing active requirement, or abort. If the user aborts, leave `.xsk/requirements/` and `.xsk/points/` entirely unchanged and stop. If more than one active requirement exists, stop, list the offending paths, and report the broken invariant for the user to resolve.
|
|
6
|
+
|
|
7
|
+
**4. Hand off to xsk-write-req.** Pass the selected `ready` points to `xsk-write-req` as the input need. Before the handoff, re-read each selected point and confirm `status: ready`; if any selected point is no longer ready, stop, archive nothing, and leave all points unchanged. For each selected point, supply the `## Aspect` and `## Landed plan` as the core input, with `## Research` as supporting context. Do not duplicate or modify the `xsk-write-req` behavior; invoke it by reference. If `xsk-write-req` stops for any reason (blocking decision, user abort, audit failure), archive nothing and leave all points unchanged.
|
|
8
|
+
|
|
9
|
+
**5. Archive folded points as consumed (write-before-remove).** After the requirement lands successfully, for each selected point that was folded in: set `status: consumed`, add `consumed_at: <ISO date>` and `consumed_by: .xsk/requirements/<slug>.md` to the frontmatter. Before writing, check the archive target `.xsk/points/archive/<slug>.md`: if it already exists, stop, ask the user how to proceed, and leave that point's source and archive unchanged rather than overwriting. Otherwise write the updated content to `.xsk/points/archive/<slug>.md`, confirm it landed, then remove `.xsk/points/<slug>.md`. Complete write-before-remove for every folded point before reporting done.
|
|
10
|
+
|
|
11
|
+
**6. Leave excluded and deferred points active.** Points not selected, or points the user deferred, stay in `.xsk/points/` unchanged with their current status.
|
|
12
|
+
|
|
13
|
+
**7. Drop consume-rejected points only on user confirmation.** If the user decides a point should be discarded rather than deferred, confirm before writing. On confirmation, set `status: dropped`, add `dropped_at: <ISO date>` and a one-line `dropped_reason`. Before writing, check the archive target `.xsk/points/archive/<slug>.md`: if it already exists, stop, ask the user how to proceed, and leave it unchanged, writing nothing. Otherwise write to `.xsk/points/archive/<slug>.md` (write-before-remove), then remove the source file.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
The path of the produced requirement document (or the existing active doc if appended), plus a summary of which points were archived as `consumed` and which remain active or were dropped.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Fold one or more researched point documents from `.xsk/points/` into a single requirement document via `xsk-write-req`. Points that land in the requirement are archived as `consumed`; the rest stay active or are dropped only on user confirmation.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Match the intent, not the exact words. Common cues:
|
|
2
|
+
|
|
3
|
+
- "把这些 point 变成需求", "消费 point", "把调研结果写成需求"
|
|
4
|
+
- "consume points", "turn points into a requirement", "fold points into a req"
|
|
5
|
+
- any request to convert accumulated point documents into a structured requirement document
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
**1. Ground in the current project.** Read the relevant code, config, and docs before forming any opinion. Never rely on memory for project-specific facts. Identify the one aspect under investigation and state it as a single sentence.
|
|
2
|
+
|
|
3
|
+
**2. Research with xsk-think's discipline.** Apply the same depth-matching, option-weighing, and premise-collapse discipline that `xsk-think` uses: match depth to the problem (lightweight, evaluation, or triage), declare premise collapse explicitly if one assumption invalidates everything, and reach a decision-complete landed plan. Do not stop at "it depends" - resolve the options and commit to one. If a blocking decision can only be resolved by the user, surface it as a single question before writing anything.
|
|
4
|
+
|
|
5
|
+
**3. Re-invoke-by-slug to refine.** If the user supplies a slug and `.xsk/points/<slug>.md` already exists, load it, treat the existing `## Research` and `## Landed plan` as prior context, and refine in place rather than starting over. Update the file with the refined content; do not create a duplicate.
|
|
6
|
+
|
|
7
|
+
**4. Persist the point document.** Write `.xsk/points/<slug>.md` where `<slug>` is generated from the aspect title using the pattern `^[a-z0-9]+(-[a-z0-9]+)*$`. Set `status: researching` while work is in progress; promote to `status: ready` only when the `## Landed plan` is decision-complete and the user confirms.
|
|
8
|
+
|
|
9
|
+
Point document schema:
|
|
10
|
+
|
|
11
|
+
```markdown
|
|
12
|
+
---
|
|
13
|
+
status: researching | ready
|
|
14
|
+
slug: <slug>
|
|
15
|
+
created_at: <ISO date>
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# <title>
|
|
19
|
+
|
|
20
|
+
## Aspect
|
|
21
|
+
|
|
22
|
+
One-sentence statement of the aspect under investigation.
|
|
23
|
+
|
|
24
|
+
## Research
|
|
25
|
+
|
|
26
|
+
Evidence gathered, options considered, and tradeoffs weighed. Grounded in project reality.
|
|
27
|
+
|
|
28
|
+
## Landed plan
|
|
29
|
+
|
|
30
|
+
The concrete, decision-complete outcome. No "TBD". No open forks.
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**5. Ensure the gitignore line.** Ensure `.xsk/.gitignore` contains the line `points/archive/` (relative to `.xsk/`). Create `.xsk/` and `.xsk/.gitignore` if absent; append the line only if it is missing; never overwrite an existing `.xsk/.gitignore`.
|
|
34
|
+
|
|
35
|
+
**6. Drop only on user confirmation.** If the user asks to drop a point, confirm before writing. On confirmation, set `status: dropped`, add `dropped_at: <ISO date>` and a one-line `dropped_reason` to the frontmatter. Before writing, check the archive target `.xsk/points/archive/<slug>.md`: if it already exists, stop, ask the user how to proceed, and leave everything unchanged, writing nothing. Otherwise move the file to `.xsk/points/archive/<slug>.md` using write-before-remove (write the archive copy first, confirm it landed, then remove the source).
|