spets 0.1.2 → 0.1.4

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/dist/index.js CHANGED
@@ -3,7 +3,9 @@ import {
3
3
  generateTaskId,
4
4
  getGitHubConfig,
5
5
  getOutputPath,
6
+ getOutputsDir,
6
7
  getSpetsDir,
8
+ getStepsDir,
7
9
  getWorkflowState,
8
10
  listTasks,
9
11
  loadConfig,
@@ -14,7 +16,7 @@ import {
14
16
  saveTaskMetadata,
15
17
  spetsExists,
16
18
  updateDocumentStatus
17
- } from "./chunk-XYU22TND.js";
19
+ } from "./chunk-YK5ZZE4P.js";
18
20
 
19
21
  // src/index.ts
20
22
  import { Command } from "commander";
@@ -23,7 +25,24 @@ import { Command } from "commander";
23
25
  import { mkdirSync, writeFileSync } from "fs";
24
26
  import { join, dirname } from "path";
25
27
  import { fileURLToPath } from "url";
28
+ import { execSync } from "child_process";
26
29
  var __dirname = dirname(fileURLToPath(import.meta.url));
30
+ function getGitHubInfoFromRemote() {
31
+ try {
32
+ const remoteUrl = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
33
+ const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
34
+ if (sshMatch) {
35
+ return { owner: sshMatch[1], repo: sshMatch[2] };
36
+ }
37
+ const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?$/);
38
+ if (httpsMatch) {
39
+ return { owner: httpsMatch[1], repo: httpsMatch[2] };
40
+ }
41
+ return null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
27
46
  async function initCommand(options) {
28
47
  const cwd = process.cwd();
29
48
  const spetsDir = getSpetsDir(cwd);
@@ -36,17 +55,21 @@ async function initCommand(options) {
36
55
  mkdirSync(join(spetsDir, "outputs"), { recursive: true });
37
56
  mkdirSync(join(spetsDir, "hooks"), { recursive: true });
38
57
  const templatesDir = join(__dirname, "..", "templates");
39
- writeFileSync(join(spetsDir, "config.yml"), getDefaultConfig());
58
+ const githubInfo = getGitHubInfoFromRemote();
59
+ writeFileSync(join(spetsDir, "config.yml"), getDefaultConfig(githubInfo));
40
60
  createDefaultSteps(spetsDir);
61
+ createClaudeCommand(cwd);
41
62
  console.log("Initialized spets in .spets/");
42
63
  console.log("");
43
64
  console.log("Created:");
44
- console.log(" .spets/config.yml - Workflow configuration");
45
- console.log(" .spets/steps/01-plan/ - Planning step");
46
- console.log(" .spets/steps/02-implement/ - Implementation step");
65
+ console.log(" .spets/config.yml - Workflow configuration");
66
+ console.log(" .spets/steps/01-plan/ - Planning step");
67
+ console.log(" .spets/steps/02-implement/ - Implementation step");
68
+ console.log(" .claude/commands/spets.md - Claude Code command");
47
69
  if (options.github) {
48
70
  createGitHubWorkflow(cwd);
49
- console.log(" .github/workflows/spets.yml - GitHub Actions workflow");
71
+ console.log(" .github/workflows/spets.yml - GitHub Actions workflow");
72
+ console.log(" .github/ISSUE_TEMPLATE/spets-task.yml - Issue template");
50
73
  }
51
74
  console.log("");
52
75
  console.log("Next steps:");
@@ -54,19 +77,30 @@ async function initCommand(options) {
54
77
  console.log(" 2. Customize step instructions in .spets/steps/");
55
78
  if (options.github) {
56
79
  console.log(" 3. Add ANTHROPIC_API_KEY to your repo secrets");
57
- console.log(' 4. Run: spets start "task" --platform github --owner <owner> --repo <repo> --issue <n>');
80
+ console.log(' 4. Create a new Issue using the "Spets Task" template');
58
81
  } else {
59
82
  console.log(' 3. Run: spets start "your task description"');
60
83
  }
61
84
  }
62
- function getDefaultConfig() {
85
+ function getDefaultConfig(githubInfo) {
86
+ const githubSection = githubInfo ? `
87
+ # GitHub integration (auto-detected from git remote)
88
+ github:
89
+ owner: ${githubInfo.owner}
90
+ repo: ${githubInfo.repo}
91
+ ` : `
92
+ # GitHub integration (uncomment and fill in to enable)
93
+ # github:
94
+ # owner: your-org
95
+ # repo: your-repo
96
+ `;
63
97
  return `# Spets Configuration
64
98
  # Define your workflow steps here
65
99
 
66
100
  steps:
67
101
  - 01-plan
68
102
  - 02-implement
69
-
103
+ ${githubSection}
70
104
  # Optional hooks (shell scripts)
71
105
  # hooks:
72
106
  # preStep: "./hooks/pre-step.sh"
@@ -237,31 +271,182 @@ None / List any deviations with justification.
237
271
  - Follow-up task 2
238
272
  `;
239
273
  }
274
+ function createClaudeCommand(cwd) {
275
+ const commandDir = join(cwd, ".claude", "commands");
276
+ mkdirSync(commandDir, { recursive: true });
277
+ writeFileSync(join(commandDir, "spets.md"), getClaudeCommand());
278
+ }
279
+ function getClaudeCommand() {
280
+ return `# Spets - Spec Driven Development
281
+
282
+ Spec-Driven Development workflow execution skill for Claude Code.
283
+
284
+ ---
285
+
286
+ ## CRITICAL: Execution Model
287
+
288
+ This skill runs **deterministically** within the current Claude Code session.
289
+ - NO subprocess spawning for AI/LLM calls
290
+ - The orchestrator manages state via JSON protocol
291
+ - Each step follows a strict state machine flow
292
+
293
+ ---
294
+
295
+ ## Orchestrator Commands
296
+
297
+ All workflow state is managed by the orchestrator CLI:
298
+
299
+ \`\`\`bash
300
+ npx spets orchestrate init "<description>" # Start new workflow
301
+ npx spets orchestrate done <taskId> # Mark step complete
302
+ npx spets orchestrate clarified <taskId> '<json>' # Submit answers
303
+ npx spets orchestrate approve <taskId> # Approve and continue
304
+ npx spets orchestrate revise <taskId> "<feedback>" # Request revision
305
+ npx spets orchestrate reject <taskId> # Stop workflow
306
+ npx spets orchestrate stop <taskId> # Pause (can resume)
307
+ npx spets orchestrate status <taskId> # Get current status
308
+ \`\`\`
309
+
310
+ ---
311
+
312
+ ## Execution Flow
313
+
314
+ ### 1. Workflow Start (FIRST ACTION - NO EXCEPTIONS)
315
+
316
+ When skill is loaded, **immediately** run:
317
+
318
+ \`\`\`bash
319
+ npx spets orchestrate init "{user_request}"
320
+ \`\`\`
321
+
322
+ ### 2. Document Generation (type: step)
323
+
324
+ When you receive \`type: "step"\`:
325
+
326
+ 1. **Read the instruction file** from context.instruction
327
+ 2. **Read the template file** from context.template (if exists)
328
+ 3. **Read previous output** from context.previousOutput (if exists)
329
+ 4. **Generate the document** following the instruction and template
330
+ 5. **Write the document** to context.output
331
+ 6. **Mark step as done**: \`npx spets orchestrate done {taskId}\`
332
+
333
+ ### 3. Question Resolution (type: checkpoint, checkpoint: clarify)
334
+
335
+ 1. **Use AskUserQuestion** to ask the questions
336
+ 2. **Collect answers** into JSON format
337
+ 3. **Submit answers**: \`npx spets orchestrate clarified {taskId} '<answers_json>'\`
338
+
339
+ ### 4. Approval Request (type: checkpoint, checkpoint: approve)
340
+
341
+ 1. **Display document summary** from specPath
342
+ 2. **Use AskUserQuestion** with options: Approve, Revise, Reject, Stop
343
+ 3. **Execute the selected action**
344
+
345
+ ### 5. Workflow Complete (type: complete)
346
+
347
+ Display completion message based on status (completed, stopped, rejected)
348
+
349
+ ---
350
+
351
+ ## Response Type Actions Summary
352
+
353
+ | Type | Checkpoint | Action |
354
+ |------|------------|--------|
355
+ | step | - | Generate document, write to output, call done |
356
+ | checkpoint | clarify | Ask questions, call clarified with answers |
357
+ | checkpoint | approve | Show doc, ask for decision, call appropriate command |
358
+ | complete | - | Show final message, end skill |
359
+ | error | - | Show error, end skill |
360
+
361
+ ---
362
+
363
+ ## Critical Implementation Rules
364
+
365
+ 1. **ORCHESTRATOR FIRST** - Always call orchestrator before any action
366
+ 2. **ONE STEP AT A TIME** - Complete one step, get approval, then next
367
+ 3. **DETERMINISTIC FLOW** - Follow the exact state machine transitions
368
+ 4. **NO PROCESS SPAWN** - Generate documents in current session
369
+ 5. **WAIT FOR APPROVAL** - Never proceed without user approval
370
+
371
+ ---
372
+
373
+ ## Example Session
374
+
375
+ \`\`\`
376
+ User: /spets Add user authentication
377
+
378
+ [Skill calls: npx spets orchestrate init "Add user authentication"]
379
+ [Receives: type=step, step=01-plan]
380
+
381
+ [Skill reads instruction.md and template.md]
382
+ [Skill generates plan document]
383
+ [Skill writes to .spets/outputs/.../01-plan.md]
384
+ [Skill calls: npx spets orchestrate done mku3abc-xyz]
385
+
386
+ [Receives: type=checkpoint, checkpoint=approve]
387
+ [Skill shows document summary]
388
+ [Skill asks user: Approve/Revise/Reject/Stop?]
389
+
390
+ User: Approve
391
+
392
+ [Skill calls: npx spets orchestrate approve mku3abc-xyz]
393
+ [Receives: type=step, step=02-implement]
394
+
395
+ [... continues with implementation step ...]
396
+ \`\`\`
397
+
398
+ $ARGUMENTS
399
+ description: The task description for the workflow
400
+ `;
401
+ }
240
402
  function createGitHubWorkflow(cwd) {
241
403
  const workflowDir = join(cwd, ".github", "workflows");
404
+ const templateDir = join(cwd, ".github", "ISSUE_TEMPLATE");
242
405
  mkdirSync(workflowDir, { recursive: true });
406
+ mkdirSync(templateDir, { recursive: true });
243
407
  writeFileSync(join(workflowDir, "spets.yml"), getGitHubWorkflow());
408
+ writeFileSync(join(templateDir, "spets-task.yml"), getIssueTemplate());
409
+ }
410
+ function getIssueTemplate() {
411
+ return `name: Spets Task
412
+ description: Start a Spets workflow
413
+ labels: ["spets"]
414
+ body:
415
+ - type: input
416
+ id: task
417
+ attributes:
418
+ label: Task Description
419
+ description: What do you want to accomplish?
420
+ placeholder: "Add user authentication"
421
+ validations:
422
+ required: true
423
+ - type: input
424
+ id: branch
425
+ attributes:
426
+ label: Branch Name
427
+ description: Leave empty to auto-generate (spets/<issue-number>)
428
+ placeholder: "feature/my-feature"
429
+ validations:
430
+ required: false
431
+ `;
244
432
  }
245
433
  function getGitHubWorkflow() {
246
434
  const gh = (expr) => `\${{ ${expr} }}`;
247
435
  return `# Spets GitHub Action
248
- # Runs Claude Code to handle /approve, /revise, /reject commands
436
+ # Handles workflow start from Issue creation and commands from comments
249
437
 
250
438
  name: Spets Workflow
251
439
 
252
440
  on:
441
+ issues:
442
+ types: [opened]
253
443
  issue_comment:
254
444
  types: [created]
255
445
 
256
446
  jobs:
257
- handle-spets-command:
258
- # Only run if comment contains a spets command
259
- if: |
260
- contains(github.event.comment.body, '/approve') ||
261
- contains(github.event.comment.body, '/revise') ||
262
- contains(github.event.comment.body, '/reject') ||
263
- contains(github.event.comment.body, '/answer')
264
-
447
+ # Start workflow when a spets Issue is created
448
+ start-workflow:
449
+ if: github.event.action == 'opened' && contains(github.event.issue.labels.*.name, 'spets')
265
450
  runs-on: ubuntu-latest
266
451
 
267
452
  steps:
@@ -270,6 +455,30 @@ jobs:
270
455
  with:
271
456
  fetch-depth: 0
272
457
 
458
+ - name: Parse Issue body
459
+ id: parse
460
+ env:
461
+ ISSUE_BODY: ${gh("github.event.issue.body")}
462
+ ISSUE_NUMBER: ${gh("github.event.issue.number")}
463
+ run: |
464
+ # Parse task description
465
+ TASK=$(echo "$ISSUE_BODY" | sed -n '/### Task Description/,/###/{/###/!p;}' | sed '/^$/d' | head -1)
466
+ echo "task=$TASK" >> $GITHUB_OUTPUT
467
+
468
+ # Parse branch name (optional)
469
+ BRANCH=$(echo "$ISSUE_BODY" | sed -n '/### Branch Name/,/###/{/###/!p;}' | sed '/^$/d' | head -1)
470
+ if [ -z "$BRANCH" ]; then
471
+ BRANCH="spets/$ISSUE_NUMBER"
472
+ fi
473
+ echo "branch=$BRANCH" >> $GITHUB_OUTPUT
474
+
475
+ - name: Create and checkout branch
476
+ run: |
477
+ git checkout -b ${gh("steps.parse.outputs.branch")}
478
+ git push -u origin ${gh("steps.parse.outputs.branch")}
479
+ env:
480
+ GH_TOKEN: ${gh("secrets.GITHUB_TOKEN")}
481
+
273
482
  - name: Setup Node.js
274
483
  uses: actions/setup-node@v4
275
484
  with:
@@ -281,43 +490,69 @@ jobs:
281
490
  - name: Install dependencies
282
491
  run: npm ci
283
492
 
284
- - name: Determine context
285
- id: context
493
+ - name: Start Spets workflow
494
+ run: |
495
+ npx spets start "$TASK" --github --issue ${gh("github.event.issue.number")}
496
+ env:
497
+ TASK: ${gh("steps.parse.outputs.task")}
498
+ ANTHROPIC_API_KEY: ${gh("secrets.ANTHROPIC_API_KEY")}
499
+ GH_TOKEN: ${gh("secrets.GITHUB_TOKEN")}
500
+
501
+ - name: Push changes
502
+ run: |
503
+ git config user.name "github-actions[bot]"
504
+ git config user.email "github-actions[bot]@users.noreply.github.com"
505
+ git add -A
506
+ git diff --staged --quiet || git commit -m "Spets: Start workflow for #${gh("github.event.issue.number")}"
507
+ git push
508
+ env:
509
+ GH_TOKEN: ${gh("secrets.GITHUB_TOKEN")}
510
+
511
+ # Handle commands from Issue/PR comments
512
+ handle-command:
513
+ if: |
514
+ github.event.action == 'created' && (
515
+ contains(github.event.comment.body, '/approve') ||
516
+ contains(github.event.comment.body, '/revise') ||
517
+ contains(github.event.comment.body, '/reject')
518
+ )
519
+ runs-on: ubuntu-latest
520
+
521
+ steps:
522
+ - name: Find linked branch
523
+ id: branch
286
524
  run: |
287
- if [ -n "${gh("github.event.issue.pull_request")}" ]; then
288
- echo "type=pr" >> $GITHUB_OUTPUT
289
- else
290
- echo "type=issue" >> $GITHUB_OUTPUT
525
+ # Try to find branch linked to this issue
526
+ BRANCH=$(gh api repos/${gh("github.repository")}/issues/${gh("github.event.issue.number")} --jq '.body' | grep -oP 'spets/\\d+' || echo "")
527
+ if [ -z "$BRANCH" ]; then
528
+ BRANCH="spets/${gh("github.event.issue.number")}"
291
529
  fi
292
- echo "number=${gh("github.event.issue.number")}" >> $GITHUB_OUTPUT
530
+ echo "name=$BRANCH" >> $GITHUB_OUTPUT
531
+ env:
532
+ GH_TOKEN: ${gh("secrets.GITHUB_TOKEN")}
533
+
534
+ - name: Checkout
535
+ uses: actions/checkout@v4
536
+ with:
537
+ ref: ${gh("steps.branch.outputs.name")}
538
+ fetch-depth: 0
293
539
 
294
- - name: Run Claude Code
540
+ - name: Setup Node.js
541
+ uses: actions/setup-node@v4
542
+ with:
543
+ node-version: '20'
544
+
545
+ - name: Install Claude Code
546
+ run: npm install -g @anthropic-ai/claude-code
547
+
548
+ - name: Install dependencies
549
+ run: npm ci
550
+
551
+ - name: Run Spets command
295
552
  run: |
296
- claude -p "
297
- You are running a Spets workflow via GitHub Actions.
298
-
299
- Context:
300
- - Repository: ${gh("github.repository")}
301
- - Issue/PR: #${gh("github.event.issue.number")}
302
- - Comment author: ${gh("github.event.comment.user.login")}
303
- - Command received: $COMMENT_BODY
304
-
305
- Instructions:
306
- 1. Parse the command from the comment (/approve, /revise, /reject, or /answer)
307
- 2. Find the active spets task in .spets/outputs/
308
- 3. Execute the appropriate action:
309
- - /approve: Mark current step as approved, generate next step
310
- - /revise <feedback>: Regenerate current step with feedback
311
- - /reject: Mark workflow as rejected
312
- - /answer: Process answers and continue
313
- 4. If generating content (plan or implementation), actually write the code/changes
314
- 5. Post a summary comment to the PR/Issue using gh cli
315
- 6. Commit any changes with a descriptive message
316
-
317
- Use 'gh issue comment' or 'gh pr comment' to post updates.
318
- "
553
+ npx spets github --issue ${gh("github.event.issue.number")} --comment "$COMMENT"
319
554
  env:
320
- COMMENT_BODY: ${gh("github.event.comment.body")}
555
+ COMMENT: ${gh("github.event.comment.body")}
321
556
  ANTHROPIC_API_KEY: ${gh("secrets.ANTHROPIC_API_KEY")}
322
557
  GH_TOKEN: ${gh("secrets.GITHUB_TOKEN")}
323
558
 
@@ -326,7 +561,7 @@ jobs:
326
561
  git config user.name "github-actions[bot]"
327
562
  git config user.email "github-actions[bot]@users.noreply.github.com"
328
563
  git add -A
329
- git diff --staged --quiet || git commit -m "Spets: Update from workflow"
564
+ git diff --staged --quiet || git commit -m "Spets: Update from #${gh("github.event.issue.number")}"
330
565
  git push
331
566
  env:
332
567
  GH_TOKEN: ${gh("secrets.GITHUB_TOKEN")}
@@ -592,7 +827,7 @@ var Executor = class {
592
827
  };
593
828
 
594
829
  // src/platform/cli.ts
595
- import { spawn as spawn2, execSync } from "child_process";
830
+ import { spawn as spawn2, execSync as execSync2 } from "child_process";
596
831
  import { input, select, confirm } from "@inquirer/prompts";
597
832
 
598
833
  // src/platform/interface.ts
@@ -744,7 +979,7 @@ var CliPlatform = class extends BasePlatform {
744
979
  isClaudeInstalled() {
745
980
  try {
746
981
  const command = process.platform === "win32" ? "where" : "which";
747
- execSync(`${command} claude`, { stdio: "ignore" });
982
+ execSync2(`${command} claude`, { stdio: "ignore" });
748
983
  return true;
749
984
  } catch {
750
985
  return false;
@@ -1019,6 +1254,83 @@ var GitHubPlatform = class extends BasePlatform {
1019
1254
  };
1020
1255
 
1021
1256
  // src/commands/start.ts
1257
+ import { execSync as execSync3 } from "child_process";
1258
+ function getGitHubInfo(cwd) {
1259
+ const config = getGitHubConfig(cwd);
1260
+ if (config?.owner && config?.repo) {
1261
+ return { owner: config.owner, repo: config.repo };
1262
+ }
1263
+ try {
1264
+ const remoteUrl = execSync3("git remote get-url origin", { encoding: "utf-8", cwd }).trim();
1265
+ const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
1266
+ if (sshMatch) {
1267
+ return { owner: sshMatch[1], repo: sshMatch[2] };
1268
+ }
1269
+ const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?$/);
1270
+ if (httpsMatch) {
1271
+ return { owner: httpsMatch[1], repo: httpsMatch[2] };
1272
+ }
1273
+ } catch {
1274
+ }
1275
+ return null;
1276
+ }
1277
+ function createGitHubIssue(info, query, taskId) {
1278
+ const body = `## Task
1279
+ ${query}
1280
+
1281
+ ## Spets Workflow
1282
+ - Task ID: \`${taskId}\`
1283
+ - Status: In Progress
1284
+
1285
+ ---
1286
+ _Managed by [Spets](https://github.com/eatnug/spets)_`;
1287
+ const result = execSync3(
1288
+ `gh issue create --repo ${info.owner}/${info.repo} --title "${query.slice(0, 50)}" --body "${body.replace(/"/g, '\\"')}" --label spets`,
1289
+ { encoding: "utf-8" }
1290
+ );
1291
+ const match = result.match(/\/issues\/(\d+)/);
1292
+ if (!match) {
1293
+ throw new Error("Failed to parse issue number from gh output");
1294
+ }
1295
+ return parseInt(match[1], 10);
1296
+ }
1297
+ function createGitHubPR(info, query, taskId) {
1298
+ const branchName = `spets/${taskId}`;
1299
+ execSync3(`git checkout -b ${branchName}`, { encoding: "utf-8" });
1300
+ execSync3(`git push -u origin ${branchName}`, { encoding: "utf-8" });
1301
+ const body = `## Task
1302
+ ${query}
1303
+
1304
+ ## Spets Workflow
1305
+ - Task ID: \`${taskId}\`
1306
+ - Status: In Progress
1307
+
1308
+ ---
1309
+ _Managed by [Spets](https://github.com/eatnug/spets)_`;
1310
+ const result = execSync3(
1311
+ `gh pr create --repo ${info.owner}/${info.repo} --title "${query.slice(0, 50)}" --body "${body.replace(/"/g, '\\"')}" --label spets`,
1312
+ { encoding: "utf-8" }
1313
+ );
1314
+ const match = result.match(/\/pull\/(\d+)/);
1315
+ if (!match) {
1316
+ throw new Error("Failed to parse PR number from gh output");
1317
+ }
1318
+ return parseInt(match[1], 10);
1319
+ }
1320
+ function findLinkedIssueOrPR(info) {
1321
+ try {
1322
+ const result = execSync3(
1323
+ `gh pr view --repo ${info.owner}/${info.repo} --json number`,
1324
+ { encoding: "utf-8" }
1325
+ );
1326
+ const pr = JSON.parse(result);
1327
+ if (pr.number) {
1328
+ return { type: "pr", number: pr.number };
1329
+ }
1330
+ } catch {
1331
+ }
1332
+ return null;
1333
+ }
1022
1334
  async function startCommand(query, options) {
1023
1335
  const cwd = process.cwd();
1024
1336
  if (!spetsExists(cwd)) {
@@ -1027,35 +1339,67 @@ async function startCommand(query, options) {
1027
1339
  }
1028
1340
  const config = loadConfig(cwd);
1029
1341
  const taskId = generateTaskId();
1030
- console.log(`Starting new workflow: ${taskId}`);
1031
- console.log(`Query: ${query}`);
1032
- console.log(`Platform: ${options.platform || "cli"}`);
1033
- console.log("");
1034
1342
  saveTaskMetadata(taskId, query, cwd);
1035
1343
  let platform;
1036
- if (options.platform === "github") {
1037
- const githubConfig = getGitHubConfig(cwd);
1038
- const owner = options.owner || githubConfig?.owner;
1039
- const repo = options.repo || githubConfig?.repo;
1040
- const pr = options.pr || (githubConfig?.defaultPr ? String(githubConfig.defaultPr) : void 0);
1041
- const issue = options.issue || (githubConfig?.defaultIssue ? String(githubConfig.defaultIssue) : void 0);
1042
- if (!owner || !repo) {
1043
- console.error("GitHub platform requires --owner and --repo (or set in .spets/config.yml)");
1344
+ if (options.github || options.issue !== void 0 || options.pr !== void 0) {
1345
+ const githubInfo = getGitHubInfo(cwd);
1346
+ if (!githubInfo) {
1347
+ console.error("Could not determine GitHub owner/repo. Set in .spets/config.yml or add git remote.");
1044
1348
  process.exit(1);
1045
1349
  }
1046
- if (!pr && !issue) {
1047
- console.error("GitHub platform requires --pr or --issue (or set defaultPr/defaultIssue in .spets/config.yml)");
1048
- process.exit(1);
1350
+ let issueNumber;
1351
+ let prNumber;
1352
+ if (options.issue !== void 0) {
1353
+ if (typeof options.issue === "string") {
1354
+ issueNumber = parseInt(options.issue, 10);
1355
+ console.log(`Using existing Issue #${issueNumber}`);
1356
+ } else {
1357
+ console.log("Creating new GitHub Issue...");
1358
+ issueNumber = createGitHubIssue(githubInfo, query, taskId);
1359
+ console.log(`Created Issue #${issueNumber}`);
1360
+ }
1361
+ }
1362
+ if (options.pr !== void 0) {
1363
+ if (typeof options.pr === "string") {
1364
+ prNumber = parseInt(options.pr, 10);
1365
+ console.log(`Using existing PR #${prNumber}`);
1366
+ } else {
1367
+ console.log("Creating new GitHub PR...");
1368
+ prNumber = createGitHubPR(githubInfo, query, taskId);
1369
+ console.log(`Created PR #${prNumber}`);
1370
+ }
1371
+ }
1372
+ if (issueNumber === void 0 && prNumber === void 0) {
1373
+ const linked = findLinkedIssueOrPR(githubInfo);
1374
+ if (linked) {
1375
+ console.log(`Found linked ${linked.type} #${linked.number}`);
1376
+ if (linked.type === "pr") {
1377
+ prNumber = linked.number;
1378
+ } else {
1379
+ issueNumber = linked.number;
1380
+ }
1381
+ } else {
1382
+ console.log("No linked Issue/PR found. Running in GitHub mode without Issue/PR.");
1383
+ }
1049
1384
  }
1050
1385
  platform = new GitHubPlatform({
1051
- owner,
1052
- repo,
1053
- prNumber: pr ? parseInt(pr, 10) : void 0,
1054
- issueNumber: issue ? parseInt(issue, 10) : void 0
1386
+ owner: githubInfo.owner,
1387
+ repo: githubInfo.repo,
1388
+ prNumber,
1389
+ issueNumber
1055
1390
  });
1391
+ console.log(`Starting workflow: ${taskId}`);
1392
+ console.log(`Query: ${query}`);
1393
+ console.log(`Platform: github (${githubInfo.owner}/${githubInfo.repo})`);
1394
+ if (issueNumber) console.log(`Issue: #${issueNumber}`);
1395
+ if (prNumber) console.log(`PR: #${prNumber}`);
1056
1396
  } else {
1057
1397
  platform = new CliPlatform();
1398
+ console.log(`Starting workflow: ${taskId}`);
1399
+ console.log(`Query: ${query}`);
1400
+ console.log(`Platform: cli`);
1058
1401
  }
1402
+ console.log("");
1059
1403
  const executor = new Executor({ platform, config, cwd });
1060
1404
  try {
1061
1405
  await executor.executeWorkflow(taskId, query);
@@ -1203,23 +1547,31 @@ function installClaudePlugin() {
1203
1547
  const claudeDir = join3(homedir(), ".claude");
1204
1548
  const commandsDir = join3(claudeDir, "commands");
1205
1549
  mkdirSync2(commandsDir, { recursive: true });
1206
- const skillPath = join3(commandsDir, "spets.md");
1550
+ const skillPath = join3(commandsDir, "sdd-do.md");
1207
1551
  writeFileSync2(skillPath, getClaudeSkillContent());
1208
1552
  console.log("Installed Claude Code plugin.");
1209
1553
  console.log(`Location: ${skillPath}`);
1210
1554
  console.log("");
1211
1555
  console.log("Usage in Claude Code:");
1212
- console.log(' /spets start "your task description"');
1213
- console.log(" /spets status");
1214
- console.log(" /spets resume");
1556
+ console.log(' /sdd-do "your task description"');
1215
1557
  console.log("");
1216
- console.log("Note: The plugin uses npx to run spets, so global installation is not required.");
1558
+ console.log("This skill runs deterministically within your Claude Code session.");
1559
+ console.log("No additional Claude processes are spawned.");
1217
1560
  }
1218
1561
  async function uninstallPlugin(name) {
1219
1562
  if (name === "claude") {
1220
- const skillPath = join3(homedir(), ".claude", "commands", "spets.md");
1221
- if (existsSync3(skillPath)) {
1222
- rmSync(skillPath);
1563
+ const oldSkillPath = join3(homedir(), ".claude", "commands", "spets.md");
1564
+ const newSkillPath = join3(homedir(), ".claude", "commands", "sdd-do.md");
1565
+ let uninstalled = false;
1566
+ if (existsSync3(oldSkillPath)) {
1567
+ rmSync(oldSkillPath);
1568
+ uninstalled = true;
1569
+ }
1570
+ if (existsSync3(newSkillPath)) {
1571
+ rmSync(newSkillPath);
1572
+ uninstalled = true;
1573
+ }
1574
+ if (uninstalled) {
1223
1575
  console.log("Uninstalled Claude Code plugin.");
1224
1576
  } else {
1225
1577
  console.log("Claude Code plugin not installed.");
@@ -1232,10 +1584,11 @@ async function uninstallPlugin(name) {
1232
1584
  async function listPlugins() {
1233
1585
  console.log("Available plugins:");
1234
1586
  console.log("");
1235
- console.log(" claude - Claude Code slash command integration");
1587
+ console.log(" claude - Claude Code /sdd-do skill");
1236
1588
  console.log("");
1237
- const claudeSkillPath = join3(homedir(), ".claude", "commands", "spets.md");
1238
- const claudeInstalled = existsSync3(claudeSkillPath);
1589
+ const oldSkillPath = join3(homedir(), ".claude", "commands", "spets.md");
1590
+ const newSkillPath = join3(homedir(), ".claude", "commands", "sdd-do.md");
1591
+ const claudeInstalled = existsSync3(oldSkillPath) || existsSync3(newSkillPath);
1239
1592
  console.log("Installed:");
1240
1593
  if (claudeInstalled) {
1241
1594
  console.log(" - claude");
@@ -1244,81 +1597,202 @@ async function listPlugins() {
1244
1597
  }
1245
1598
  }
1246
1599
  function getClaudeSkillContent() {
1247
- return `# Spets - Spec Driven Development
1600
+ return `# SDD-Do Skill
1601
+
1602
+ Spec-Driven Development workflow execution skill for Claude Code.
1603
+
1604
+ ---
1605
+
1606
+ ## When to Use This Skill
1607
+
1608
+ Automatically invoked when user uses:
1609
+ - \`/sdd-do\` - Run SDD workflow
1610
+
1611
+ ---
1612
+
1613
+ ## CRITICAL: Execution Model
1614
+
1615
+ This skill runs **deterministically** within the current Claude Code session.
1616
+ - NO subprocess spawning for AI/LLM calls
1617
+ - The orchestrator manages state via JSON protocol
1618
+ - Each step follows a strict state machine flow
1619
+
1620
+ ---
1621
+
1622
+ ## Orchestrator Commands
1623
+
1624
+ All workflow state is managed by the orchestrator CLI:
1625
+
1626
+ \`\`\`bash
1627
+ npx spets orchestrate init "<description>" # Start new workflow
1628
+ npx spets orchestrate done <taskId> # Mark step complete
1629
+ npx spets orchestrate clarified <taskId> '<json>' # Submit answers
1630
+ npx spets orchestrate approve <taskId> # Approve and continue
1631
+ npx spets orchestrate revise <taskId> "<feedback>" # Request revision
1632
+ npx spets orchestrate reject <taskId> # Stop workflow
1633
+ npx spets orchestrate stop <taskId> # Pause (can resume)
1634
+ npx spets orchestrate status <taskId> # Get current status
1635
+ \`\`\`
1636
+
1637
+ ---
1638
+
1639
+ ## Execution Flow
1248
1640
 
1249
- Run spets workflows from Claude Code.
1641
+ ### 1. Workflow Start (FIRST ACTION - NO EXCEPTIONS)
1250
1642
 
1251
- ## Usage
1643
+ When skill is loaded, **immediately** run:
1252
1644
 
1645
+ \`\`\`bash
1646
+ npx spets orchestrate init "{user_request}"
1253
1647
  \`\`\`
1254
- /spets start "task description" - Start a new workflow
1255
- /spets status - Show workflow status
1256
- /spets resume - Resume paused workflow
1648
+
1649
+ **JSON Output:**
1650
+ \`\`\`json
1651
+ {
1652
+ "type": "step",
1653
+ "step": "01-plan",
1654
+ "stepIndex": 1,
1655
+ "totalSteps": 2,
1656
+ "taskId": "mku3abc-xyz",
1657
+ "description": "User request here",
1658
+ "context": {
1659
+ "instruction": ".spets/steps/01-plan/instruction.md",
1660
+ "template": ".spets/steps/01-plan/template.md",
1661
+ "output": ".spets/outputs/mku3abc-xyz/01-plan.md",
1662
+ "revisionFeedback": null
1663
+ },
1664
+ "onComplete": "done mku3abc-xyz"
1665
+ }
1257
1666
  \`\`\`
1258
1667
 
1259
- ## Instructions
1668
+ ---
1260
1669
 
1261
- When the user invokes this command:
1670
+ ### 2. Document Generation (type: step)
1262
1671
 
1263
- 1. Parse the subcommand (start, status, resume)
1264
- 2. Execute the appropriate spets CLI command using Bash
1265
- 3. For 'start', read the generated documents and help iterate
1672
+ When you receive \`type: "step"\`:
1266
1673
 
1267
- **Important**: Use \`npx spets\` to run commands. This ensures the command works even if spets is not globally installed.
1674
+ 1. **Read the instruction file** using Read tool
1675
+ 2. **Read the template file** (if exists) using Read tool
1676
+ 3. **Read previous output** (if exists) using Read tool
1677
+ 4. **Generate the document** following the instruction and template
1678
+ 5. **Write the document** to context.output using Write tool
1679
+ 6. **Mark step as done**:
1680
+ \`\`\`bash
1681
+ npx spets orchestrate done {taskId}
1682
+ \`\`\`
1268
1683
 
1269
- ### Start Flow
1684
+ ---
1270
1685
 
1271
- 1. Run: \`npx spets start "<query>"\`
1272
- 2. Read the generated plan document from .spets/outputs/<taskId>/
1273
- 3. Present the plan to the user
1274
- 4. If user approves, continue. If they want changes, provide feedback.
1686
+ ### 3. Question Resolution (type: checkpoint, checkpoint: clarify)
1275
1687
 
1276
- ### Status Flow
1688
+ When you receive \`type: "checkpoint"\` with \`checkpoint: "clarify"\`:
1277
1689
 
1278
- 1. Run: \`npx spets status\`
1279
- 2. Present the current workflow state
1690
+ 1. **Use AskUserQuestion** tool to ask the questions
1691
+ 2. **Collect answers** into JSON format
1692
+ 3. **Submit answers**:
1693
+ \`\`\`bash
1694
+ npx spets orchestrate clarified {taskId} '[{"questionId":"q1","answer":"..."}]'
1695
+ \`\`\`
1280
1696
 
1281
- ### Resume Flow
1697
+ ---
1282
1698
 
1283
- 1. Run: \`npx spets resume\`
1284
- 2. Continue the workflow from where it paused
1699
+ ### 4. Approval Request (type: checkpoint, checkpoint: approve)
1285
1700
 
1286
- ## Example Session
1701
+ When you receive \`type: "checkpoint"\` with \`checkpoint: "approve"\`:
1702
+
1703
+ 1. **Display document summary** from specPath using Read tool
1704
+ 2. **Use AskUserQuestion** with options:
1705
+ - Approve & Continue
1706
+ - Revise (with feedback)
1707
+ - Reject
1708
+ - Stop (pause for later)
1709
+
1710
+ 3. **Execute the selected action**:
1711
+ - **Approve**: \`npx spets orchestrate approve {taskId}\`
1712
+ - **Revise**: \`npx spets orchestrate revise {taskId} "{feedback}"\`
1713
+ - **Reject**: \`npx spets orchestrate reject {taskId}\`
1714
+ - **Stop**: \`npx spets orchestrate stop {taskId}\`
1715
+
1716
+ ---
1717
+
1718
+ ### 5. Workflow Complete (type: complete)
1719
+
1720
+ Display completion message based on status:
1721
+ - \`completed\`: Show success and list outputs
1722
+ - \`stopped\`: Show resume instructions
1723
+ - \`rejected\`: Show rejection message
1287
1724
 
1288
- User: /spets start "Create a REST API for user management"
1725
+ ---
1289
1726
 
1290
- Claude:
1291
- 1. Runs \`npx spets start "Create a REST API for user management"\`
1292
- 2. Reads the generated plan
1293
- 3. Asks user: "Here's the plan. [shows plan] Would you like to approve or revise?"
1294
- 4. On approve: \`npx spets resume --approve\`
1295
- 5. On revise: \`npx spets resume --revise "user feedback"\`
1727
+ ## Response Type Actions Summary
1728
+
1729
+ | Type | Checkpoint | Action |
1730
+ |------|------------|--------|
1731
+ | step | - | Generate document, write to output, call done |
1732
+ | checkpoint | clarify | Ask questions, call clarified with answers |
1733
+ | checkpoint | approve | Show doc, ask for decision, call appropriate command |
1734
+ | complete | - | Show final message, end skill |
1735
+ | error | - | Show error, end skill |
1736
+
1737
+ ---
1738
+
1739
+ ## Critical Implementation Rules
1740
+
1741
+ 1. **ORCHESTRATOR FIRST** - Always call orchestrator before any action
1742
+ 2. **ONE STEP AT A TIME** - Complete one step, get approval, then next
1743
+ 3. **DETERMINISTIC FLOW** - Follow the exact state machine transitions
1744
+ 4. **NO PROCESS SPAWN FOR AI** - Generate documents in current session
1745
+ 5. **WAIT FOR APPROVAL** - Never proceed without user approval
1746
+
1747
+ ---
1748
+
1749
+ ## Error Handling
1750
+
1751
+ - If \`.spets/\` not found, suggest running \`npx spets init\`
1752
+ - If orchestrator returns \`type: "error"\`, display the error and stop
1753
+ - If file read fails, show error and allow retry
1296
1754
 
1297
1755
  $ARGUMENTS
1298
- command: The spets command to run (start, status, resume)
1299
- args: Additional arguments for the command
1756
+ request: The user's task description for the workflow
1300
1757
  `;
1301
1758
  }
1302
1759
 
1303
1760
  // src/commands/github.ts
1761
+ import { execSync as execSync4 } from "child_process";
1762
+ function getGitHubInfo2(cwd) {
1763
+ const config = getGitHubConfig(cwd);
1764
+ if (config?.owner && config?.repo) {
1765
+ return { owner: config.owner, repo: config.repo };
1766
+ }
1767
+ try {
1768
+ const remoteUrl = execSync4("git remote get-url origin", { encoding: "utf-8", cwd }).trim();
1769
+ const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
1770
+ if (sshMatch) {
1771
+ return { owner: sshMatch[1], repo: sshMatch[2] };
1772
+ }
1773
+ const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?$/);
1774
+ if (httpsMatch) {
1775
+ return { owner: httpsMatch[1], repo: httpsMatch[2] };
1776
+ }
1777
+ } catch {
1778
+ }
1779
+ return null;
1780
+ }
1304
1781
  async function githubCommand(options) {
1305
1782
  const cwd = process.cwd();
1306
1783
  if (!spetsExists(cwd)) {
1307
1784
  console.error("Spets not initialized.");
1308
1785
  process.exit(1);
1309
1786
  }
1310
- const configGitHub = getGitHubConfig(cwd);
1311
- const owner = options.owner || configGitHub?.owner;
1312
- const repo = options.repo || configGitHub?.repo;
1313
- const pr = options.pr || (configGitHub?.defaultPr ? String(configGitHub.defaultPr) : void 0);
1314
- const issue = options.issue || (configGitHub?.defaultIssue ? String(configGitHub.defaultIssue) : void 0);
1315
- const { comment, task } = options;
1316
- if (!owner || !repo) {
1317
- console.error("GitHub requires --owner and --repo (or set in .spets/config.yml)");
1787
+ const githubInfo = getGitHubInfo2(cwd);
1788
+ if (!githubInfo) {
1789
+ console.error("Could not determine GitHub owner/repo. Set in .spets/config.yml or add git remote.");
1318
1790
  process.exit(1);
1319
1791
  }
1792
+ const { owner, repo } = githubInfo;
1793
+ const { pr, issue, comment, task } = options;
1320
1794
  if (!pr && !issue) {
1321
- console.error("Either --pr or --issue is required (or set defaultPr/defaultIssue in .spets/config.yml)");
1795
+ console.error("Either --pr or --issue is required");
1322
1796
  process.exit(1);
1323
1797
  }
1324
1798
  const parsed = parseGitHubCommand(comment);
@@ -1336,7 +1810,7 @@ async function githubCommand(options) {
1336
1810
  }
1337
1811
  if (!taskId) {
1338
1812
  const config2 = loadConfig(cwd);
1339
- const { listTasks: listTasks2 } = await import("./state-H2GQS43T.js");
1813
+ const { listTasks: listTasks2 } = await import("./state-JLYPJWUT.js");
1340
1814
  const tasks = listTasks2(cwd);
1341
1815
  for (const tid of tasks) {
1342
1816
  const state2 = getWorkflowState(tid, config2, cwd);
@@ -1461,13 +1935,476 @@ async function postComment(config, body) {
1461
1935
  });
1462
1936
  }
1463
1937
 
1938
+ // src/orchestrator/index.ts
1939
+ import { readFileSync, writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
1940
+ import { join as join4, dirname as dirname2 } from "path";
1941
+ import matter from "gray-matter";
1942
+ var Orchestrator = class {
1943
+ cwd;
1944
+ constructor(cwd = process.cwd()) {
1945
+ this.cwd = cwd;
1946
+ }
1947
+ // ===========================================================================
1948
+ // Config & Path Helpers
1949
+ // ===========================================================================
1950
+ getSteps() {
1951
+ const config = loadConfig(this.cwd);
1952
+ return config.steps;
1953
+ }
1954
+ getOutputPath() {
1955
+ return getOutputsDir(this.cwd);
1956
+ }
1957
+ getStatePath(taskId) {
1958
+ return join4(this.getOutputPath(), taskId, ".state.json");
1959
+ }
1960
+ getSpecPath(taskId, step) {
1961
+ return join4(this.getOutputPath(), taskId, `${step}.md`);
1962
+ }
1963
+ getStepInstructionPath(step) {
1964
+ return join4(getStepsDir(this.cwd), step, "instruction.md");
1965
+ }
1966
+ getStepTemplatePath(step) {
1967
+ return join4(getStepsDir(this.cwd), step, "template.md");
1968
+ }
1969
+ // ===========================================================================
1970
+ // State Management
1971
+ // ===========================================================================
1972
+ loadState(taskId) {
1973
+ const statePath = this.getStatePath(taskId);
1974
+ if (!existsSync4(statePath)) {
1975
+ return null;
1976
+ }
1977
+ const data = JSON.parse(readFileSync(statePath, "utf-8"));
1978
+ return data;
1979
+ }
1980
+ saveState(state) {
1981
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1982
+ const statePath = this.getStatePath(state.taskId);
1983
+ const dir = dirname2(statePath);
1984
+ if (!existsSync4(dir)) {
1985
+ mkdirSync3(dir, { recursive: true });
1986
+ }
1987
+ writeFileSync3(statePath, JSON.stringify(state, null, 2));
1988
+ }
1989
+ // ===========================================================================
1990
+ // Spec Helpers
1991
+ // ===========================================================================
1992
+ checkUnresolvedQuestions(specPath) {
1993
+ if (!existsSync4(specPath)) {
1994
+ return [];
1995
+ }
1996
+ const content = readFileSync(specPath, "utf-8");
1997
+ const { data } = matter(content);
1998
+ const questions = [];
1999
+ if (data.open_questions && Array.isArray(data.open_questions)) {
2000
+ for (let i = 0; i < data.open_questions.length; i++) {
2001
+ const q = data.open_questions[i];
2002
+ if (!q.resolved) {
2003
+ questions.push({
2004
+ id: `q${i + 1}`,
2005
+ question: q.question || q.id || `Question ${i + 1}`,
2006
+ context: q.context,
2007
+ options: q.options,
2008
+ resolved: false
2009
+ });
2010
+ }
2011
+ }
2012
+ }
2013
+ return questions;
2014
+ }
2015
+ generateTaskId(description) {
2016
+ const timestamp = Date.now().toString(36);
2017
+ const random = Math.random().toString(36).substring(2, 6);
2018
+ return `${timestamp}-${random}`;
2019
+ }
2020
+ // ===========================================================================
2021
+ // Protocol Response Builders
2022
+ // ===========================================================================
2023
+ responseStep(state) {
2024
+ const steps = this.getSteps();
2025
+ const outputPath = this.getOutputPath();
2026
+ let previousOutput;
2027
+ if (state.stepIndex > 1) {
2028
+ const prevStep = steps[state.stepIndex - 2];
2029
+ const prevPath = join4(outputPath, state.taskId, `${prevStep}.md`);
2030
+ if (existsSync4(prevPath)) {
2031
+ previousOutput = prevPath;
2032
+ }
2033
+ }
2034
+ const templatePath = this.getStepTemplatePath(state.currentStep);
2035
+ const hasTemplate = existsSync4(templatePath);
2036
+ return {
2037
+ type: "step",
2038
+ step: state.currentStep,
2039
+ stepIndex: state.stepIndex,
2040
+ totalSteps: state.totalSteps,
2041
+ taskId: state.taskId,
2042
+ description: state.description,
2043
+ context: {
2044
+ instruction: this.getStepInstructionPath(state.currentStep),
2045
+ template: hasTemplate ? templatePath : void 0,
2046
+ previousOutput,
2047
+ output: join4(outputPath, state.taskId, `${state.currentStep}.md`),
2048
+ revisionFeedback: state.revisionFeedback
2049
+ },
2050
+ onComplete: `done ${state.taskId}`
2051
+ };
2052
+ }
2053
+ responseCheckpointClarify(state, questions) {
2054
+ return {
2055
+ type: "checkpoint",
2056
+ checkpoint: "clarify",
2057
+ taskId: state.taskId,
2058
+ step: state.currentStep,
2059
+ questions: questions.map((q) => ({
2060
+ id: q.id,
2061
+ question: q.question,
2062
+ context: q.context,
2063
+ options: q.options
2064
+ })),
2065
+ onComplete: `clarified ${state.taskId} '<answers_json>'`
2066
+ };
2067
+ }
2068
+ responseCheckpointApprove(state) {
2069
+ const outputPath = this.getOutputPath();
2070
+ return {
2071
+ type: "checkpoint",
2072
+ checkpoint: "approve",
2073
+ taskId: state.taskId,
2074
+ step: state.currentStep,
2075
+ stepIndex: state.stepIndex,
2076
+ totalSteps: state.totalSteps,
2077
+ specPath: join4(outputPath, state.taskId, `${state.currentStep}.md`),
2078
+ options: ["approve", "revise", "reject", "stop"],
2079
+ onComplete: {
2080
+ approve: `approve ${state.taskId}`,
2081
+ revise: `revise ${state.taskId} '<feedback>'`,
2082
+ reject: `reject ${state.taskId}`,
2083
+ stop: `stop ${state.taskId}`
2084
+ }
2085
+ };
2086
+ }
2087
+ responseComplete(state, status) {
2088
+ const steps = this.getSteps();
2089
+ const outputPath = this.getOutputPath();
2090
+ const outputs = [];
2091
+ for (let i = 0; i < state.stepIndex; i++) {
2092
+ const specPath = join4(outputPath, state.taskId, `${steps[i]}.md`);
2093
+ if (existsSync4(specPath)) {
2094
+ outputs.push(specPath);
2095
+ }
2096
+ }
2097
+ const messages = {
2098
+ completed: "Workflow completed successfully",
2099
+ stopped: `Workflow stopped. Resume with: spets resume --task ${state.taskId}`,
2100
+ rejected: "Workflow rejected"
2101
+ };
2102
+ return {
2103
+ type: "complete",
2104
+ status,
2105
+ taskId: state.taskId,
2106
+ outputs,
2107
+ message: messages[status]
2108
+ };
2109
+ }
2110
+ responseError(error, taskId, step) {
2111
+ const response = { type: "error", error };
2112
+ if (taskId) response.taskId = taskId;
2113
+ if (step) response.step = step;
2114
+ return response;
2115
+ }
2116
+ // ===========================================================================
2117
+ // Command Handlers
2118
+ // ===========================================================================
2119
+ /**
2120
+ * Initialize a new workflow
2121
+ */
2122
+ cmdInit(description) {
2123
+ try {
2124
+ const steps = this.getSteps();
2125
+ const taskId = this.generateTaskId(description);
2126
+ const state = {
2127
+ taskId,
2128
+ description,
2129
+ currentStep: steps[0],
2130
+ stepIndex: 1,
2131
+ totalSteps: steps.length,
2132
+ status: "awaiting_spec",
2133
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2134
+ };
2135
+ this.saveState(state);
2136
+ return this.responseStep(state);
2137
+ } catch (e) {
2138
+ return this.responseError(e.message);
2139
+ }
2140
+ }
2141
+ /**
2142
+ * Mark current step as done, check for questions or go to approve
2143
+ */
2144
+ cmdDone(taskId) {
2145
+ const state = this.loadState(taskId);
2146
+ if (!state) {
2147
+ return this.responseError(`No workflow found: ${taskId}`, taskId);
2148
+ }
2149
+ const specPath = this.getSpecPath(taskId, state.currentStep);
2150
+ if (!existsSync4(specPath)) {
2151
+ return this.responseError(`Spec not found: ${specPath}`, taskId, state.currentStep);
2152
+ }
2153
+ const questions = this.checkUnresolvedQuestions(specPath);
2154
+ if (questions.length > 0) {
2155
+ state.status = "awaiting_clarify";
2156
+ this.saveState(state);
2157
+ return this.responseCheckpointClarify(state, questions);
2158
+ }
2159
+ state.status = "awaiting_approve";
2160
+ this.saveState(state);
2161
+ return this.responseCheckpointApprove(state);
2162
+ }
2163
+ /**
2164
+ * Submit clarification answers
2165
+ */
2166
+ cmdClarified(taskId, answers) {
2167
+ const state = this.loadState(taskId);
2168
+ if (!state) {
2169
+ return this.responseError(`No workflow found: ${taskId}`, taskId);
2170
+ }
2171
+ const specPath = this.getSpecPath(taskId, state.currentStep);
2172
+ if (existsSync4(specPath)) {
2173
+ const content = readFileSync(specPath, "utf-8");
2174
+ const { content: body, data } = matter(content);
2175
+ if (data.open_questions && Array.isArray(data.open_questions)) {
2176
+ data.open_questions = data.open_questions.map((q, i) => ({
2177
+ ...q,
2178
+ resolved: true,
2179
+ answer: answers.find((a) => a.questionId === `q${i + 1}`)?.answer
2180
+ }));
2181
+ }
2182
+ writeFileSync3(specPath, matter.stringify(body, data));
2183
+ }
2184
+ state.status = "awaiting_spec";
2185
+ state.revisionFeedback = `Answers provided: ${JSON.stringify(answers)}`;
2186
+ this.saveState(state);
2187
+ return this.responseStep(state);
2188
+ }
2189
+ /**
2190
+ * Approve current step and move to next
2191
+ */
2192
+ cmdApprove(taskId) {
2193
+ const state = this.loadState(taskId);
2194
+ if (!state) {
2195
+ return this.responseError(`No workflow found: ${taskId}`, taskId);
2196
+ }
2197
+ const steps = this.getSteps();
2198
+ const specPath = this.getSpecPath(taskId, state.currentStep);
2199
+ if (existsSync4(specPath)) {
2200
+ const content = readFileSync(specPath, "utf-8");
2201
+ const { content: body, data } = matter(content);
2202
+ data.status = "approved";
2203
+ data.updated_at = (/* @__PURE__ */ new Date()).toISOString();
2204
+ writeFileSync3(specPath, matter.stringify(body, data));
2205
+ }
2206
+ if (state.stepIndex < state.totalSteps) {
2207
+ state.currentStep = steps[state.stepIndex];
2208
+ state.stepIndex += 1;
2209
+ state.status = "awaiting_spec";
2210
+ state.revisionFeedback = void 0;
2211
+ this.saveState(state);
2212
+ return this.responseStep(state);
2213
+ } else {
2214
+ state.status = "completed";
2215
+ this.saveState(state);
2216
+ return this.responseComplete(state, "completed");
2217
+ }
2218
+ }
2219
+ /**
2220
+ * Request revision with feedback
2221
+ */
2222
+ cmdRevise(taskId, feedback) {
2223
+ const state = this.loadState(taskId);
2224
+ if (!state) {
2225
+ return this.responseError(`No workflow found: ${taskId}`, taskId);
2226
+ }
2227
+ state.status = "awaiting_spec";
2228
+ state.revisionFeedback = feedback;
2229
+ this.saveState(state);
2230
+ return this.responseStep(state);
2231
+ }
2232
+ /**
2233
+ * Reject and stop workflow
2234
+ */
2235
+ cmdReject(taskId) {
2236
+ const state = this.loadState(taskId);
2237
+ if (!state) {
2238
+ return this.responseError(`No workflow found: ${taskId}`, taskId);
2239
+ }
2240
+ const specPath = this.getSpecPath(taskId, state.currentStep);
2241
+ if (existsSync4(specPath)) {
2242
+ const content = readFileSync(specPath, "utf-8");
2243
+ const { content: body, data } = matter(content);
2244
+ data.status = "rejected";
2245
+ data.updated_at = (/* @__PURE__ */ new Date()).toISOString();
2246
+ writeFileSync3(specPath, matter.stringify(body, data));
2247
+ }
2248
+ state.status = "rejected";
2249
+ this.saveState(state);
2250
+ return this.responseComplete(state, "rejected");
2251
+ }
2252
+ /**
2253
+ * Stop workflow (can resume later)
2254
+ */
2255
+ cmdStop(taskId) {
2256
+ const state = this.loadState(taskId);
2257
+ if (!state) {
2258
+ return this.responseError(`No workflow found: ${taskId}`, taskId);
2259
+ }
2260
+ state.status = "stopped";
2261
+ this.saveState(state);
2262
+ return this.responseComplete(state, "stopped");
2263
+ }
2264
+ /**
2265
+ * Get current workflow status
2266
+ */
2267
+ cmdStatus(taskId) {
2268
+ const state = this.loadState(taskId);
2269
+ if (!state) {
2270
+ return this.responseError(`No workflow found: ${taskId}`, taskId);
2271
+ }
2272
+ switch (state.status) {
2273
+ case "awaiting_spec":
2274
+ return this.responseStep(state);
2275
+ case "awaiting_clarify": {
2276
+ const questions = this.checkUnresolvedQuestions(this.getSpecPath(taskId, state.currentStep));
2277
+ return this.responseCheckpointClarify(state, questions);
2278
+ }
2279
+ case "awaiting_approve":
2280
+ return this.responseCheckpointApprove(state);
2281
+ case "completed":
2282
+ case "stopped":
2283
+ case "rejected":
2284
+ return this.responseComplete(state, state.status);
2285
+ default:
2286
+ return this.responseError(`Unknown status: ${state.status}`, taskId);
2287
+ }
2288
+ }
2289
+ };
2290
+
2291
+ // src/commands/orchestrate.ts
2292
+ function outputJSON(data) {
2293
+ console.log(JSON.stringify(data, null, 2));
2294
+ }
2295
+ function outputError(error) {
2296
+ console.log(JSON.stringify({ type: "error", error }, null, 2));
2297
+ process.exit(1);
2298
+ }
2299
+ async function orchestrateCommand(action, args) {
2300
+ try {
2301
+ const orchestrator = new Orchestrator();
2302
+ switch (action) {
2303
+ case "init": {
2304
+ const description = args[0];
2305
+ if (!description) {
2306
+ outputError("Description is required for init");
2307
+ return;
2308
+ }
2309
+ const result = orchestrator.cmdInit(description);
2310
+ outputJSON(result);
2311
+ break;
2312
+ }
2313
+ case "done": {
2314
+ const taskId = args[0];
2315
+ if (!taskId) {
2316
+ outputError("Task ID is required for done");
2317
+ return;
2318
+ }
2319
+ const result = orchestrator.cmdDone(taskId);
2320
+ outputJSON(result);
2321
+ break;
2322
+ }
2323
+ case "clarified": {
2324
+ const taskId = args[0];
2325
+ const answersJson = args[1];
2326
+ if (!taskId || !answersJson) {
2327
+ outputError("Task ID and answers JSON are required for clarified");
2328
+ return;
2329
+ }
2330
+ let answers;
2331
+ try {
2332
+ answers = JSON.parse(answersJson);
2333
+ } catch {
2334
+ outputError("Invalid JSON for answers");
2335
+ return;
2336
+ }
2337
+ const result = orchestrator.cmdClarified(taskId, answers);
2338
+ outputJSON(result);
2339
+ break;
2340
+ }
2341
+ case "approve": {
2342
+ const taskId = args[0];
2343
+ if (!taskId) {
2344
+ outputError("Task ID is required for approve");
2345
+ return;
2346
+ }
2347
+ const result = orchestrator.cmdApprove(taskId);
2348
+ outputJSON(result);
2349
+ break;
2350
+ }
2351
+ case "revise": {
2352
+ const taskId = args[0];
2353
+ const feedback = args[1];
2354
+ if (!taskId || !feedback) {
2355
+ outputError("Task ID and feedback are required for revise");
2356
+ return;
2357
+ }
2358
+ const result = orchestrator.cmdRevise(taskId, feedback);
2359
+ outputJSON(result);
2360
+ break;
2361
+ }
2362
+ case "reject": {
2363
+ const taskId = args[0];
2364
+ if (!taskId) {
2365
+ outputError("Task ID is required for reject");
2366
+ return;
2367
+ }
2368
+ const result = orchestrator.cmdReject(taskId);
2369
+ outputJSON(result);
2370
+ break;
2371
+ }
2372
+ case "stop": {
2373
+ const taskId = args[0];
2374
+ if (!taskId) {
2375
+ outputError("Task ID is required for stop");
2376
+ return;
2377
+ }
2378
+ const result = orchestrator.cmdStop(taskId);
2379
+ outputJSON(result);
2380
+ break;
2381
+ }
2382
+ case "status": {
2383
+ const taskId = args[0];
2384
+ if (!taskId) {
2385
+ outputError("Task ID is required for status");
2386
+ return;
2387
+ }
2388
+ const result = orchestrator.cmdStatus(taskId);
2389
+ outputJSON(result);
2390
+ break;
2391
+ }
2392
+ default:
2393
+ outputError(`Unknown action: ${action}. Valid actions: init, done, clarified, approve, revise, reject, stop, status`);
2394
+ }
2395
+ } catch (e) {
2396
+ outputError(e.message);
2397
+ }
2398
+ }
2399
+
1464
2400
  // src/index.ts
1465
2401
  var program = new Command();
1466
- program.name("spets").description("Spec Driven Development Execution Framework").version("0.1.0");
2402
+ program.name("spets").description("Spec Driven Development Execution Framework").version("0.1.3");
1467
2403
  program.command("init").description("Initialize spets in current directory").option("-f, --force", "Overwrite existing config").option("--github", "Add GitHub Actions workflow for PR/Issue integration").action(initCommand);
1468
2404
  program.command("status").description("Show current workflow status").option("-t, --task <taskId>", "Show status for specific task").action(statusCommand);
1469
- program.command("start").description("Start a new workflow").argument("<query>", "User query describing the task").option("-p, --platform <platform>", "Platform to use (cli, github)", "cli").option("--owner <owner>", "GitHub owner (for github platform)").option("--repo <repo>", "GitHub repo (for github platform)").option("--pr <number>", "GitHub PR number (for github platform)").option("--issue <number>", "GitHub issue number (for github platform)").action(startCommand);
2405
+ program.command("start").description("Start a new workflow").argument("<query>", "User query describing the task").option("--github", "Use GitHub platform (reads owner/repo from config or git remote)").option("--issue [number]", "Use or create GitHub Issue (optional: specify existing issue number)").option("--pr [number]", "Use or create GitHub PR (optional: specify existing PR number)").action(startCommand);
1470
2406
  program.command("resume").description("Resume paused workflow").option("-t, --task <taskId>", "Resume specific task").option("--approve", "Approve current document and proceed").option("--revise <feedback>", "Request revision with feedback").action(resumeCommand);
1471
- program.command("github").description("Handle GitHub Action callback (internal)").option("--owner <owner>", "GitHub owner (or set in .spets/config.yml)").option("--repo <repo>", "GitHub repo (or set in .spets/config.yml)").option("--pr <number>", "PR number (or set defaultPr in .spets/config.yml)").option("--issue <number>", "Issue number (or set defaultIssue in .spets/config.yml)").option("-t, --task <taskId>", "Task ID").requiredOption("--comment <comment>", "Comment body").action(githubCommand);
2407
+ program.command("github").description("Handle GitHub Action callback (internal)").option("--pr <number>", "PR number").option("--issue <number>", "Issue number").option("-t, --task <taskId>", "Task ID").requiredOption("--comment <comment>", "Comment body").action(githubCommand);
1472
2408
  program.command("plugin").description("Manage plugins").argument("<action>", "Action: install, uninstall, list").argument("[name]", "Plugin name").action(pluginCommand);
2409
+ program.command("orchestrate").description("Workflow orchestration (JSON API for Claude Code)").argument("<action>", "Action: init, done, clarified, approve, revise, reject, stop, status").argument("[args...]", "Action arguments").action(orchestrateCommand);
1473
2410
  program.parse();