spets 0.1.3 → 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,7 +55,8 @@ 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);
41
61
  createClaudeCommand(cwd);
42
62
  console.log("Initialized spets in .spets/");
@@ -48,7 +68,8 @@ async function initCommand(options) {
48
68
  console.log(" .claude/commands/spets.md - Claude Code command");
49
69
  if (options.github) {
50
70
  createGitHubWorkflow(cwd);
51
- 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");
52
73
  }
53
74
  console.log("");
54
75
  console.log("Next steps:");
@@ -56,19 +77,30 @@ async function initCommand(options) {
56
77
  console.log(" 2. Customize step instructions in .spets/steps/");
57
78
  if (options.github) {
58
79
  console.log(" 3. Add ANTHROPIC_API_KEY to your repo secrets");
59
- 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');
60
81
  } else {
61
82
  console.log(' 3. Run: spets start "your task description"');
62
83
  }
63
84
  }
64
- 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
+ `;
65
97
  return `# Spets Configuration
66
98
  # Define your workflow steps here
67
99
 
68
100
  steps:
69
101
  - 01-plan
70
102
  - 02-implement
71
-
103
+ ${githubSection}
72
104
  # Optional hooks (shell scripts)
73
105
  # hooks:
74
106
  # preStep: "./hooks/pre-step.sh"
@@ -247,82 +279,174 @@ function createClaudeCommand(cwd) {
247
279
  function getClaudeCommand() {
248
280
  return `# Spets - Spec Driven Development
249
281
 
250
- Run spets workflows from Claude Code.
282
+ Spec-Driven Development workflow execution skill for Claude Code.
283
+
284
+ ---
251
285
 
252
- ## Usage
286
+ ## CRITICAL: Execution Model
253
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
254
308
  \`\`\`
255
- /spets start "task description" - Start a new workflow
256
- /spets status - Show workflow status
257
- /spets resume - Resume paused workflow
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}"
258
320
  \`\`\`
259
321
 
260
- ## Instructions
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)
261
340
 
262
- When the user invokes this command:
341
+ 1. **Display document summary** from specPath
342
+ 2. **Use AskUserQuestion** with options: Approve, Revise, Reject, Stop
343
+ 3. **Execute the selected action**
263
344
 
264
- 1. Parse the subcommand (start, status, resume)
265
- 2. Execute the appropriate spets CLI command using Bash
266
- 3. For 'start', read the generated documents and help iterate
345
+ ### 5. Workflow Complete (type: complete)
267
346
 
268
- ### Start Flow
347
+ Display completion message based on status (completed, stopped, rejected)
269
348
 
270
- 1. Run: \`npx spets start "<query>"\`
271
- 2. Read the generated plan document from .spets/outputs/<taskId>/
272
- 3. Present the plan to the user
273
- 4. If user approves, continue. If they want changes, provide feedback.
349
+ ---
274
350
 
275
- ### Status Flow
351
+ ## Response Type Actions Summary
276
352
 
277
- 1. Run: \`npx spets status\`
278
- 2. Present the current workflow state
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 |
279
360
 
280
- ### Resume Flow
361
+ ---
281
362
 
282
- 1. Run: \`npx spets resume\`
283
- 2. Continue the workflow from where it paused
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
+ ---
284
372
 
285
373
  ## Example Session
286
374
 
287
- User: /spets start "Create a REST API for user management"
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
288
391
 
289
- Claude:
290
- 1. Runs \`npx spets start "Create a REST API for user management"\`
291
- 2. Reads the generated plan
292
- 3. Asks user: "Here's the plan. [shows plan] Would you like to approve or revise?"
293
- 4. On approve: \`npx spets resume --approve\`
294
- 5. On revise: \`npx spets resume --revise "user feedback"\`
392
+ [Skill calls: npx spets orchestrate approve mku3abc-xyz]
393
+ [Receives: type=step, step=02-implement]
394
+
395
+ [... continues with implementation step ...]
396
+ \`\`\`
295
397
 
296
398
  $ARGUMENTS
297
- command: The spets command to run (start, status, resume)
298
- args: Additional arguments for the command
399
+ description: The task description for the workflow
299
400
  `;
300
401
  }
301
402
  function createGitHubWorkflow(cwd) {
302
403
  const workflowDir = join(cwd, ".github", "workflows");
404
+ const templateDir = join(cwd, ".github", "ISSUE_TEMPLATE");
303
405
  mkdirSync(workflowDir, { recursive: true });
406
+ mkdirSync(templateDir, { recursive: true });
304
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
+ `;
305
432
  }
306
433
  function getGitHubWorkflow() {
307
434
  const gh = (expr) => `\${{ ${expr} }}`;
308
435
  return `# Spets GitHub Action
309
- # Runs Claude Code to handle /approve, /revise, /reject commands
436
+ # Handles workflow start from Issue creation and commands from comments
310
437
 
311
438
  name: Spets Workflow
312
439
 
313
440
  on:
441
+ issues:
442
+ types: [opened]
314
443
  issue_comment:
315
444
  types: [created]
316
445
 
317
446
  jobs:
318
- handle-spets-command:
319
- # Only run if comment contains a spets command
320
- if: |
321
- contains(github.event.comment.body, '/approve') ||
322
- contains(github.event.comment.body, '/revise') ||
323
- contains(github.event.comment.body, '/reject') ||
324
- contains(github.event.comment.body, '/answer')
325
-
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')
326
450
  runs-on: ubuntu-latest
327
451
 
328
452
  steps:
@@ -331,6 +455,30 @@ jobs:
331
455
  with:
332
456
  fetch-depth: 0
333
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
+
334
482
  - name: Setup Node.js
335
483
  uses: actions/setup-node@v4
336
484
  with:
@@ -342,43 +490,69 @@ jobs:
342
490
  - name: Install dependencies
343
491
  run: npm ci
344
492
 
345
- - name: Determine context
346
- id: context
493
+ - name: Start Spets workflow
347
494
  run: |
348
- if [ -n "${gh("github.event.issue.pull_request")}" ]; then
349
- echo "type=pr" >> $GITHUB_OUTPUT
350
- else
351
- echo "type=issue" >> $GITHUB_OUTPUT
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
524
+ run: |
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")}"
352
529
  fi
353
- 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
539
+
540
+ - name: Setup Node.js
541
+ uses: actions/setup-node@v4
542
+ with:
543
+ node-version: '20'
354
544
 
355
- - name: Run Claude Code
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
356
552
  run: |
357
- claude -p "
358
- You are running a Spets workflow via GitHub Actions.
359
-
360
- Context:
361
- - Repository: ${gh("github.repository")}
362
- - Issue/PR: #${gh("github.event.issue.number")}
363
- - Comment author: ${gh("github.event.comment.user.login")}
364
- - Command received: $COMMENT_BODY
365
-
366
- Instructions:
367
- 1. Parse the command from the comment (/approve, /revise, /reject, or /answer)
368
- 2. Find the active spets task in .spets/outputs/
369
- 3. Execute the appropriate action:
370
- - /approve: Mark current step as approved, generate next step
371
- - /revise <feedback>: Regenerate current step with feedback
372
- - /reject: Mark workflow as rejected
373
- - /answer: Process answers and continue
374
- 4. If generating content (plan or implementation), actually write the code/changes
375
- 5. Post a summary comment to the PR/Issue using gh cli
376
- 6. Commit any changes with a descriptive message
377
-
378
- Use 'gh issue comment' or 'gh pr comment' to post updates.
379
- "
553
+ npx spets github --issue ${gh("github.event.issue.number")} --comment "$COMMENT"
380
554
  env:
381
- COMMENT_BODY: ${gh("github.event.comment.body")}
555
+ COMMENT: ${gh("github.event.comment.body")}
382
556
  ANTHROPIC_API_KEY: ${gh("secrets.ANTHROPIC_API_KEY")}
383
557
  GH_TOKEN: ${gh("secrets.GITHUB_TOKEN")}
384
558
 
@@ -387,7 +561,7 @@ jobs:
387
561
  git config user.name "github-actions[bot]"
388
562
  git config user.email "github-actions[bot]@users.noreply.github.com"
389
563
  git add -A
390
- 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")}"
391
565
  git push
392
566
  env:
393
567
  GH_TOKEN: ${gh("secrets.GITHUB_TOKEN")}
@@ -653,7 +827,7 @@ var Executor = class {
653
827
  };
654
828
 
655
829
  // src/platform/cli.ts
656
- import { spawn as spawn2, execSync } from "child_process";
830
+ import { spawn as spawn2, execSync as execSync2 } from "child_process";
657
831
  import { input, select, confirm } from "@inquirer/prompts";
658
832
 
659
833
  // src/platform/interface.ts
@@ -805,7 +979,7 @@ var CliPlatform = class extends BasePlatform {
805
979
  isClaudeInstalled() {
806
980
  try {
807
981
  const command = process.platform === "win32" ? "where" : "which";
808
- execSync(`${command} claude`, { stdio: "ignore" });
982
+ execSync2(`${command} claude`, { stdio: "ignore" });
809
983
  return true;
810
984
  } catch {
811
985
  return false;
@@ -1080,6 +1254,83 @@ var GitHubPlatform = class extends BasePlatform {
1080
1254
  };
1081
1255
 
1082
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
+ }
1083
1334
  async function startCommand(query, options) {
1084
1335
  const cwd = process.cwd();
1085
1336
  if (!spetsExists(cwd)) {
@@ -1088,35 +1339,67 @@ async function startCommand(query, options) {
1088
1339
  }
1089
1340
  const config = loadConfig(cwd);
1090
1341
  const taskId = generateTaskId();
1091
- console.log(`Starting new workflow: ${taskId}`);
1092
- console.log(`Query: ${query}`);
1093
- console.log(`Platform: ${options.platform || "cli"}`);
1094
- console.log("");
1095
1342
  saveTaskMetadata(taskId, query, cwd);
1096
1343
  let platform;
1097
- if (options.platform === "github") {
1098
- const githubConfig = getGitHubConfig(cwd);
1099
- const owner = options.owner || githubConfig?.owner;
1100
- const repo = options.repo || githubConfig?.repo;
1101
- const pr = options.pr || (githubConfig?.defaultPr ? String(githubConfig.defaultPr) : void 0);
1102
- const issue = options.issue || (githubConfig?.defaultIssue ? String(githubConfig.defaultIssue) : void 0);
1103
- if (!owner || !repo) {
1104
- 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.");
1105
1348
  process.exit(1);
1106
1349
  }
1107
- if (!pr && !issue) {
1108
- console.error("GitHub platform requires --pr or --issue (or set defaultPr/defaultIssue in .spets/config.yml)");
1109
- 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
+ }
1110
1384
  }
1111
1385
  platform = new GitHubPlatform({
1112
- owner,
1113
- repo,
1114
- prNumber: pr ? parseInt(pr, 10) : void 0,
1115
- issueNumber: issue ? parseInt(issue, 10) : void 0
1386
+ owner: githubInfo.owner,
1387
+ repo: githubInfo.repo,
1388
+ prNumber,
1389
+ issueNumber
1116
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}`);
1117
1396
  } else {
1118
1397
  platform = new CliPlatform();
1398
+ console.log(`Starting workflow: ${taskId}`);
1399
+ console.log(`Query: ${query}`);
1400
+ console.log(`Platform: cli`);
1119
1401
  }
1402
+ console.log("");
1120
1403
  const executor = new Executor({ platform, config, cwd });
1121
1404
  try {
1122
1405
  await executor.executeWorkflow(taskId, query);
@@ -1264,23 +1547,31 @@ function installClaudePlugin() {
1264
1547
  const claudeDir = join3(homedir(), ".claude");
1265
1548
  const commandsDir = join3(claudeDir, "commands");
1266
1549
  mkdirSync2(commandsDir, { recursive: true });
1267
- const skillPath = join3(commandsDir, "spets.md");
1550
+ const skillPath = join3(commandsDir, "sdd-do.md");
1268
1551
  writeFileSync2(skillPath, getClaudeSkillContent());
1269
1552
  console.log("Installed Claude Code plugin.");
1270
1553
  console.log(`Location: ${skillPath}`);
1271
1554
  console.log("");
1272
1555
  console.log("Usage in Claude Code:");
1273
- console.log(' /spets start "your task description"');
1274
- console.log(" /spets status");
1275
- console.log(" /spets resume");
1556
+ console.log(' /sdd-do "your task description"');
1276
1557
  console.log("");
1277
- 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.");
1278
1560
  }
1279
1561
  async function uninstallPlugin(name) {
1280
1562
  if (name === "claude") {
1281
- const skillPath = join3(homedir(), ".claude", "commands", "spets.md");
1282
- if (existsSync3(skillPath)) {
1283
- 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) {
1284
1575
  console.log("Uninstalled Claude Code plugin.");
1285
1576
  } else {
1286
1577
  console.log("Claude Code plugin not installed.");
@@ -1293,10 +1584,11 @@ async function uninstallPlugin(name) {
1293
1584
  async function listPlugins() {
1294
1585
  console.log("Available plugins:");
1295
1586
  console.log("");
1296
- console.log(" claude - Claude Code slash command integration");
1587
+ console.log(" claude - Claude Code /sdd-do skill");
1297
1588
  console.log("");
1298
- const claudeSkillPath = join3(homedir(), ".claude", "commands", "spets.md");
1299
- 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);
1300
1592
  console.log("Installed:");
1301
1593
  if (claudeInstalled) {
1302
1594
  console.log(" - claude");
@@ -1305,81 +1597,202 @@ async function listPlugins() {
1305
1597
  }
1306
1598
  }
1307
1599
  function getClaudeSkillContent() {
1308
- 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
1309
1607
 
1310
- Run spets workflows from Claude Code.
1608
+ Automatically invoked when user uses:
1609
+ - \`/sdd-do\` - Run SDD workflow
1311
1610
 
1312
- ## Usage
1611
+ ---
1313
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
1314
1635
  \`\`\`
1315
- /spets start "task description" - Start a new workflow
1316
- /spets status - Show workflow status
1317
- /spets resume - Resume paused workflow
1636
+
1637
+ ---
1638
+
1639
+ ## Execution Flow
1640
+
1641
+ ### 1. Workflow Start (FIRST ACTION - NO EXCEPTIONS)
1642
+
1643
+ When skill is loaded, **immediately** run:
1644
+
1645
+ \`\`\`bash
1646
+ npx spets orchestrate init "{user_request}"
1647
+ \`\`\`
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
+ }
1318
1666
  \`\`\`
1319
1667
 
1320
- ## Instructions
1668
+ ---
1321
1669
 
1322
- When the user invokes this command:
1670
+ ### 2. Document Generation (type: step)
1323
1671
 
1324
- 1. Parse the subcommand (start, status, resume)
1325
- 2. Execute the appropriate spets CLI command using Bash
1326
- 3. For 'start', read the generated documents and help iterate
1672
+ When you receive \`type: "step"\`:
1327
1673
 
1328
- **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
+ \`\`\`
1329
1683
 
1330
- ### Start Flow
1684
+ ---
1331
1685
 
1332
- 1. Run: \`npx spets start "<query>"\`
1333
- 2. Read the generated plan document from .spets/outputs/<taskId>/
1334
- 3. Present the plan to the user
1335
- 4. If user approves, continue. If they want changes, provide feedback.
1686
+ ### 3. Question Resolution (type: checkpoint, checkpoint: clarify)
1336
1687
 
1337
- ### Status Flow
1688
+ When you receive \`type: "checkpoint"\` with \`checkpoint: "clarify"\`:
1338
1689
 
1339
- 1. Run: \`npx spets status\`
1340
- 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
+ \`\`\`
1341
1696
 
1342
- ### Resume Flow
1697
+ ---
1343
1698
 
1344
- 1. Run: \`npx spets resume\`
1345
- 2. Continue the workflow from where it paused
1699
+ ### 4. Approval Request (type: checkpoint, checkpoint: approve)
1346
1700
 
1347
- ## 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
1724
+
1725
+ ---
1726
+
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 |
1348
1736
 
1349
- User: /spets start "Create a REST API for user management"
1737
+ ---
1350
1738
 
1351
- Claude:
1352
- 1. Runs \`npx spets start "Create a REST API for user management"\`
1353
- 2. Reads the generated plan
1354
- 3. Asks user: "Here's the plan. [shows plan] Would you like to approve or revise?"
1355
- 4. On approve: \`npx spets resume --approve\`
1356
- 5. On revise: \`npx spets resume --revise "user feedback"\`
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
1357
1754
 
1358
1755
  $ARGUMENTS
1359
- command: The spets command to run (start, status, resume)
1360
- args: Additional arguments for the command
1756
+ request: The user's task description for the workflow
1361
1757
  `;
1362
1758
  }
1363
1759
 
1364
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
+ }
1365
1781
  async function githubCommand(options) {
1366
1782
  const cwd = process.cwd();
1367
1783
  if (!spetsExists(cwd)) {
1368
1784
  console.error("Spets not initialized.");
1369
1785
  process.exit(1);
1370
1786
  }
1371
- const configGitHub = getGitHubConfig(cwd);
1372
- const owner = options.owner || configGitHub?.owner;
1373
- const repo = options.repo || configGitHub?.repo;
1374
- const pr = options.pr || (configGitHub?.defaultPr ? String(configGitHub.defaultPr) : void 0);
1375
- const issue = options.issue || (configGitHub?.defaultIssue ? String(configGitHub.defaultIssue) : void 0);
1376
- const { comment, task } = options;
1377
- if (!owner || !repo) {
1378
- 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.");
1379
1790
  process.exit(1);
1380
1791
  }
1792
+ const { owner, repo } = githubInfo;
1793
+ const { pr, issue, comment, task } = options;
1381
1794
  if (!pr && !issue) {
1382
- 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");
1383
1796
  process.exit(1);
1384
1797
  }
1385
1798
  const parsed = parseGitHubCommand(comment);
@@ -1397,7 +1810,7 @@ async function githubCommand(options) {
1397
1810
  }
1398
1811
  if (!taskId) {
1399
1812
  const config2 = loadConfig(cwd);
1400
- const { listTasks: listTasks2 } = await import("./state-H2GQS43T.js");
1813
+ const { listTasks: listTasks2 } = await import("./state-JLYPJWUT.js");
1401
1814
  const tasks = listTasks2(cwd);
1402
1815
  for (const tid of tasks) {
1403
1816
  const state2 = getWorkflowState(tid, config2, cwd);
@@ -1522,13 +1935,476 @@ async function postComment(config, body) {
1522
1935
  });
1523
1936
  }
1524
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
+
1525
2400
  // src/index.ts
1526
2401
  var program = new Command();
1527
- 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");
1528
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);
1529
2404
  program.command("status").description("Show current workflow status").option("-t, --task <taskId>", "Show status for specific task").action(statusCommand);
1530
- 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);
1531
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);
1532
- 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);
1533
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);
1534
2410
  program.parse();