durable-context 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +24 -0
  3. package/bin/durable-context.js +62 -0
  4. package/lib/installer.js +553 -0
  5. package/package.json +31 -0
  6. package/template/.agents/skills/README.md +6 -0
  7. package/template/.agents/skills/dive-into-plan/SKILL.md +27 -0
  8. package/template/.agents/skills/plan-with-context/SKILL.md +27 -0
  9. package/template/AGENTS.md +17 -0
  10. package/template/context/README.md +24 -0
  11. package/template/context/_templates/initiative/README.md +29 -0
  12. package/template/context/_templates/initiative/architecture.md +44 -0
  13. package/template/context/_templates/initiative/backlog.md +23 -0
  14. package/template/context/_templates/initiative/brief.html +56 -0
  15. package/template/context/_templates/initiative/decisions/ADR-0000-template.md +34 -0
  16. package/template/context/_templates/initiative/delivery.md +45 -0
  17. package/template/context/_templates/initiative/infrastructure.md +41 -0
  18. package/template/context/_templates/initiative/interface.md +41 -0
  19. package/template/context/_templates/initiative/operations.md +47 -0
  20. package/template/context/_templates/initiative/plan.md +23 -0
  21. package/template/context/_templates/initiative/release-doc-notes.md +34 -0
  22. package/template/context/_templates/initiative/spec.md +35 -0
  23. package/template/context/_templates/initiative/testing.md +45 -0
  24. package/template/context/initiatives/.gitkeep +0 -0
  25. package/template/context/project-profile.md +67 -0
  26. package/template/decisions/0000-template.md +38 -0
  27. package/template/decisions/README.md +38 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ricardo Mendez Rodriguez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # durable-context
