openspecpm 0.1.0-alpha.0 → 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.
@@ -1,198 +1,201 @@
1
- #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import { audited } from '../src/audit.js';
4
- import { runInit } from '../src/commands/init.js';
5
- import { runDoctor } from '../src/commands/doctor.js';
6
- import { runPropose } from '../src/commands/propose.js';
7
- import { runDecompose } from '../src/commands/decompose.js';
8
- import { runSync } from '../src/commands/sync.js';
9
- import { runComment } from '../src/commands/comment.js';
10
- import { runReconcile } from '../src/commands/reconcile.js';
11
- import { runStatus } from '../src/commands/status.js';
12
- import { runStandup } from '../src/commands/standup.js';
13
- import { runNext } from '../src/commands/next.js';
14
- import { runBlocked } from '../src/commands/blocked.js';
15
- import { runValidate } from '../src/commands/validate.js';
16
- import { runSearch } from '../src/commands/search.js';
17
- import { runFanOut } from '../src/commands/fan-out.js';
18
- import { runBugReport } from '../src/commands/bug-report.js';
19
- import { runShip } from '../src/commands/ship.js';
20
- import { runHelp } from '../src/commands/help.js';
21
- import { runAssign } from '../src/commands/assign.js';
22
- import { runSyncAll, runShipAllReady } from '../src/commands/bulk.js';
23
- import { runWatch } from '../src/commands/watch.js';
24
-
25
- const program = new Command();
26
-
27
- program
28
- .name('openspecpm')
29
- .description('Spec-driven, BDD-shaped project management for AI agents.')
30
- .version('0.1.0-alpha.0');
31
-
32
- program
33
- .command('init')
34
- .description('Interactive wizard: pick a PM tool and write .openspecpm/config.json')
35
- .option('--non-interactive', 'Fail instead of prompting when input is required')
36
- .action((opts) => audited('init', runInit)(opts).catch(fatal));
37
-
38
- program
39
- .command('doctor [adapter]')
40
- .description('Check auth + tooling health for one or all adapters')
41
- .option('--install', 'Print OS-specific install commands for missing CLIs')
42
- .option('--setup-auth', 'Print PAT/token URLs and required scopes')
43
- .action((adapter, opts) => audited('doctor', runDoctor)({ adapter, install: opts.install, setupAuth: opts.setupAuth }).catch(fatal));
44
-
45
- program
46
- .command('propose <feature>')
47
- .description('Create an OpenSpec proposal (proposal.md, design.md, tasks.md, specs/) for <feature>')
48
- .option('-p, --prompt <text>', 'One-line description for the AI to seed the proposal')
49
- .option('-t, --type <type>', 'Change type: feature | bug | refactor | incident', 'feature')
50
- .option('--offline', 'Scaffold from templates without calling the openspec CLI')
51
- .action((feature, opts) => audited('propose', runPropose)({ feature, prompt: opts.prompt, type: opts.type, offline: opts.offline }).catch(fatal));
52
-
53
- program
54
- .command('decompose <feature>')
55
- .description('Extract tasks from proposal + BDD scenarios into tasks.md')
56
- .option('--force', 'Merge into an existing tasks.md instead of refusing')
57
- .action((feature, opts) => audited('decompose', runDecompose)({ feature, force: opts.force }).catch(fatal));
58
-
59
- program
60
- .command('sync [feature]')
61
- .description('Push an OpenSpec change to the configured PM tool (idempotent; BDD-linted)')
62
- .option('--all', 'Sync every change under openspec/changes/')
63
- .option('--dry-run', 'Print the call plan without making remote changes')
64
- .option('--force', 'Bypass BDD lint errors')
65
- .option('--diff', 'Show the call plan in detail')
66
- .option('-y, --yes', 'Skip the confirmation prompt for --all')
67
- .action((feature, opts) => {
68
- if (opts.all) {
69
- return audited('sync-all', runSyncAll)({ dryRun: opts.dryRun, force: opts.force, yes: opts.yes }).catch(fatal);
70
- }
71
- if (!feature) {
72
- process.stderr.write('error: provide a <feature> or pass --all\n');
73
- process.exit(1);
74
- }
75
- return audited('sync', runSync)({ feature, dryRun: opts.dryRun, force: opts.force, diff: opts.diff }).catch(fatal);
76
- });
77
-
78
- program
79
- .command('comment <feature> <task>')
80
- .description('Post a progress comment to the PM tool work item')
81
- .option('-m, --message <text>', 'Inline message (otherwise reads from local progress.md)')
82
- .option('--dry-run', 'Print the comment instead of posting')
83
- .action((feature, task, opts) =>
84
- audited('comment', runComment)({ feature, task, message: opts.message, dryRun: opts.dryRun }).catch(fatal),
85
- );
86
-
87
- program
88
- .command('reconcile <feature>')
89
- .description('Pull remote state back into local task frontmatter')
90
- .option('--dry-run', 'Show drift without writing local changes')
91
- .action((feature, opts) => audited('reconcile', runReconcile)({ feature, dryRun: opts.dryRun }).catch(fatal));
92
-
93
- program
94
- .command('status')
95
- .description('Local snapshot: configured adapter + per-change task counts')
96
- .action(() => audited('status', runStatus)().catch(fatal));
97
-
98
- program
99
- .command('standup')
100
- .description('Show progress updates within a recent window')
101
- .option('--since <window>', 'Time window: e.g. 12h, 2d, 1w', '24h')
102
- .option('--broadcast', 'Also POST to configured Slack/Teams/generic webhooks')
103
- .action((opts) => audited('standup', runStandup)({ since: opts.since, broadcast: opts.broadcast }).catch(fatal));
104
-
105
- program
106
- .command('next')
107
- .description('List tasks ready to start (no unmet dependencies)')
108
- .option('-l, --limit <n>', 'Max items to show', (v) => parseInt(v, 10), 5)
109
- .action((opts) => audited('next', runNext)({ limit: opts.limit }).catch(fatal));
110
-
111
- program
112
- .command('blocked')
113
- .description('List tasks waiting on unmet dependencies')
114
- .action(() => audited('blocked', runBlocked)().catch(fatal));
115
-
116
- program
117
- .command('validate')
118
- .description('Schema + dependency + BDD-lint sweep across every change')
119
- .action(() => audited('validate', runValidate)().catch(fatal));
120
-
121
- program
122
- .command('search <query>')
123
- .description('Grep across proposals, specs, tasks, and progress notes')
124
- .option('-l, --limit <n>', 'Max matches to show', (v) => parseInt(v, 10), 50)
125
- .option('--case-sensitive', 'Match case')
126
- .action((query, opts) =>
127
- audited('search', runSearch)({ query, limit: opts.limit, caseSensitive: opts.caseSensitive }).catch(fatal),
128
- );
129
-
130
- program
131
- .command('fan-out <feature>')
132
- .description('Emit ready-to-paste agent prompts for parallel:true tasks')
133
- .option('-l, --limit <n>', 'Max prompts to emit', (v) => parseInt(v, 10), 5)
134
- .action((feature, opts) => audited('fan-out', runFanOut)({ feature, limit: opts.limit }).catch(fatal));
135
-
136
- program
137
- .command('bug-report <feature> <task>')
138
- .description('File a regression bug linked to a shipped task')
139
- .option('-t, --title <text>', 'Bug title (required)')
140
- .option('-b, --body <text>', 'Bug body')
141
- .action((feature, task, opts) =>
142
- audited('bug-report', runBugReport)({ feature, task, title: opts.title, body: opts.body }).catch(fatal),
143
- );
144
-
145
- program
146
- .command('assign <feature> <task>')
147
- .description('Set assignee / sprint / iteration / area / story-points on a synced task')
148
- .option('--assignee <id>', 'Assignee id (backend-specific)')
149
- .option('--sprint <id>', 'Sprint / cycle / milestone id (whichever the backend supports)')
150
- .option('--iteration <path>', 'Iteration path (Azure DevOps; alias for --sprint elsewhere)')
151
- .option('--area <path>', 'Area path (Azure DevOps)')
152
- .option('--story-points <n>', 'Story points / estimate / weight')
153
- .action((feature, task, opts) =>
154
- audited('assign', runAssign)({
155
- feature, task,
156
- assignee: opts.assignee, sprint: opts.sprint, iteration: opts.iteration,
157
- area: opts.area, storyPoints: opts.storyPoints,
158
- }).catch(fatal),
159
- );
160
-
161
- program
162
- .command('ship [feature]')
163
- .description('Close all work items for <feature> and archive the OpenSpec change')
164
- .option('--all-ready', 'Ship every change whose tasks are all synced (no pending/failed)')
165
- .option('-y, --yes', 'Skip the confirmation prompt')
166
- .option('--skip-archive', 'Close work items but leave openspec/changes/<feature>/ in place')
167
- .action((feature, opts) => {
168
- if (opts.allReady) {
169
- return audited('ship-all-ready', runShipAllReady)({ yes: opts.yes, skipArchive: opts.skipArchive }).catch(fatal);
170
- }
171
- if (!feature) {
172
- process.stderr.write('error: provide a <feature> or pass --all-ready\n');
173
- process.exit(1);
174
- }
175
- return audited('ship', runShip)({ feature, yes: opts.yes, skipArchive: opts.skipArchive }).catch(fatal);
176
- });
177
-
178
- program
179
- .command('watch [feature]')
180
- .description('Re-lint on file change. Use --all to validate the whole project on every change.')
181
- .option('--all', 'Validate every change instead of linting one feature')
182
- .option('--debounce <ms>', 'Debounce window in milliseconds', (v) => parseInt(v, 10), 300)
183
- .action((feature, opts) =>
184
- audited('watch', runWatch)({ feature, allChanges: opts.all, debounceMs: opts.debounce }).catch(fatal),
185
- );
186
-
187
- program
188
- .command('help-table [topic]')
189
- .description('Context-aware help grouped by workflow phase')
190
- .action((topic) => { runHelp({ topic }); });
191
-
192
- program.parseAsync(process.argv);
193
-
194
- function fatal(err) {
195
- process.stderr.write(`\n✖ ${err.message}\n`);
196
- if (err.remediation) process.stderr.write(` → ${err.remediation}\n`);
197
- process.exit(1);
198
- }
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { audited } from '../src/audit.js';
4
+ import { runInit } from '../src/commands/init.js';
5
+ import { runDoctor } from '../src/commands/doctor.js';
6
+ import { runPropose } from '../src/commands/propose.js';
7
+ import { runDecompose } from '../src/commands/decompose.js';
8
+ import { runSync } from '../src/commands/sync.js';
9
+ import { runComment } from '../src/commands/comment.js';
10
+ import { runReconcile } from '../src/commands/reconcile.js';
11
+ import { runStatus } from '../src/commands/status.js';
12
+ import { runStandup } from '../src/commands/standup.js';
13
+ import { runNext } from '../src/commands/next.js';
14
+ import { runBlocked } from '../src/commands/blocked.js';
15
+ import { runValidate } from '../src/commands/validate.js';
16
+ import { runSearch } from '../src/commands/search.js';
17
+ import { runFanOut } from '../src/commands/fan-out.js';
18
+ import { runBugReport } from '../src/commands/bug-report.js';
19
+ import { runShip } from '../src/commands/ship.js';
20
+ import { runHelp } from '../src/commands/help.js';
21
+ import { runAssign } from '../src/commands/assign.js';
22
+ import { runSyncAll, runShipAllReady } from '../src/commands/bulk.js';
23
+ import { runWatch } from '../src/commands/watch.js';
24
+
25
+ const program = new Command();
26
+
27
+ program
28
+ .name('openspecpm')
29
+ .description('Spec-driven, BDD-shaped project management for AI agents.')
30
+ .version('0.1.0-alpha.0');
31
+
32
+ program
33
+ .command('init')
34
+ .description('Interactive wizard: pick a PM tool and write .openspecpm/config.json')
35
+ .option('--non-interactive', 'Fail instead of prompting when input is required')
36
+ .action((opts) => audited('init', runInit)(opts).catch(fatal));
37
+
38
+ program
39
+ .command('doctor [adapter]')
40
+ .description('Check auth + tooling health for one or all adapters')
41
+ .option('--install', 'Print OS-specific install commands for missing CLIs')
42
+ .option('--setup-auth', 'Print PAT/token URLs and required scopes')
43
+ .action((adapter, opts) => audited('doctor', runDoctor)({ adapter, install: opts.install, setupAuth: opts.setupAuth }).catch(fatal));
44
+
45
+ program
46
+ .command('propose <feature>')
47
+ .description('Create an OpenSpec proposal (proposal.md, design.md, tasks.md, specs/) for <feature>')
48
+ .option('-p, --prompt <text>', 'One-line description for the AI to seed the proposal')
49
+ .option('-t, --type <type>', 'Change type: feature | bug | refactor | incident', 'feature')
50
+ .option('--offline', 'Scaffold from templates without calling the openspec CLI')
51
+ .option('--llm', 'Augment BDD soft-lint with the LLM judge (requires ANTHROPIC_API_KEY)')
52
+ .action((feature, opts) => audited('propose', runPropose)({ feature, prompt: opts.prompt, type: opts.type, offline: opts.offline, llm: opts.llm }).catch(fatal));
53
+
54
+ program
55
+ .command('decompose <feature>')
56
+ .description('Extract tasks from proposal + BDD scenarios into tasks.md')
57
+ .option('--force', 'Merge into an existing tasks.md instead of refusing')
58
+ .action((feature, opts) => audited('decompose', runDecompose)({ feature, force: opts.force }).catch(fatal));
59
+
60
+ program
61
+ .command('sync [feature]')
62
+ .description('Push an OpenSpec change to the configured PM tool (idempotent; BDD-linted)')
63
+ .option('--all', 'Sync every change under openspec/changes/')
64
+ .option('--dry-run', 'Print the call plan without making remote changes')
65
+ .option('--force', 'Bypass BDD lint errors')
66
+ .option('--diff', 'Show the call plan in detail')
67
+ .option('--llm', 'Augment BDD hard-lint with the LLM judge (requires ANTHROPIC_API_KEY)')
68
+ .option('-y, --yes', 'Skip the confirmation prompt for --all')
69
+ .action((feature, opts) => {
70
+ if (opts.all) {
71
+ return audited('sync-all', runSyncAll)({ dryRun: opts.dryRun, force: opts.force, yes: opts.yes }).catch(fatal);
72
+ }
73
+ if (!feature) {
74
+ process.stderr.write('error: provide a <feature> or pass --all\n');
75
+ process.exit(1);
76
+ }
77
+ return audited('sync', runSync)({ feature, dryRun: opts.dryRun, force: opts.force, diff: opts.diff, llm: opts.llm }).catch(fatal);
78
+ });
79
+
80
+ program
81
+ .command('comment <feature> <task>')
82
+ .description('Post a progress comment to the PM tool work item')
83
+ .option('-m, --message <text>', 'Inline message (otherwise reads from local progress.md)')
84
+ .option('--dry-run', 'Print the comment instead of posting')
85
+ .action((feature, task, opts) =>
86
+ audited('comment', runComment)({ feature, task, message: opts.message, dryRun: opts.dryRun }).catch(fatal),
87
+ );
88
+
89
+ program
90
+ .command('reconcile <feature>')
91
+ .description('Pull remote state back into local task frontmatter')
92
+ .option('--dry-run', 'Show drift without writing local changes')
93
+ .action((feature, opts) => audited('reconcile', runReconcile)({ feature, dryRun: opts.dryRun }).catch(fatal));
94
+
95
+ program
96
+ .command('status')
97
+ .description('Local snapshot: configured adapter + per-change task counts')
98
+ .action(() => audited('status', runStatus)().catch(fatal));
99
+
100
+ program
101
+ .command('standup')
102
+ .description('Show progress updates within a recent window')
103
+ .option('--since <window>', 'Time window: e.g. 12h, 2d, 1w', '24h')
104
+ .option('--broadcast', 'Also POST to configured Slack/Teams/generic webhooks')
105
+ .action((opts) => audited('standup', runStandup)({ since: opts.since, broadcast: opts.broadcast }).catch(fatal));
106
+
107
+ program
108
+ .command('next')
109
+ .description('List tasks ready to start (no unmet dependencies)')
110
+ .option('-l, --limit <n>', 'Max items to show', (v) => parseInt(v, 10), 5)
111
+ .action((opts) => audited('next', runNext)({ limit: opts.limit }).catch(fatal));
112
+
113
+ program
114
+ .command('blocked')
115
+ .description('List tasks waiting on unmet dependencies')
116
+ .action(() => audited('blocked', runBlocked)().catch(fatal));
117
+
118
+ program
119
+ .command('validate')
120
+ .description('Schema + dependency + BDD-lint sweep across every change')
121
+ .option('--llm', 'Augment BDD lint with the LLM judge (requires ANTHROPIC_API_KEY)')
122
+ .action((opts) => audited('validate', runValidate)({ llm: opts.llm }).catch(fatal));
123
+
124
+ program
125
+ .command('search <query>')
126
+ .description('Grep across proposals, specs, tasks, and progress notes')
127
+ .option('-l, --limit <n>', 'Max matches to show', (v) => parseInt(v, 10), 50)
128
+ .option('--case-sensitive', 'Match case')
129
+ .action((query, opts) =>
130
+ audited('search', runSearch)({ query, limit: opts.limit, caseSensitive: opts.caseSensitive }).catch(fatal),
131
+ );
132
+
133
+ program
134
+ .command('fan-out <feature>')
135
+ .description('Emit ready-to-paste agent prompts for parallel:true tasks')
136
+ .option('-l, --limit <n>', 'Max prompts to emit', (v) => parseInt(v, 10), 5)
137
+ .action((feature, opts) => audited('fan-out', runFanOut)({ feature, limit: opts.limit }).catch(fatal));
138
+
139
+ program
140
+ .command('bug-report <feature> <task>')
141
+ .description('File a regression bug linked to a shipped task')
142
+ .option('-t, --title <text>', 'Bug title (required)')
143
+ .option('-b, --body <text>', 'Bug body')
144
+ .action((feature, task, opts) =>
145
+ audited('bug-report', runBugReport)({ feature, task, title: opts.title, body: opts.body }).catch(fatal),
146
+ );
147
+
148
+ program
149
+ .command('assign <feature> <task>')
150
+ .description('Set assignee / sprint / iteration / area / story-points on a synced task')
151
+ .option('--assignee <id>', 'Assignee id (backend-specific)')
152
+ .option('--sprint <id>', 'Sprint / cycle / milestone id (whichever the backend supports)')
153
+ .option('--iteration <path>', 'Iteration path (Azure DevOps; alias for --sprint elsewhere)')
154
+ .option('--area <path>', 'Area path (Azure DevOps)')
155
+ .option('--story-points <n>', 'Story points / estimate / weight')
156
+ .action((feature, task, opts) =>
157
+ audited('assign', runAssign)({
158
+ feature, task,
159
+ assignee: opts.assignee, sprint: opts.sprint, iteration: opts.iteration,
160
+ area: opts.area, storyPoints: opts.storyPoints,
161
+ }).catch(fatal),
162
+ );
163
+
164
+ program
165
+ .command('ship [feature]')
166
+ .description('Close all work items for <feature> and archive the OpenSpec change')
167
+ .option('--all-ready', 'Ship every change whose tasks are all synced (no pending/failed)')
168
+ .option('-y, --yes', 'Skip the confirmation prompt')
169
+ .option('--skip-archive', 'Close work items but leave openspec/changes/<feature>/ in place')
170
+ .action((feature, opts) => {
171
+ if (opts.allReady) {
172
+ return audited('ship-all-ready', runShipAllReady)({ yes: opts.yes, skipArchive: opts.skipArchive }).catch(fatal);
173
+ }
174
+ if (!feature) {
175
+ process.stderr.write('error: provide a <feature> or pass --all-ready\n');
176
+ process.exit(1);
177
+ }
178
+ return audited('ship', runShip)({ feature, yes: opts.yes, skipArchive: opts.skipArchive }).catch(fatal);
179
+ });
180
+
181
+ program
182
+ .command('watch [feature]')
183
+ .description('Re-lint on file change. Use --all to validate the whole project on every change.')
184
+ .option('--all', 'Validate every change instead of linting one feature')
185
+ .option('--debounce <ms>', 'Debounce window in milliseconds', (v) => parseInt(v, 10), 300)
186
+ .action((feature, opts) =>
187
+ audited('watch', runWatch)({ feature, allChanges: opts.all, debounceMs: opts.debounce }).catch(fatal),
188
+ );
189
+
190
+ program
191
+ .command('help-table [topic]')
192
+ .description('Context-aware help grouped by workflow phase')
193
+ .action((topic) => { runHelp({ topic }); });
194
+
195
+ program.parseAsync(process.argv);
196
+
197
+ function fatal(err) {
198
+ process.stderr.write(`\n✖ ${err.message}\n`);
199
+ if (err.remediation) process.stderr.write(` → ${err.remediation}\n`);
200
+ process.exit(1);
201
+ }
package/cli/src/audit.js CHANGED
@@ -9,7 +9,7 @@ export function auditPath(cwd = process.cwd()) {
9
9
  return join(cwd, DIR, FILE);
10
10
  }
11
11
 
12
- export async function record({ command, args = {}, result = null, error = null, cwd = process.cwd() } = {}) {
12
+ export async function record({ command, args = {}, result = null, error = null, meta = null, cwd = process.cwd() } = {}) {
13
13
  if (!command) return;
14
14
  const path = auditPath(cwd);
15
15
  await mkdir(dirname(path), { recursive: true });
@@ -20,6 +20,7 @@ export async function record({ command, args = {}, result = null, error = null,
20
20
  result: result ? truncate(result, 500) : null,
21
21
  error: error ? truncate(typeof error === 'string' ? error : error.message ?? String(error), 500) : null,
22
22
  };
23
+ if (meta && typeof meta === 'object') entry.meta = scrub(meta);
23
24
  await appendFile(path, JSON.stringify(entry) + '\n', 'utf8');
24
25
  }
25
26
 
@@ -33,14 +34,22 @@ export async function tail(n = 50, cwd = process.cwd()) {
33
34
  });
34
35
  }
35
36
 
36
- const SECRET_KEYS = /token|secret|password|pat|api[_-]?key|auth|credential/i;
37
+ const SECRET_SEGMENTS = new Set(['token', 'secret', 'password', 'pat', 'auth', 'credential']);
38
+
39
+ function isSecretKey(k) {
40
+ if (/api[_-]?key/i.test(k)) return true;
41
+ for (const seg of k.toLowerCase().split(/[^a-z]+/)) {
42
+ if (seg && SECRET_SEGMENTS.has(seg)) return true;
43
+ }
44
+ return false;
45
+ }
37
46
 
38
47
  function scrub(obj) {
39
48
  if (!obj || typeof obj !== 'object') return obj;
40
49
  if (Array.isArray(obj)) return obj.map(scrub);
41
50
  const out = {};
42
51
  for (const [k, v] of Object.entries(obj)) {
43
- if (SECRET_KEYS.test(k)) {
52
+ if (isSecretKey(k)) {
44
53
  out[k] = '<redacted>';
45
54
  } else if (v && typeof v === 'object') {
46
55
  out[k] = scrub(v);