2
+
3
+ Invocation-only skills and scaffold for durable planning in the repo.
4
+
5
+ Install into a project:
6
+
7
+ ```bash
8
+ npx durable-context init --project-name "My App"
9
+ ```
10
+
11
+ Adds `context/`, `decisions/`, and skills `plan-with-context` and `dive-into-plan`.
12
+
13
+ ## Use
14
+
15
+ ```text
16
+ Plan with durable context: <what you want to build>
17
+ Dive into the plan.
18
+ ```
19
+
20
+ Skills are invocation-only — they do not run automatically.
21
+
22
+ Options: `--target`, `--dry-run`, `--force`, `status`.
23
+
24
+ For release-anchored documentation, see the `reference-docs` package.
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ import { runCli, reportError } from '../lib/installer.js';
8
+
9
+ const cliName = 'durable-context';
10
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
11
+ const packageJson = JSON.parse(await readFile(path.join(packageRoot, 'package.json'), 'utf8'));
12
+
13
+ const agentsStart = '<!-- durable-context:start -->';
14
+ const agentsEnd = '<!-- durable-context:end -->';
15
+
16
+ const skills = [
17
+ {
18
+ name: 'plan-with-context',
19
+ readmeEntry:
20
+ '- `plan-with-context` - invoke explicitly to draft a durable plan in an initiative `plan.md`.'
21
+ },
22
+ {
23
+ name: 'dive-into-plan',
24
+ readmeEntry:
25
+ '- `dive-into-plan` - invoke explicitly to interrogate a settled plan, distribute it into initiative docs, and promote decisions.'
26
+ }
27
+ ];
28
+
29
+ function renderAgentSection(projectName) {
30
+ return `${agentsStart}
31
+ ## Durable Context
32
+
33
+ Working context under [\`context/\`](context/); durable decisions under [\`decisions/\`](decisions/).
34
+ Initiatives under [\`context/initiatives/\`](context/initiatives/) are disposable; promote accepted decisions to [\`decisions/\`](decisions/).
35
+
36
+ Invocation-only skills — ask by name:
37
+
38
+ - [\`.agents/skills/plan-with-context/SKILL.md\`](.agents/skills/plan-with-context/SKILL.md) — draft a plan in \`plan.md\`.
39
+ - [\`.agents/skills/dive-into-plan/SKILL.md\`](.agents/skills/dive-into-plan/SKILL.md) — interrogate gaps, distribute into per-concern docs, promote to [\`decisions/\`](decisions/).
40
+
41
+ [\`context/project-profile.md\`](context/project-profile.md) — repo-wide stack, commands, and test facts when populated.
42
+ ${agentsEnd}`;
43
+ }
44
+
45
+ const config = {
46
+ cliName,
47
+ packageRoot,
48
+ packageJson,
49
+ summaryLabel: 'Durable Context',
50
+ metadataPath: '.durable-context/install.json',
51
+ skills,
52
+ agents: {
53
+ start: agentsStart,
54
+ end: agentsEnd,
55
+ render: renderAgentSection
56
+ },
57
+ nextSteps: [
58
+ 'Then: invoke .agents/skills/plan-with-context/SKILL.md when you start planning an initiative.'
59
+ ]
60
+ };
61
+
62
+ runCli(config, process.argv.slice(2)).catch((error) => reportError(error, cliName));
@@ -0,0 +1,553 @@
1
+ import { execPath } from 'node:process';
2
+ import { constants as fsConstants } from 'node:fs';
3
+ import { access, mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
6
+ const agentsSkillsRelative = '.agents/skills';
7
+
8
+ /**
9
+ * Generic installer shared by durable-context and reference-docs packages.
10
+ *
11
+ * Each package supplies a `config` describing its payload:
12
+ * - cliName: bin name, used in help and error text
13
+ * - packageRoot: absolute path to the package directory
14
+ * - packageJson: parsed package.json of the package
15
+ * - skills: [{ name, readmeEntry }] copied into .agents/skills
16
+ * - agents: { start, end, render(projectName) } AGENTS.md section
17
+ * - metadataPath: relative path of the install metadata file in the target
18
+ * - nextSteps: strings printed after a successful install
19
+ * - summaryLabel: short label used in the final summary line
20
+ */
21
+ export async function runCli(config, argv) {
22
+ const args = parseArgs(argv, config);
23
+
24
+ if (args.help) {
25
+ printHelp(config);
26
+ return;
27
+ }
28
+
29
+ if (args.version) {
30
+ console.log(config.packageJson.version);
31
+ return;
32
+ }
33
+
34
+ if (args.command === 'status') {
35
+ await printStatus(config, args.target);
36
+ return;
37
+ }
38
+
39
+ if (args.command !== 'init') {
40
+ throw new Error(`Unknown command "${args.command}". Run "${config.cliName} --help".`);
41
+ }
42
+
43
+ const targetRoot = path.resolve(args.target);
44
+ const projectName = args.projectName ?? (await inferProjectName(targetRoot));
45
+ const installer = new Installer({
46
+ config,
47
+ targetRoot,
48
+ projectName,
49
+ force: args.force,
50
+ dryRun: args.dryRun
51
+ });
52
+
53
+ await installer.init();
54
+ }
55
+
56
+ function parseArgs(argv, config) {
57
+ const options = {
58
+ command: 'help',
59
+ target: process.cwd(),
60
+ projectName: undefined,
61
+ force: false,
62
+ dryRun: false,
63
+ help: false,
64
+ version: false
65
+ };
66
+
67
+ const positionals = [];
68
+
69
+ for (let index = 0; index < argv.length; index += 1) {
70
+ const arg = argv[index];
71
+
72
+ if (arg === '--help' || arg === '-h') {
73
+ options.help = true;
74
+ continue;
75
+ }
76
+
77
+ if (arg === '--version' || arg === '-v') {
78
+ options.version = true;
79
+ continue;
80
+ }
81
+
82
+ if (arg === '--force') {
83
+ options.force = true;
84
+ continue;
85
+ }
86
+
87
+ if (arg === '--dry-run') {
88
+ options.dryRun = true;
89
+ continue;
90
+ }
91
+
92
+ if (arg.startsWith('--target=')) {
93
+ options.target = arg.slice('--target='.length);
94
+ continue;
95
+ }
96
+
97
+ if (arg === '--target') {
98
+ options.target = readOptionValue(argv, index, '--target');
99
+ index += 1;
100
+ continue;
101
+ }
102
+
103
+ if (arg.startsWith('--project-name=')) {
104
+ options.projectName = arg.slice('--project-name='.length);
105
+ continue;
106
+ }
107
+
108
+ if (arg === '--project-name') {
109
+ options.projectName = readOptionValue(argv, index, '--project-name');
110
+ index += 1;
111
+ continue;
112
+ }
113
+
114
+ if (arg.startsWith('-')) {
115
+ throw new Error(`Unknown option "${arg}". Run "${config.cliName} --help".`);
116
+ }
117
+
118
+ positionals.push(arg);
119
+ }
120
+
121
+ if (positionals.length > 1) {
122
+ throw new Error(`Unexpected arguments: ${positionals.slice(1).join(' ')}`);
123
+ }
124
+
125
+ options.command = positionals[0] ?? (options.help || options.version ? 'meta' : 'help');
126
+ options.help = options.help || options.command === 'help';
127
+
128
+ return options;
129
+ }
130
+
131
+ function readOptionValue(argv, index, optionName) {
132
+ const value = argv[index + 1];
133
+
134
+ if (!value || value.startsWith('-')) {
135
+ throw new Error(`${optionName} requires a value.`);
136
+ }
137
+
138
+ return value;
139
+ }
140
+
141
+ function printHelp(config) {
142
+ console.log(`${config.cliName}
143
+
144
+ Usage:
145
+ ${config.cliName} init [options]
146
+ ${config.cliName} status [options]
147
+
148
+ Options:
149
+ --target <path> Project root to install into. Defaults to cwd.
150
+ --project-name <name> Name used to replace PROJECT_NAME placeholders.
151
+ --force Replace existing generated directories.
152
+ --dry-run Show planned changes without writing files.
153
+ -h, --help Show help.
154
+ -v, --version Show package version.
155
+
156
+ Examples:
157
+ npx ${config.packageJson.name} init --project-name "My App"
158
+ npx ${config.packageJson.name}@${config.packageJson.version} init --project-name "My App"
159
+ npx ${config.packageJson.name} status --target ../existing-project
160
+
161
+ The status command reads ${config.metadataPath} from an initialized project.
162
+ `);
163
+ }
164
+
165
+ async function inferProjectName(targetRoot) {
166
+ try {
167
+ const packageJson = JSON.parse(await readFile(path.join(targetRoot, 'package.json'), 'utf8'));
168
+
169
+ if (typeof packageJson.name === 'string' && packageJson.name.trim()) {
170
+ return packageJson.name.replace(/^@[^/]+\//, '');
171
+ }
172
+ } catch {
173
+ // Fall through to the directory name.
174
+ }
175
+
176
+ return path.basename(targetRoot);
177
+ }
178
+
179
+ class Installer {
180
+ constructor({ config, targetRoot, projectName, force, dryRun }) {
181
+ this.config = config;
182
+ this.templateDir = path.join(config.packageRoot, 'template');
183
+ this.targetRoot = targetRoot;
184
+ this.projectName = projectName;
185
+ this.force = force;
186
+ this.dryRun = dryRun;
187
+ this.actions = [];
188
+ this.agentsFilePath = path.join(targetRoot, 'AGENTS.md');
189
+ }
190
+
191
+ async init() {
192
+ await this.ensureDirectory(this.targetRoot);
193
+
194
+ await this.installAgentsFile();
195
+ await this.installSkills();
196
+ await this.installPayloadRoots();
197
+ await this.writeMetadata();
198
+
199
+ this.printSummary();
200
+ }
201
+
202
+ async installPayloadRoots() {
203
+ const entries = await readdir(this.templateDir, { withFileTypes: true });
204
+
205
+ for (const entry of entries) {
206
+ if (entry.name === 'AGENTS.md' || entry.name === '.agents' || entry.name === '.DS_Store') {
207
+ continue;
208
+ }
209
+
210
+ await this.copyTemplatePath(entry.name, entry.name);
211
+ }
212
+ }
213
+
214
+ async installAgentsFile() {
215
+ const targetFile = await this.findAgentsFile();
216
+ const targetDisplay = path.basename(targetFile);
217
+ this.agentsFilePath = targetFile;
218
+
219
+ const section = this.config.agents.render(this.projectName);
220
+ const { start, end } = this.config.agents;
221
+
222
+ if (!(await exists(targetFile))) {
223
+ const templateAgents = path.join(this.templateDir, 'AGENTS.md');
224
+
225
+ if (await exists(templateAgents)) {
226
+ const text = (await readFile(templateAgents, 'utf8')).replaceAll('PROJECT_NAME', this.projectName);
227
+ await this.writeFile(path.join(this.targetRoot, 'AGENTS.md'), text, 'create AGENTS.md');
228
+ this.agentsFilePath = path.join(this.targetRoot, 'AGENTS.md');
229
+ return;
230
+ }
231
+
232
+ await this.writeFile(path.join(this.targetRoot, 'AGENTS.md'), `${section}\n`, 'create AGENTS.md');
233
+ this.agentsFilePath = path.join(this.targetRoot, 'AGENTS.md');
234
+ return;
235
+ }
236
+
237
+ const current = await readFile(targetFile, 'utf8');
238
+
239
+ if (current.includes(start) && current.includes(end)) {
240
+ const updated = current.replace(
241
+ new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}`),
242
+ section
243
+ );
244
+
245
+ if (updated === current) {
246
+ this.note(`${targetDisplay} already has the ${this.config.summaryLabel} guidance`);
247
+ return;
248
+ }
249
+
250
+ await this.writeFile(targetFile, updated, `update ${targetDisplay} ${this.config.summaryLabel} section`);
251
+ return;
252
+ }
253
+
254
+ const separator = current.endsWith('\n') ? '\n' : '\n\n';
255
+ await this.writeFile(
256
+ targetFile,
257
+ `${current}${separator}${section}\n`,
258
+ `append ${this.config.summaryLabel} guidance to ${targetDisplay}`
259
+ );
260
+ }
261
+
262
+ async findAgentsFile() {
263
+ const canonicalPath = path.join(this.targetRoot, 'AGENTS.md');
264
+
265
+ let entries;
266
+
267
+ try {
268
+ entries = await readdir(this.targetRoot, { withFileTypes: true });
269
+ } catch {
270
+ return canonicalPath;
271
+ }
272
+
273
+ if (entries.some((entry) => entry.isFile() && entry.name === 'AGENTS.md')) {
274
+ return canonicalPath;
275
+ }
276
+
277
+ const match = entries
278
+ .filter((entry) => entry.isFile() && entry.name.toLowerCase() === 'agents.md')
279
+ .map((entry) => entry.name)
280
+ .sort((left, right) => left.localeCompare(right))[0];
281
+
282
+ return match ? path.join(this.targetRoot, match) : canonicalPath;
283
+ }
284
+
285
+ async installSkills() {
286
+ for (const skill of this.config.skills) {
287
+ await this.copyTemplatePath(
288
+ `${agentsSkillsRelative}/${skill.name}`,
289
+ `${agentsSkillsRelative}/${skill.name}`
290
+ );
291
+ }
292
+
293
+ const readmeTarget = await this.findExistingTargetPath(`${agentsSkillsRelative}/README.md`);
294
+ const readmePath = readmeTarget.exists
295
+ ? readmeTarget.path
296
+ : path.join(this.targetRoot, `${agentsSkillsRelative}/README.md`);
297
+ const readmeDisplay = readmeTarget.exists ? readmeTarget.display : `${agentsSkillsRelative}/README.md`;
298
+
299
+ if (!(await exists(readmePath))) {
300
+ await this.copyTemplatePath(`${agentsSkillsRelative}/README.md`, `${agentsSkillsRelative}/README.md`);
301
+ return;
302
+ }
303
+
304
+ const current = await readFile(readmePath, 'utf8');
305
+ const missingSkills = this.config.skills.filter((skill) => !current.includes(skill.name));
306
+
307
+ if (missingSkills.length === 0) {
308
+ this.note(`${readmeDisplay} already lists the ${this.config.summaryLabel} skills`);
309
+ return;
310
+ }
311
+
312
+ const entry = [
313
+ '',
314
+ `## ${this.config.summaryLabel} Skills`,
315
+ '',
316
+ ...missingSkills.map((skill) => skill.readmeEntry),
317
+ ''
318
+ ].join('\n');
319
+
320
+ await this.writeFile(
321
+ readmePath,
322
+ `${current.trimEnd()}\n${entry}`,
323
+ `append ${this.config.summaryLabel} skills to ${readmeDisplay}`
324
+ );
325
+ }
326
+
327
+ async copyTemplatePath(sourceRelative, targetRelative) {
328
+ const sourcePath = path.join(this.templateDir, sourceRelative);
329
+ const targetInfo = await this.findExistingTargetPath(targetRelative);
330
+
331
+ if (!(await exists(sourcePath))) {
332
+ return false;
333
+ }
334
+
335
+ const targetPath = path.join(this.targetRoot, targetRelative);
336
+
337
+ if (targetInfo.exists) {
338
+ const variantNote = targetInfo.caseVariant ? ` at ${targetInfo.display}` : '';
339
+
340
+ if (!this.force) {
341
+ this.note(`skip ${targetRelative} (already exists${variantNote}; use --force to replace)`);
342
+ return false;
343
+ }
344
+
345
+ await this.removePath(targetInfo.path, `replace ${targetInfo.display}`);
346
+ }
347
+
348
+ await this.copyRecursive(sourcePath, targetPath, targetRelative);
349
+ return true;
350
+ }
351
+
352
+ async findExistingTargetPath(targetRelative) {
353
+ const parts = targetRelative.split('/').filter(Boolean);
354
+ let currentPath = this.targetRoot;
355
+ const displayParts = [];
356
+
357
+ for (const part of parts) {
358
+ let entries;
359
+
360
+ try {
361
+ entries = await readdir(currentPath, { withFileTypes: true });
362
+ } catch {
363
+ return this.missingTargetPath(targetRelative);
364
+ }
365
+
366
+ const exactMatch = entries.find((entry) => entry.name === part);
367
+ const caseMatch = exactMatch ?? entries.find((entry) => entry.name.toLowerCase() === part.toLowerCase());
368
+
369
+ if (!caseMatch) {
370
+ return this.missingTargetPath(targetRelative);
371
+ }
372
+
373
+ currentPath = path.join(currentPath, caseMatch.name);
374
+ displayParts.push(caseMatch.name);
375
+ }
376
+
377
+ const display = displayParts.join('/');
378
+
379
+ return {
380
+ caseVariant: display !== targetRelative,
381
+ display,
382
+ exists: true,
383
+ path: currentPath
384
+ };
385
+ }
386
+
387
+ missingTargetPath(targetRelative) {
388
+ return {
389
+ caseVariant: false,
390
+ display: targetRelative,
391
+ exists: false,
392
+ path: path.join(this.targetRoot, targetRelative)
393
+ };
394
+ }
395
+
396
+ async copyRecursive(sourcePath, targetPath, displayPath) {
397
+ const sourceStats = await stat(sourcePath);
398
+
399
+ if (sourceStats.isDirectory()) {
400
+ await this.ensureDirectory(targetPath, `create ${displayPath}/`);
401
+ const entries = await readdir(sourcePath, { withFileTypes: true });
402
+
403
+ for (const entry of entries) {
404
+ if (entry.name === '.DS_Store') {
405
+ continue;
406
+ }
407
+
408
+ await this.copyRecursive(
409
+ path.join(sourcePath, entry.name),
410
+ path.join(targetPath, entry.name),
411
+ path.posix.join(displayPath, entry.name)
412
+ );
413
+ }
414
+
415
+ return;
416
+ }
417
+
418
+ const contents = await readFile(sourcePath);
419
+ const transformed = this.transformText(contents);
420
+ await this.writeFile(targetPath, transformed, `create ${displayPath}`);
421
+ }
422
+
423
+ transformText(contents) {
424
+ return contents.toString('utf8').replaceAll('PROJECT_NAME', this.projectName);
425
+ }
426
+
427
+ async writeMetadata() {
428
+ const metadataPath = path.join(this.targetRoot, this.config.metadataPath);
429
+ const previous = await readOptionalJson(metadataPath);
430
+ const now = new Date().toISOString();
431
+ const metadata = {
432
+ schemaVersion: 1,
433
+ packageName: this.config.packageJson.name,
434
+ installedVersion: this.config.packageJson.version,
435
+ firstInstalledVersion:
436
+ previous?.firstInstalledVersion ?? previous?.installedVersion ?? this.config.packageJson.version,
437
+ firstInstalledAt: previous?.firstInstalledAt ?? previous?.installedAt ?? now,
438
+ lastUpdatedAt: now,
439
+ projectName: this.projectName,
440
+ installedSkills: this.config.skills.map((skill) => skill.name)
441
+ };
442
+
443
+ await this.writeFile(
444
+ metadataPath,
445
+ `${JSON.stringify(metadata, null, 2)}\n`,
446
+ `${previous ? 'update' : 'create'} ${this.config.metadataPath}`
447
+ );
448
+ }
449
+
450
+ async ensureDirectory(directory, message) {
451
+ if (message) {
452
+ this.note(message);
453
+ }
454
+
455
+ if (!this.dryRun) {
456
+ await mkdir(directory, { recursive: true });
457
+ }
458
+ }
459
+
460
+ async removePath(filePath, message) {
461
+ this.note(message);
462
+
463
+ if (!this.dryRun) {
464
+ await rm(filePath, { recursive: true, force: true });
465
+ }
466
+ }
467
+
468
+ async writeFile(filePath, contents, message) {
469
+ this.note(message);
470
+
471
+ if (!this.dryRun) {
472
+ await mkdir(path.dirname(filePath), { recursive: true });
473
+ await writeFile(filePath, contents);
474
+ }
475
+ }
476
+
477
+ note(message) {
478
+ this.actions.push(message);
479
+ }
480
+
481
+ printSummary() {
482
+ const prefix = this.dryRun ? '[dry-run] ' : '';
483
+
484
+ for (const action of this.actions) {
485
+ console.log(`${prefix}${action}`);
486
+ }
487
+
488
+ console.log(`${prefix}${this.config.summaryLabel} ready for ${this.projectName}.`);
489
+
490
+ if (!this.dryRun) {
491
+ console.log(`Next: ask your agent to read ${this.agentsFilePath}.`);
492
+
493
+ for (const step of this.config.nextSteps ?? []) {
494
+ console.log(step);
495
+ }
496
+ }
497
+ }
498
+ }
499
+
500
+ async function printStatus(config, target) {
501
+ const targetRoot = path.resolve(target);
502
+ const metadataPath = path.join(targetRoot, config.metadataPath);
503
+ const metadata = await readOptionalJson(metadataPath);
504
+
505
+ console.log(`${config.summaryLabel} status for ${targetRoot}`);
506
+ console.log(`Running CLI version: ${config.packageJson.version}`);
507
+
508
+ if (!metadata) {
509
+ console.log('Installed metadata: not found');
510
+ console.log(`Metadata path: ${config.metadataPath}`);
511
+ return;
512
+ }
513
+
514
+ console.log(`Installed version: ${metadata.installedVersion ?? 'Unknown'}`);
515
+ console.log(`Project: ${metadata.projectName ?? 'Unknown'}`);
516
+ console.log(`Installed skills: ${formatList(metadata.installedSkills)}`);
517
+ console.log(`Metadata path: ${config.metadataPath}`);
518
+
519
+ if (metadata.installedVersion && metadata.installedVersion !== config.packageJson.version) {
520
+ console.log('Note: running CLI version differs from installed metadata.');
521
+ }
522
+ }
523
+
524
+ function formatList(value) {
525
+ return Array.isArray(value) && value.length > 0 ? value.join(', ') : 'Unknown';
526
+ }
527
+
528
+ async function readOptionalJson(filePath) {
529
+ try {
530
+ return JSON.parse(await readFile(filePath, 'utf8'));
531
+ } catch {
532
+ return undefined;
533
+ }
534
+ }
535
+
536
+ async function exists(filePath) {
537
+ try {
538
+ await access(filePath, fsConstants.F_OK);
539
+ return true;
540
+ } catch {
541
+ return false;
542
+ }
543
+ }
544
+
545
+ function escapeRegExp(value) {
546
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
547
+ }
548
+
549
+ export function reportError(error, cliName) {
550
+ console.error(error.message);
551
+ console.error(`Run "${path.basename(execPath)} ${cliName} --help" for usage.`);
552
+ process.exitCode = 1;
553
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "durable-context",
3
+ "version": "1.0.0",
4
+ "description": "Invocation-only skills and scaffold for durable planning: initiatives in context/, decisions in an append-only log.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "durable-context": "bin/durable-context.js"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "lib/",
13
+ "template/",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "test": "node --test"
19
+ },
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "keywords": [
24
+ "agents",
25
+ "ai",
26
+ "planning",
27
+ "working-context",
28
+ "adr",
29
+ "decisions"
30
+ ]
31
+ }
@@ -0,0 +1,6 @@
1
+ # Project Skills
2
+
3
+ Invocation-only — ask by name.
4
+
5
+ - `plan-with-context` — draft a durable plan in an initiative `plan.md`.
6
+ - `dive-into-plan` — interrogate a settled plan, distribute into initiative docs, promote to `decisions/`.