@team-attention/hoyeon-cli 0.11.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -6351,14 +6351,14 @@ var require_format = __commonJS({
6351
6351
  }
6352
6352
  }
6353
6353
  function validateFormat() {
6354
- const formatDef = self.formats[schema];
6355
- if (!formatDef) {
6354
+ const formatDef2 = self.formats[schema];
6355
+ if (!formatDef2) {
6356
6356
  unknownFormat();
6357
6357
  return;
6358
6358
  }
6359
- if (formatDef === true)
6359
+ if (formatDef2 === true)
6360
6360
  return;
6361
- const [fmtType, format, fmtRef] = getFormat(formatDef);
6361
+ const [fmtType, format, fmtRef] = getFormat(formatDef2);
6362
6362
  if (fmtType === ruleType)
6363
6363
  cxt.pass(validCondition());
6364
6364
  function unknownFormat() {
@@ -6380,7 +6380,7 @@ var require_format = __commonJS({
6380
6380
  return ["string", fmtDef, fmt];
6381
6381
  }
6382
6382
  function validCondition() {
6383
- if (typeof formatDef == "object" && !(formatDef instanceof RegExp) && formatDef.async) {
6383
+ if (typeof formatDef2 == "object" && !(formatDef2 instanceof RegExp) && formatDef2.async) {
6384
6384
  if (!schemaEnv.$async)
6385
6385
  throw new Error("async format in sync schema");
6386
6386
  return (0, codegen_1._)`await ${fmtRef}(${data})`;
@@ -7841,6 +7841,11 @@ var dev_spec_v4_schema_default = {
7841
7841
  type: "string",
7842
7842
  enum: ["dev", "plain"],
7843
7843
  description: "Spec type: dev = developer task spec (default), plain = lightweight plain task spec"
7844
+ },
7845
+ schema_version: {
7846
+ type: "string",
7847
+ enum: ["v4", "v5"],
7848
+ description: "Schema version for validation routing. Defaults to v5 if omitted."
7844
7849
  }
7845
7850
  }
7846
7851
  },
@@ -7958,6 +7963,31 @@ var dev_spec_v4_schema_default = {
7958
7963
  }
7959
7964
  }
7960
7965
  },
7966
+ derivedFrom: {
7967
+ type: "object",
7968
+ required: ["parent", "trigger"],
7969
+ additionalProperties: false,
7970
+ description: "Provenance record linking a derived task back to its planned parent",
7971
+ properties: {
7972
+ parent: {
7973
+ type: "string",
7974
+ description: "ID of the planned parent task (depth-1 only)"
7975
+ },
7976
+ source: {
7977
+ type: "string",
7978
+ description: "Source context that triggered the derivation (e.g. file path, agent name)"
7979
+ },
7980
+ trigger: {
7981
+ type: "string",
7982
+ enum: ["adapt", "retry", "code_review", "final_verify"],
7983
+ description: "Event that caused this task to be derived"
7984
+ },
7985
+ reason: {
7986
+ type: "string",
7987
+ description: "Human-readable explanation for the derivation"
7988
+ }
7989
+ }
7990
+ },
7961
7991
  task: {
7962
7992
  type: "object",
7963
7993
  required: ["id", "action", "type"],
@@ -7969,6 +7999,16 @@ var dev_spec_v4_schema_default = {
7969
7999
  type: "string",
7970
8000
  enum: ["work", "verification"]
7971
8001
  },
8002
+ origin: {
8003
+ type: "string",
8004
+ enum: ["planned", "derived", "adapted"],
8005
+ default: "planned",
8006
+ description: "How this task was created: planned=authored in spec, derived=created at runtime via spec derive, adapted=manually adjusted from a derived task"
8007
+ },
8008
+ derived_from: {
8009
+ $ref: "#/$defs/derivedFrom",
8010
+ description: "Provenance record \u2014 required when origin is derived or adapted"
8011
+ },
7972
8012
  risk: {
7973
8013
  type: "string",
7974
8014
  enum: ["low", "medium", "high"]
@@ -8060,7 +8100,13 @@ var dev_spec_v4_schema_default = {
8060
8100
  acceptance_criteria: { $ref: "#/$defs/taskAcceptanceCriteria" },
8061
8101
  tool: { type: "string" },
8062
8102
  args: { type: "string" }
8063
- }
8103
+ },
8104
+ allOf: [
8105
+ {
8106
+ if: { properties: { origin: { enum: ["derived", "adapted"] } }, required: ["origin"] },
8107
+ then: { required: ["derived_from"] }
8108
+ }
8109
+ ]
8064
8110
  },
8065
8111
  historyEntry: {
8066
8112
  type: "object",
@@ -8088,28 +8134,819 @@ var dev_spec_v4_schema_default = {
8088
8134
  type: "string",
8089
8135
  enum: ["PASS", "FAIL", "SKIP"]
8090
8136
  }
8091
- }
8137
+ }
8138
+ },
8139
+ taskAcceptanceCriteria: {
8140
+ type: "object",
8141
+ additionalProperties: false,
8142
+ properties: {
8143
+ functional: {
8144
+ type: "array",
8145
+ items: { $ref: "#/$defs/acceptanceCriterionItem" }
8146
+ },
8147
+ static: {
8148
+ type: "array",
8149
+ items: { $ref: "#/$defs/acceptanceCriterionItem" }
8150
+ },
8151
+ runtime: {
8152
+ type: "array",
8153
+ items: { $ref: "#/$defs/acceptanceCriterionItem" }
8154
+ },
8155
+ cleanup: {
8156
+ type: "array",
8157
+ items: { $ref: "#/$defs/acceptanceCriterionItem" }
8158
+ }
8159
+ }
8160
+ },
8161
+ verificationSummary: {
8162
+ type: "object",
8163
+ additionalProperties: false,
8164
+ properties: {
8165
+ agent_items: {
8166
+ type: "array",
8167
+ items: {
8168
+ type: "object",
8169
+ required: ["id", "criterion", "method"],
8170
+ additionalProperties: false,
8171
+ properties: {
8172
+ id: { type: "string" },
8173
+ criterion: { type: "string" },
8174
+ method: { type: "string" },
8175
+ related_task: { type: "string" }
8176
+ }
8177
+ }
8178
+ },
8179
+ human_items: {
8180
+ type: "array",
8181
+ items: {
8182
+ type: "object",
8183
+ required: ["id", "criterion", "reason"],
8184
+ additionalProperties: false,
8185
+ properties: {
8186
+ id: { type: "string" },
8187
+ criterion: { type: "string" },
8188
+ reason: { type: "string" },
8189
+ review_material: { type: "string" }
8190
+ }
8191
+ }
8192
+ },
8193
+ sandbox_items: {
8194
+ type: "array",
8195
+ items: {
8196
+ type: "object",
8197
+ required: ["id", "scenario", "agent", "method"],
8198
+ additionalProperties: false,
8199
+ properties: {
8200
+ id: { type: "string" },
8201
+ scenario: { type: "string" },
8202
+ agent: { type: "string" },
8203
+ method: { type: "string" }
8204
+ }
8205
+ }
8206
+ },
8207
+ gaps: {
8208
+ type: "array",
8209
+ items: { type: "string" }
8210
+ }
8211
+ }
8212
+ },
8213
+ externalDependencies: {
8214
+ description: "Human-only tasks that the agent cannot perform (infra setup, API keys, deployment, etc.). If automatable, put it in the Task DAG instead.",
8215
+ type: "object",
8216
+ additionalProperties: false,
8217
+ properties: {
8218
+ pre_work: {
8219
+ description: "Human actions that must be completed BEFORE execution starts.",
8220
+ type: "array",
8221
+ items: {
8222
+ type: "object",
8223
+ required: ["dependency", "action"],
8224
+ additionalProperties: false,
8225
+ properties: {
8226
+ id: { type: "string", description: "Optional identifier (e.g., PW-1)" },
8227
+ dependency: { type: "string" },
8228
+ action: { type: "string", description: "What the human must do" },
8229
+ command: { type: "string", description: "Hint command for the human (not agent-executed)" },
8230
+ blocking: { type: "boolean", description: "If true, execution halts until human confirms completion" }
8231
+ }
8232
+ }
8233
+ },
8234
+ post_work: {
8235
+ description: "Human actions to perform AFTER execution completes.",
8236
+ type: "array",
8237
+ items: {
8238
+ type: "object",
8239
+ required: ["dependency", "action"],
8240
+ additionalProperties: false,
8241
+ properties: {
8242
+ id: { type: "string", description: "Optional identifier (e.g., POW-1)" },
8243
+ dependency: { type: "string" },
8244
+ action: { type: "string", description: "What the human must do" },
8245
+ command: { type: "string", description: "Hint command for the human (not agent-executed)" }
8246
+ }
8247
+ }
8248
+ }
8249
+ }
8250
+ },
8251
+ researchFindings: {
8252
+ type: "object",
8253
+ additionalProperties: false,
8254
+ properties: {
8255
+ summary: {
8256
+ type: "string",
8257
+ description: "High-level summary of exploration results"
8258
+ },
8259
+ patterns: {
8260
+ type: "array",
8261
+ description: "Existing code patterns discovered (file:line format)",
8262
+ items: {
8263
+ type: "object",
8264
+ required: ["path", "description"],
8265
+ additionalProperties: false,
8266
+ properties: {
8267
+ path: { type: "string" },
8268
+ start_line: { type: "integer", minimum: 1 },
8269
+ end_line: { type: "integer", minimum: 1 },
8270
+ description: { type: "string" }
8271
+ }
8272
+ }
8273
+ },
8274
+ structure: {
8275
+ type: "array",
8276
+ description: "Key directory/file structure paths",
8277
+ items: { type: "string" }
8278
+ },
8279
+ commands: {
8280
+ type: "object",
8281
+ description: "Project commands discovered",
8282
+ additionalProperties: false,
8283
+ properties: {
8284
+ typecheck: { type: "string" },
8285
+ lint: { type: "string" },
8286
+ test: { type: "string" },
8287
+ build: { type: "string" }
8288
+ }
8289
+ },
8290
+ documentation: {
8291
+ type: "array",
8292
+ description: "Internal docs findings (ADRs, conventions, constraints)",
8293
+ items: {
8294
+ type: "object",
8295
+ required: ["path", "description"],
8296
+ additionalProperties: false,
8297
+ properties: {
8298
+ path: { type: "string" },
8299
+ line: { type: "integer", minimum: 1 },
8300
+ description: { type: "string" }
8301
+ }
8302
+ }
8303
+ },
8304
+ ux_review: {
8305
+ type: "object",
8306
+ description: "UX impact assessment from ux-reviewer agent",
8307
+ additionalProperties: false,
8308
+ properties: {
8309
+ current_flow: { type: "string" },
8310
+ impact: { type: "string" },
8311
+ recommendations: {
8312
+ type: "array",
8313
+ items: { type: "string" }
8314
+ },
8315
+ must_not_do: {
8316
+ type: "array",
8317
+ items: { type: "string" }
8318
+ }
8319
+ }
8320
+ }
8321
+ }
8322
+ },
8323
+ constraint: {
8324
+ type: "object",
8325
+ required: ["id", "type", "rule", "verified_by", "verify"],
8326
+ additionalProperties: false,
8327
+ properties: {
8328
+ id: { type: "string" },
8329
+ type: {
8330
+ type: "string",
8331
+ enum: ["must_not_do", "preserve"]
8332
+ },
8333
+ rule: { type: "string" },
8334
+ verified_by: {
8335
+ type: "string",
8336
+ enum: ["machine", "agent", "human"]
8337
+ },
8338
+ verify: { $ref: "#/$defs/verify" }
8339
+ },
8340
+ allOf: [
8341
+ {
8342
+ if: {
8343
+ properties: { verified_by: { const: "machine" } },
8344
+ required: ["verified_by"]
8345
+ },
8346
+ then: {
8347
+ properties: {
8348
+ verify: { $ref: "#/$defs/verifyCommand" }
8349
+ }
8350
+ }
8351
+ },
8352
+ {
8353
+ if: {
8354
+ properties: { verified_by: { const: "agent" } },
8355
+ required: ["verified_by"]
8356
+ },
8357
+ then: {
8358
+ properties: {
8359
+ verify: { $ref: "#/$defs/verifyAssertion" }
8360
+ }
8361
+ }
8362
+ },
8363
+ {
8364
+ if: {
8365
+ properties: { verified_by: { const: "human" } },
8366
+ required: ["verified_by"]
8367
+ },
8368
+ then: {
8369
+ properties: {
8370
+ verify: { $ref: "#/$defs/verifyInstruction" }
8371
+ }
8372
+ }
8373
+ }
8374
+ ]
8375
+ }
8376
+ }
8377
+ };
8378
+
8379
+ // schemas/dev-spec-v5.schema.json
8380
+ var dev_spec_v5_schema_default = {
8381
+ $schema: "https://json-schema.org/draft/2020-12/schema",
8382
+ $id: "dev-spec/v5",
8383
+ title: "dev-spec v5",
8384
+ description: "JSON Schema for dev spec v5 \u2014 unified spec + state document. AC uses scenarios[] + checks[] instead of functional/static/runtime/cleanup categories.",
8385
+ type: "object",
8386
+ required: ["meta", "tasks"],
8387
+ additionalProperties: false,
8388
+ properties: {
8389
+ $schema: { type: "string" },
8390
+ meta: { $ref: "#/$defs/meta" },
8391
+ context: { $ref: "#/$defs/context" },
8392
+ requirements: {
8393
+ type: "array",
8394
+ items: { $ref: "#/$defs/requirement" }
8395
+ },
8396
+ tasks: {
8397
+ type: "array",
8398
+ minItems: 1,
8399
+ items: { $ref: "#/$defs/task" }
8400
+ },
8401
+ constraints: {
8402
+ type: "array",
8403
+ items: { $ref: "#/$defs/constraint" }
8404
+ },
8405
+ history: {
8406
+ type: "array",
8407
+ items: { $ref: "#/$defs/historyEntry" }
8408
+ },
8409
+ verification_summary: {
8410
+ $ref: "#/$defs/verificationSummary",
8411
+ description: "Derived from requirements \u2014 A/H/S classification. Do not author independently."
8412
+ },
8413
+ external_dependencies: { $ref: "#/$defs/externalDependencies" }
8414
+ },
8415
+ $defs: {
8416
+ sha256Hash: {
8417
+ type: "string",
8418
+ pattern: "^[a-f0-9]{64}$",
8419
+ description: "Raw SHA-256 hex digest (64 lowercase hex characters, no prefix)"
8420
+ },
8421
+ reference: {
8422
+ type: "object",
8423
+ required: ["path"],
8424
+ additionalProperties: false,
8425
+ properties: {
8426
+ path: { type: "string" },
8427
+ start_line: { type: "integer", minimum: 1 },
8428
+ end_line: { type: "integer", minimum: 1 }
8429
+ }
8430
+ },
8431
+ taskConstraint: {
8432
+ type: "object",
8433
+ required: ["type", "target"],
8434
+ additionalProperties: false,
8435
+ properties: {
8436
+ type: {
8437
+ type: "string",
8438
+ enum: ["no_modify", "no_delete", "preserve_string", "read_only"]
8439
+ },
8440
+ target: { type: "string" }
8441
+ }
8442
+ },
8443
+ expect: {
8444
+ type: "object",
8445
+ required: ["exit_code"],
8446
+ additionalProperties: false,
8447
+ properties: {
8448
+ exit_code: { type: "integer" },
8449
+ stdout_contains: { type: "string" },
8450
+ stderr_empty: { type: "boolean" }
8451
+ }
8452
+ },
8453
+ verifyCommand: {
8454
+ type: "object",
8455
+ required: ["type", "run", "expect"],
8456
+ additionalProperties: false,
8457
+ properties: {
8458
+ type: { type: "string", const: "command" },
8459
+ run: { type: "string" },
8460
+ expect: { $ref: "#/$defs/expect" }
8461
+ }
8462
+ },
8463
+ verifyAssertion: {
8464
+ type: "object",
8465
+ required: ["type", "checks"],
8466
+ additionalProperties: false,
8467
+ properties: {
8468
+ type: { type: "string", const: "assertion" },
8469
+ checks: {
8470
+ type: "array",
8471
+ minItems: 1,
8472
+ items: { type: "string" }
8473
+ }
8474
+ }
8475
+ },
8476
+ verifyInstruction: {
8477
+ type: "object",
8478
+ required: ["type", "ask"],
8479
+ additionalProperties: false,
8480
+ properties: {
8481
+ type: { type: "string", const: "instruction" },
8482
+ ask: { type: "string" }
8483
+ }
8484
+ },
8485
+ verify: {
8486
+ oneOf: [
8487
+ { $ref: "#/$defs/verifyCommand" },
8488
+ { $ref: "#/$defs/verifyAssertion" },
8489
+ { $ref: "#/$defs/verifyInstruction" }
8490
+ ]
8491
+ },
8492
+ scenario: {
8493
+ type: "object",
8494
+ required: ["id", "given", "when", "then", "verified_by", "verify"],
8495
+ additionalProperties: false,
8496
+ properties: {
8497
+ id: { type: "string" },
8498
+ given: { type: "string" },
8499
+ when: { type: "string" },
8500
+ then: { type: "string" },
8501
+ verified_by: {
8502
+ type: "string",
8503
+ enum: ["machine", "agent", "human"],
8504
+ description: "WHO verifies: machine=automated command, agent=AI assertion, human=manual inspection"
8505
+ },
8506
+ execution_env: {
8507
+ type: "string",
8508
+ enum: ["host", "sandbox", "ci"],
8509
+ description: "WHERE verification runs: host=local, sandbox=docker/container, ci=CI pipeline"
8510
+ },
8511
+ verify: { $ref: "#/$defs/verify" },
8512
+ status: {
8513
+ type: "string",
8514
+ enum: ["pass", "fail", "pending", "skipped"],
8515
+ default: "pending",
8516
+ description: "Verification result for this scenario"
8517
+ },
8518
+ verified_by_task: {
8519
+ oneOf: [
8520
+ { type: "string" },
8521
+ { type: "null" }
8522
+ ],
8523
+ default: null,
8524
+ description: "The task ID that verified this scenario (e.g. 'T1' or 'T_SV1')"
8525
+ }
8526
+ },
8527
+ allOf: [
8528
+ {
8529
+ if: {
8530
+ properties: { verified_by: { const: "machine" } },
8531
+ required: ["verified_by"]
8532
+ },
8533
+ then: {
8534
+ properties: {
8535
+ verify: { $ref: "#/$defs/verifyCommand" }
8536
+ }
8537
+ }
8538
+ },
8539
+ {
8540
+ if: {
8541
+ properties: { verified_by: { const: "agent" } },
8542
+ required: ["verified_by"]
8543
+ },
8544
+ then: {
8545
+ properties: {
8546
+ verify: { $ref: "#/$defs/verifyAssertion" }
8547
+ }
8548
+ }
8549
+ },
8550
+ {
8551
+ if: {
8552
+ properties: { verified_by: { const: "human" } },
8553
+ required: ["verified_by"]
8554
+ },
8555
+ then: {
8556
+ properties: {
8557
+ verify: { $ref: "#/$defs/verifyInstruction" }
8558
+ }
8559
+ }
8560
+ }
8561
+ ]
8562
+ },
8563
+ meta: {
8564
+ type: "object",
8565
+ required: ["name", "goal"],
8566
+ additionalProperties: false,
8567
+ properties: {
8568
+ name: { type: "string" },
8569
+ goal: { type: "string" },
8570
+ non_goals: {
8571
+ type: "array",
8572
+ items: { type: "string" },
8573
+ description: "What this project is explicitly NOT trying to achieve (strategic scope exclusion)"
8574
+ },
8575
+ deliverables: {
8576
+ type: "array",
8577
+ items: {
8578
+ type: "object",
8579
+ required: ["path", "description"],
8580
+ additionalProperties: false,
8581
+ properties: {
8582
+ path: { type: "string" },
8583
+ description: { type: "string" }
8584
+ }
8585
+ }
8586
+ },
8587
+ mode: {
8588
+ type: "object",
8589
+ additionalProperties: false,
8590
+ properties: {
8591
+ depth: {
8592
+ type: "string",
8593
+ enum: ["quick", "standard"]
8594
+ },
8595
+ interaction: {
8596
+ type: "string",
8597
+ enum: ["interactive", "autopilot"]
8598
+ }
8599
+ }
8600
+ },
8601
+ derived_from: { type: "string" },
8602
+ created_at: { type: "string" },
8603
+ updated_at: { type: "string" },
8604
+ approved_by: { type: "string" },
8605
+ approved_at: { type: "string" },
8606
+ type: {
8607
+ type: "string",
8608
+ enum: ["dev", "plain"],
8609
+ description: "Spec type: dev = developer task spec (default), plain = lightweight plain task spec"
8610
+ },
8611
+ schema_version: {
8612
+ type: "string",
8613
+ enum: ["v4", "v5"],
8614
+ description: "Schema version for validation routing. Defaults to v5 if omitted."
8615
+ }
8616
+ }
8617
+ },
8618
+ context: {
8619
+ type: "object",
8620
+ additionalProperties: false,
8621
+ properties: {
8622
+ request: { type: "string" },
8623
+ confirmed_goal: {
8624
+ type: "string",
8625
+ description: "The confirmed goal statement from the mirror phase. Set after user confirms the mirrored interpretation."
8626
+ },
8627
+ interview: {
8628
+ type: "array",
8629
+ items: {
8630
+ type: "object",
8631
+ required: ["topic", "decision"],
8632
+ additionalProperties: false,
8633
+ properties: {
8634
+ topic: { type: "string" },
8635
+ decision: { type: "string" }
8636
+ }
8637
+ }
8638
+ },
8639
+ research: {
8640
+ oneOf: [
8641
+ { type: "string" },
8642
+ { $ref: "#/$defs/researchFindings" }
8643
+ ]
8644
+ },
8645
+ assumptions: {
8646
+ type: "array",
8647
+ items: {
8648
+ type: "object",
8649
+ required: ["id", "belief", "if_wrong", "impact"],
8650
+ additionalProperties: false,
8651
+ properties: {
8652
+ id: { type: "string" },
8653
+ belief: { type: "string" },
8654
+ if_wrong: { type: "string" },
8655
+ impact: {
8656
+ type: "string",
8657
+ enum: ["minor", "major", "critical"]
8658
+ }
8659
+ }
8660
+ }
8661
+ },
8662
+ decisions: {
8663
+ type: "array",
8664
+ items: {
8665
+ type: "object",
8666
+ required: ["id", "decision", "rationale"],
8667
+ additionalProperties: false,
8668
+ properties: {
8669
+ id: { type: "string" },
8670
+ decision: { type: "string" },
8671
+ rationale: { type: "string" },
8672
+ alternatives_rejected: {
8673
+ type: "array",
8674
+ items: {
8675
+ type: "object",
8676
+ required: ["option", "reason"],
8677
+ additionalProperties: false,
8678
+ properties: {
8679
+ option: { type: "string" },
8680
+ reason: { type: "string" }
8681
+ }
8682
+ }
8683
+ }
8684
+ }
8685
+ }
8686
+ },
8687
+ known_gaps: {
8688
+ type: "array",
8689
+ items: {
8690
+ type: "object",
8691
+ required: ["gap", "severity", "mitigation"],
8692
+ additionalProperties: false,
8693
+ properties: {
8694
+ gap: { type: "string" },
8695
+ severity: {
8696
+ type: "string",
8697
+ enum: ["low", "medium", "high", "critical"]
8698
+ },
8699
+ mitigation: { type: "string" },
8700
+ auto_merged: {
8701
+ type: "boolean",
8702
+ description: "True when this gap was auto-merged (medium/low severity) without asking the user"
8703
+ }
8704
+ }
8705
+ }
8706
+ }
8707
+ }
8708
+ },
8709
+ requirement: {
8710
+ type: "object",
8711
+ required: ["id", "behavior", "priority", "scenarios"],
8712
+ additionalProperties: false,
8713
+ properties: {
8714
+ id: { type: "string" },
8715
+ behavior: { type: "string" },
8716
+ priority: {
8717
+ type: "integer",
8718
+ minimum: 1,
8719
+ maximum: 5
8720
+ },
8721
+ scenarios: {
8722
+ type: "array",
8723
+ items: { $ref: "#/$defs/scenario" }
8724
+ },
8725
+ source: {
8726
+ type: "object",
8727
+ required: ["type"],
8728
+ additionalProperties: false,
8729
+ description: "Traceability: where this requirement originated from",
8730
+ properties: {
8731
+ type: {
8732
+ type: "string",
8733
+ enum: ["goal", "decision", "gap", "implicit", "negative"],
8734
+ description: "Category of origin: goal=from project goal, decision=from a context decision, gap=from a known gap, implicit=inferred, negative=from non-goals"
8735
+ },
8736
+ ref: {
8737
+ type: "string",
8738
+ description: "Optional reference ID (e.g. 'D3', 'G1') pointing to a specific context entry"
8739
+ }
8740
+ }
8741
+ }
8742
+ }
8743
+ },
8744
+ checkpoint: {
8745
+ type: "object",
8746
+ required: ["enabled"],
8747
+ additionalProperties: false,
8748
+ properties: {
8749
+ enabled: { type: "boolean" },
8750
+ message: { type: "string" },
8751
+ condition: {
8752
+ type: "string",
8753
+ enum: ["always", "on_fulfill", "manual"]
8754
+ }
8755
+ }
8756
+ },
8757
+ derivedFrom: {
8758
+ type: "object",
8759
+ required: ["parent", "trigger"],
8760
+ additionalProperties: false,
8761
+ description: "Provenance record linking a derived task back to its planned parent",
8762
+ properties: {
8763
+ parent: {
8764
+ type: "string",
8765
+ description: "ID of the planned parent task (depth-1 only)"
8766
+ },
8767
+ source: {
8768
+ type: "string",
8769
+ description: "Source context that triggered the derivation (e.g. file path, agent name)"
8770
+ },
8771
+ trigger: {
8772
+ type: "string",
8773
+ enum: ["adapt", "retry", "code_review", "final_verify"],
8774
+ description: "Event that caused this task to be derived"
8775
+ },
8776
+ reason: {
8777
+ type: "string",
8778
+ description: "Human-readable explanation for the derivation"
8779
+ }
8780
+ }
8781
+ },
8782
+ taskCheck: {
8783
+ type: "object",
8784
+ required: ["type", "run"],
8785
+ additionalProperties: false,
8786
+ description: "A static/build/lint/format check to run as part of task acceptance",
8787
+ properties: {
8788
+ type: {
8789
+ type: "string",
8790
+ enum: ["static", "build", "lint", "format"],
8791
+ description: "Category of check: static=type check, build=compilation, lint=linting, format=formatting"
8792
+ },
8793
+ run: {
8794
+ type: "string",
8795
+ description: "Shell command to execute for this check"
8796
+ }
8797
+ }
8798
+ },
8799
+ taskAcceptanceCriteria: {
8800
+ type: "object",
8801
+ required: ["scenarios", "checks"],
8802
+ additionalProperties: false,
8803
+ description: "v5: AC is expressed as scenario references + automated checks. scenarios[] references requirements[].scenarios[].id. checks[] are runnable static/build/lint/format commands.",
8804
+ properties: {
8805
+ scenarios: {
8806
+ type: "array",
8807
+ description: "List of scenario IDs from requirements[].scenarios[].id that this task fulfills",
8808
+ items: { type: "string" }
8809
+ },
8810
+ checks: {
8811
+ type: "array",
8812
+ description: "Automated checks to verify the task (static, build, lint, format)",
8813
+ items: { $ref: "#/$defs/taskCheck" }
8814
+ }
8815
+ }
8816
+ },
8817
+ task: {
8818
+ type: "object",
8819
+ required: ["id", "action", "type"],
8820
+ additionalProperties: false,
8821
+ properties: {
8822
+ id: { type: "string" },
8823
+ action: { type: "string" },
8824
+ type: {
8825
+ type: "string",
8826
+ enum: ["work", "verification"]
8827
+ },
8828
+ origin: {
8829
+ type: "string",
8830
+ enum: ["planned", "derived", "adapted"],
8831
+ default: "planned",
8832
+ description: "How this task was created: planned=authored in spec, derived=created at runtime via spec derive, adapted=manually adjusted from a derived task"
8833
+ },
8834
+ derived_from: {
8835
+ $ref: "#/$defs/derivedFrom",
8836
+ description: "Provenance record \u2014 required when origin is derived or adapted"
8837
+ },
8838
+ risk: {
8839
+ type: "string",
8840
+ enum: ["low", "medium", "high"]
8841
+ },
8842
+ file_scope: {
8843
+ type: "array",
8844
+ items: { type: "string" }
8845
+ },
8846
+ fulfills: {
8847
+ type: "array",
8848
+ items: { type: "string" }
8849
+ },
8850
+ depends_on: {
8851
+ type: "array",
8852
+ items: { type: "string" }
8853
+ },
8854
+ steps: {
8855
+ type: "array",
8856
+ description: "Task steps \u2014 either plain strings (backward compat) or structured objects with done tracking",
8857
+ items: {
8858
+ oneOf: [
8859
+ { type: "string" },
8860
+ {
8861
+ type: "object",
8862
+ required: ["text"],
8863
+ additionalProperties: false,
8864
+ properties: {
8865
+ text: { type: "string" },
8866
+ done: { type: "boolean", default: false }
8867
+ }
8868
+ }
8869
+ ]
8870
+ }
8871
+ },
8872
+ references: {
8873
+ type: "array",
8874
+ items: { $ref: "#/$defs/reference" }
8875
+ },
8876
+ inputs: {
8877
+ type: "array",
8878
+ items: {
8879
+ type: "object",
8880
+ required: ["from_task", "artifact"],
8881
+ additionalProperties: false,
8882
+ properties: {
8883
+ from_task: { type: "string" },
8884
+ artifact: { type: "string" }
8885
+ }
8886
+ }
8887
+ },
8888
+ outputs: {
8889
+ type: "array",
8890
+ items: {
8891
+ type: "object",
8892
+ required: ["id", "path"],
8893
+ additionalProperties: false,
8894
+ properties: {
8895
+ id: { type: "string" },
8896
+ path: { type: "string" }
8897
+ }
8898
+ }
8899
+ },
8900
+ task_constraints: {
8901
+ type: "array",
8902
+ items: { $ref: "#/$defs/taskConstraint" }
8903
+ },
8904
+ checkpoint: {
8905
+ oneOf: [
8906
+ { $ref: "#/$defs/checkpoint" },
8907
+ { type: "null" }
8908
+ ]
8909
+ },
8910
+ status: {
8911
+ type: "string",
8912
+ enum: ["pending", "in_progress", "done"],
8913
+ default: "pending"
8914
+ },
8915
+ started_at: { type: "string" },
8916
+ completed_at: { type: "string" },
8917
+ summary: { type: "string" },
8918
+ required_tools: {
8919
+ type: "array",
8920
+ items: { type: "string" }
8921
+ },
8922
+ must_not_do: {
8923
+ type: "array",
8924
+ items: { type: "string" }
8925
+ },
8926
+ acceptance_criteria: { $ref: "#/$defs/taskAcceptanceCriteria" },
8927
+ tool: { type: "string" },
8928
+ args: { type: "string" }
8929
+ },
8930
+ allOf: [
8931
+ {
8932
+ if: { properties: { origin: { enum: ["derived", "adapted"] } }, required: ["origin"] },
8933
+ then: { required: ["derived_from"] }
8934
+ }
8935
+ ]
8092
8936
  },
8093
- taskAcceptanceCriteria: {
8937
+ historyEntry: {
8094
8938
  type: "object",
8939
+ required: ["ts", "type"],
8095
8940
  additionalProperties: false,
8096
8941
  properties: {
8097
- functional: {
8098
- type: "array",
8099
- items: { $ref: "#/$defs/acceptanceCriterionItem" }
8100
- },
8101
- static: {
8102
- type: "array",
8103
- items: { $ref: "#/$defs/acceptanceCriterionItem" }
8104
- },
8105
- runtime: {
8106
- type: "array",
8107
- items: { $ref: "#/$defs/acceptanceCriterionItem" }
8942
+ ts: { type: "string" },
8943
+ type: {
8944
+ type: "string",
8945
+ enum: ["spec_created", "task_start", "task_done", "tasks_changed", "spec_updated"]
8108
8946
  },
8109
- cleanup: {
8110
- type: "array",
8111
- items: { $ref: "#/$defs/acceptanceCriterionItem" }
8112
- }
8947
+ task: { type: "string" },
8948
+ summary: { type: "string" },
8949
+ detail: { type: "string" }
8113
8950
  }
8114
8951
  },
8115
8952
  verificationSummary: {
@@ -8371,6 +9208,8 @@ var SPEC_HELP = `
8371
9208
  Usage:
8372
9209
  hoyeon-cli spec init <name> --goal "..." <path> Create a minimal valid spec.json
8373
9210
  hoyeon-cli spec merge <path> --json '{...}' Deep-merge a JSON fragment into spec.json
9211
+ --append: concatenate arrays
9212
+ --patch: ID-based merge (match by id, update in place)
8374
9213
  hoyeon-cli spec validate <path> Validate a spec.json file against the schema
8375
9214
  hoyeon-cli spec plan <path> [--format text|mermaid|json] Show execution plan with parallel groups
8376
9215
  hoyeon-cli spec task <task-id> --status <status> [--summary "..."] <path> Update task status
@@ -8379,6 +9218,14 @@ Usage:
8379
9218
  hoyeon-cli spec meta <path> Show spec meta (name, goal, non_goals, mode, etc.)
8380
9219
  hoyeon-cli spec check <path> Check internal consistency
8381
9220
  hoyeon-cli spec amend --reason <feedback-id> --spec <path> Amend spec.json based on feedback
9221
+ hoyeon-cli spec guide [section] Show schema guide for a section
9222
+ hoyeon-cli spec scenario <scenario-id> --get <path> Get scenario details as JSON
9223
+ hoyeon-cli spec derive --parent <id> --source <src> --trigger <t> --action <a> --reason <r> <path> Create a derived task
9224
+ hoyeon-cli spec drift <path> Show drift ratio (derived vs planned tasks)
9225
+ hoyeon-cli spec requirement --status <path> [--json] Show all requirements/scenarios with verification status
9226
+ hoyeon-cli spec requirement <id> --get <path> Get individual scenario as JSON
9227
+ hoyeon-cli spec requirement <id> --status pass|fail|skipped --task <task_id> [--reason <msg>] <path> Update scenario status
9228
+ hoyeon-cli spec sandbox-tasks <path> [--json] Auto-generate T_SANDBOX + T_SV tasks for sandbox scenarios
8382
9229
 
8383
9230
  Options:
8384
9231
  --help, -h Show this help message
@@ -8394,14 +9241,36 @@ Examples:
8394
9241
  hoyeon-cli spec meta ./spec.json
8395
9242
  hoyeon-cli spec check ./spec.json
8396
9243
  hoyeon-cli spec amend --reason fb-001 --spec ./spec.json
9244
+ hoyeon-cli spec requirement --status ./spec.json
9245
+ hoyeon-cli spec requirement R1-S1 --get ./spec.json
9246
+ hoyeon-cli spec requirement R1-S1 --status pass --task T1 ./spec.json
9247
+ hoyeon-cli spec sandbox-tasks ./spec.json
8397
9248
  `;
8398
- function loadSchema() {
8399
- return dev_spec_v4_schema_default;
9249
+ function loadSchema(specData) {
9250
+ if (specData?.meta?.schema_version === "v4") {
9251
+ return dev_spec_v4_schema_default;
9252
+ }
9253
+ return dev_spec_v5_schema_default;
9254
+ }
9255
+ function printGuideHints(errors) {
9256
+ const sections = /* @__PURE__ */ new Set();
9257
+ for (const e of errors) {
9258
+ const path2 = e.instancePath || "";
9259
+ const match = path2.match(/^\/([^/]+)/);
9260
+ if (match) sections.add(match[1]);
9261
+ }
9262
+ if (sections.size > 0) {
9263
+ process.stderr.write("\nHint: check schema with:\n");
9264
+ for (const s of sections) {
9265
+ process.stderr.write(` hoyeon-cli spec guide ${s}
9266
+ `);
9267
+ }
9268
+ }
8400
9269
  }
8401
9270
  function validateSpec(specData) {
8402
9271
  let schema;
8403
9272
  try {
8404
- schema = loadSchema();
9273
+ schema = loadSchema(specData);
8405
9274
  } catch (err) {
8406
9275
  process.stderr.write(`Error: could not load schema: ${err.message}
8407
9276
  `);
@@ -8418,16 +9287,30 @@ function validateSpec(specData) {
8418
9287
  process.stderr.write(` ${path2}: ${e.message}
8419
9288
  `);
8420
9289
  }
9290
+ printGuideHints(validate.errors);
8421
9291
  process.exit(1);
8422
9292
  }
8423
9293
  }
8424
- function deepMerge(target, source, append = false) {
9294
+ function deepMerge(target, source, append = false, patch = false) {
8425
9295
  for (const key of Object.keys(source)) {
8426
9296
  if (source[key] === null || source[key] === void 0) {
8427
9297
  continue;
8428
9298
  }
8429
9299
  if (Array.isArray(source[key])) {
8430
- if (append && Array.isArray(target[key])) {
9300
+ if (patch && Array.isArray(target[key])) {
9301
+ for (const item of source[key]) {
9302
+ if (item && typeof item === "object" && item.id) {
9303
+ const idx = target[key].findIndex((t) => t && t.id === item.id);
9304
+ if (idx >= 0) {
9305
+ target[key][idx] = { ...target[key][idx], ...item };
9306
+ } else {
9307
+ target[key].push(item);
9308
+ }
9309
+ } else {
9310
+ target[key].push(item);
9311
+ }
9312
+ }
9313
+ } else if (append && Array.isArray(target[key])) {
8431
9314
  target[key] = target[key].concat(source[key]);
8432
9315
  } else {
8433
9316
  target[key] = source[key];
@@ -8436,7 +9319,7 @@ function deepMerge(target, source, append = false) {
8436
9319
  if (!target[key] || typeof target[key] !== "object" || Array.isArray(target[key])) {
8437
9320
  target[key] = {};
8438
9321
  }
8439
- deepMerge(target[key], source[key], append);
9322
+ deepMerge(target[key], source[key], append, patch);
8440
9323
  } else {
8441
9324
  target[key] = source[key];
8442
9325
  }
@@ -8528,7 +9411,7 @@ async function handleMerge(args) {
8528
9411
  }
8529
9412
  if (!parsed.json) {
8530
9413
  process.stderr.write("Error: --json '{...}' is required\n");
8531
- process.stderr.write("Usage: hoyeon-cli spec merge <path> --json '{...}' [--append]\n");
9414
+ process.stderr.write("Usage: hoyeon-cli spec merge <path> --json '{...}' [--append] [--patch]\n");
8532
9415
  process.exit(1);
8533
9416
  }
8534
9417
  let fragment;
@@ -8546,7 +9429,12 @@ async function handleMerge(args) {
8546
9429
  const specPath = resolve(filePath);
8547
9430
  const specData = loadSpec(specPath);
8548
9431
  const append = parsed.append === true;
8549
- deepMerge(specData, fragment, append);
9432
+ const patch = parsed.patch === true;
9433
+ if (append && patch) {
9434
+ process.stderr.write("Error: --append and --patch are mutually exclusive\n");
9435
+ process.exit(1);
9436
+ }
9437
+ deepMerge(specData, fragment, append, patch);
8550
9438
  const now = (/* @__PURE__ */ new Date()).toISOString();
8551
9439
  if (!specData.history) specData.history = [];
8552
9440
  const mergedKeys = Object.keys(fragment).join(", ");
@@ -8565,6 +9453,7 @@ async function handleMerge(args) {
8565
9453
  process.stdout.write(` merged keys: ${mergedKeys}
8566
9454
  `);
8567
9455
  if (append) process.stdout.write(" mode: append (arrays concatenated)\n");
9456
+ if (patch) process.stdout.write(" mode: patch (ID-based merge)\n");
8568
9457
  process.exit(0);
8569
9458
  }
8570
9459
  async function handleValidate(args) {
@@ -8593,7 +9482,7 @@ async function handleValidate(args) {
8593
9482
  }
8594
9483
  let schema;
8595
9484
  try {
8596
- schema = loadSchema();
9485
+ schema = loadSchema(data);
8597
9486
  } catch (err) {
8598
9487
  process.stderr.write(`Error: could not load schema: ${err.message}
8599
9488
  `);
@@ -8621,6 +9510,7 @@ async function handleValidate(args) {
8621
9510
  process.stderr.write(` ${path2}: ${e.message}
8622
9511
  `);
8623
9512
  }
9513
+ printGuideHints(validate.errors);
8624
9514
  process.exit(1);
8625
9515
  }
8626
9516
  }
@@ -8835,11 +9725,13 @@ function formatSlim(spec2, rounds, criticalPath) {
8835
9725
  parallel: round.length > 1,
8836
9726
  tasks: round.map((id) => {
8837
9727
  const t = (spec2.tasks || []).find((task) => task.id === id) || {};
9728
+ const isDerived = t.origin === "derived";
8838
9729
  return {
8839
9730
  id: t.id,
8840
9731
  action: t.action,
8841
9732
  type: t.type,
8842
9733
  status: t.status || "pending",
9734
+ derived: isDerived,
8843
9735
  depends_on: t.depends_on || [],
8844
9736
  ...t.tool ? { tool: t.tool } : {},
8845
9737
  ...t.args ? { args: t.args } : {}
@@ -9041,7 +9933,7 @@ async function handleTask(args) {
9041
9933
  specData.history.push(entry);
9042
9934
  let schema;
9043
9935
  try {
9044
- schema = loadSchema();
9936
+ schema = loadSchema(specData);
9045
9937
  } catch (err) {
9046
9938
  process.stderr.write(`Error: could not load schema: ${err.message}
9047
9939
  `);
@@ -9058,6 +9950,7 @@ async function handleTask(args) {
9058
9950
  process.stderr.write(` ${path2}: ${e.message}
9059
9951
  `);
9060
9952
  }
9953
+ printGuideHints(validate.errors);
9061
9954
  process.exit(1);
9062
9955
  }
9063
9956
  writeState(specPath, specData);
@@ -9078,12 +9971,18 @@ async function handleStatus(args) {
9078
9971
  const inProgress = tasks.filter((t) => t.status === "in_progress");
9079
9972
  const pending = tasks.filter((t) => t.status === "pending" || !t.status);
9080
9973
  const remaining = tasks.filter((t) => t.status !== "done");
9974
+ const plannedTasks = tasks.filter((t) => t.origin !== "derived");
9975
+ const derivedTasks = tasks.filter((t) => t.origin === "derived");
9976
+ const plannedDone = plannedTasks.filter((t) => t.status === "done");
9977
+ const derivedDone = derivedTasks.filter((t) => t.status === "done");
9081
9978
  const result = {
9082
9979
  name: specData.meta?.name || "unknown",
9083
9980
  done: done.length,
9084
9981
  in_progress: inProgress.length,
9085
9982
  pending: pending.length,
9086
9983
  total: tasks.length,
9984
+ planned: { done: plannedDone.length, total: plannedTasks.length },
9985
+ derived: { done: derivedDone.length, total: derivedTasks.length },
9087
9986
  complete: remaining.length === 0,
9088
9987
  remaining: remaining.map((t) => ({ id: t.id, action: t.action, status: t.status || "pending" }))
9089
9988
  };
@@ -9137,6 +10036,28 @@ async function handleCheck(args) {
9137
10036
  }
9138
10037
  }
9139
10038
  }
10039
+ for (const task of specData.tasks) {
10040
+ if (task.origin === "derived") {
10041
+ if (!task.derived_from || !task.derived_from.parent) {
10042
+ issues.push(`task '${task.id}' has origin=derived but is missing derived_from.parent`);
10043
+ } else if (!taskIds.has(task.derived_from.parent)) {
10044
+ issues.push(`task '${task.id}' derived_from.parent '${task.derived_from.parent}' does not reference a valid task ID`);
10045
+ }
10046
+ }
10047
+ }
10048
+ const allScenarioIds = /* @__PURE__ */ new Set();
10049
+ for (const req of specData.requirements || []) {
10050
+ for (const sc of req.scenarios || []) {
10051
+ if (sc.id) allScenarioIds.add(sc.id);
10052
+ }
10053
+ }
10054
+ for (const task of specData.tasks) {
10055
+ for (const scenarioRef of task.acceptance_criteria?.scenarios || []) {
10056
+ if (!allScenarioIds.has(scenarioRef)) {
10057
+ issues.push(`task '${task.id}' acceptance_criteria.scenarios references unknown scenario '${scenarioRef}'`);
10058
+ }
10059
+ }
10060
+ }
9140
10061
  const warnings = [];
9141
10062
  const fileScopeMap = /* @__PURE__ */ new Map();
9142
10063
  for (const task of specData.tasks) {
@@ -9168,6 +10089,715 @@ async function handleCheck(args) {
9168
10089
  process.stdout.write("Spec check passed: internal consistency OK\n");
9169
10090
  process.exit(0);
9170
10091
  }
10092
+ function generateGuide(section) {
10093
+ const schema = loadSchema();
10094
+ const defs = schema.$defs || {};
10095
+ const SECTIONS = {
10096
+ meta: { ref: "meta", desc: "Spec metadata (name, goal, mode, etc.)" },
10097
+ context: { ref: "context", desc: "Request context, interview decisions, research, assumptions" },
10098
+ tasks: { ref: "task", desc: "Task DAG (work items + verification)", isArray: true },
10099
+ requirements: { ref: "requirement", desc: "Requirements with scenarios and verification", isArray: true },
10100
+ constraints: { ref: "constraint", desc: "Must-not-do / preserve constraints", isArray: true },
10101
+ history: { ref: "historyEntry", desc: "Spec change history entries", isArray: true },
10102
+ verification: { ref: "verificationSummary", desc: "A/H/S verification classification summary" },
10103
+ external: { ref: "externalDependencies", desc: "Human-only pre/post-work dependencies" },
10104
+ scenario: { ref: "scenario", desc: "Requirement scenario (given/when/then + verify)" },
10105
+ verify: { ref: null, desc: "Verify types: command, assertion, instruction", custom: "verify" },
10106
+ merge: { ref: null, desc: "Merge modes: replace (default), --append, --patch", custom: "merge" },
10107
+ "acceptance-criteria": { ref: null, desc: "v5 AC structure: scenarios[] + checks[]", custom: "acceptance-criteria" }
10108
+ };
10109
+ if (!section || section === "list") {
10110
+ const lines = ["Available guide sections:"];
10111
+ for (const [name, info2] of Object.entries(SECTIONS)) {
10112
+ lines.push(` ${name.padEnd(16)} ${info2.desc}`);
10113
+ }
10114
+ lines.push("");
10115
+ lines.push("Usage: hoyeon-cli spec guide <section>");
10116
+ lines.push(" hoyeon-cli spec guide full (all sections)");
10117
+ lines.push(" hoyeon-cli spec guide root (top-level structure)");
10118
+ return lines.join("\n");
10119
+ }
10120
+ if (section === "root") {
10121
+ return formatRoot(schema);
10122
+ }
10123
+ if (section === "full") {
10124
+ const lines = [formatRoot(schema), ""];
10125
+ for (const [name, info2] of Object.entries(SECTIONS)) {
10126
+ const def2 = defs[info2.ref];
10127
+ if (def2) {
10128
+ lines.push(`--- ${name} ---`);
10129
+ lines.push(formatDef(name, def2, defs, info2.isArray));
10130
+ lines.push("");
10131
+ }
10132
+ }
10133
+ return lines.join("\n");
10134
+ }
10135
+ const info = SECTIONS[section];
10136
+ if (!info) {
10137
+ return `Error: unknown section '${section}'. Run 'hoyeon-cli spec guide' to see available sections.`;
10138
+ }
10139
+ if (info.custom === "verify") {
10140
+ return formatVerifyGuide(defs);
10141
+ }
10142
+ if (info.custom === "merge") {
10143
+ return formatMergeGuide();
10144
+ }
10145
+ if (info.custom === "acceptance-criteria") {
10146
+ return formatAcceptanceCriteriaGuide();
10147
+ }
10148
+ const def = defs[info.ref];
10149
+ if (!def) {
10150
+ return `Error: schema definition '${info.ref}' not found.`;
10151
+ }
10152
+ return formatDef(section, def, defs, info.isArray);
10153
+ }
10154
+ function formatRoot(schema) {
10155
+ const lines = ["spec.json top-level structure:"];
10156
+ lines.push(` required: ${(schema.required || []).join(", ")}`);
10157
+ lines.push(" fields:");
10158
+ for (const [key, val] of Object.entries(schema.properties || {})) {
10159
+ if (key === "$schema") continue;
10160
+ const req = (schema.required || []).includes(key) ? "*" : " ";
10161
+ const desc = val.description || "";
10162
+ lines.push(` ${req} ${key}${desc ? ` \u2014 ${desc}` : ""}`);
10163
+ }
10164
+ lines.push("");
10165
+ lines.push(" * = required");
10166
+ return lines.join("\n");
10167
+ }
10168
+ function formatDef(name, def, defs, isArray) {
10169
+ const lines = [];
10170
+ if (isArray) {
10171
+ lines.push(`${name}: array of objects`);
10172
+ } else {
10173
+ lines.push(`${name}: object`);
10174
+ }
10175
+ const required = new Set(def.required || []);
10176
+ if (required.size > 0) {
10177
+ lines.push(` required: ${[...required].join(", ")}`);
10178
+ }
10179
+ const props = def.properties || {};
10180
+ for (const [key, prop] of Object.entries(props)) {
10181
+ const req = required.has(key) ? "*" : " ";
10182
+ const typeStr = resolveType(prop, defs, " ");
10183
+ lines.push(` ${req} ${key}: ${typeStr}`);
10184
+ }
10185
+ const example = generateExample(name, def, defs, required);
10186
+ if (example) {
10187
+ lines.push("");
10188
+ if (isArray) {
10189
+ lines.push(` example merge: --json '{"${name}":[${example}]}'`);
10190
+ } else {
10191
+ lines.push(` example merge: --json '{"${name}":${example}}'`);
10192
+ }
10193
+ }
10194
+ return lines.join("\n");
10195
+ }
10196
+ function resolveType(prop, defs, indent) {
10197
+ if (prop.$ref) {
10198
+ const refName = prop.$ref.replace("#/$defs/", "");
10199
+ const refDef = defs[refName];
10200
+ if (refDef) {
10201
+ if (refDef.enum) return `enum(${refDef.enum.join("|")})`;
10202
+ if (refDef.type === "object") return `{${refName}}`;
10203
+ return refDef.type || refName;
10204
+ }
10205
+ return refName;
10206
+ }
10207
+ if (prop.oneOf) {
10208
+ const types = prop.oneOf.map((o) => {
10209
+ if (o.$ref) return `{${o.$ref.replace("#/$defs/", "")}}`;
10210
+ if (o.type) return o.type;
10211
+ return "?";
10212
+ });
10213
+ return types.join(" | ");
10214
+ }
10215
+ if (prop.enum) return `enum(${prop.enum.join("|")})`;
10216
+ if (prop.type === "array") {
10217
+ if (prop.items) {
10218
+ if (prop.items.$ref) {
10219
+ const refName = prop.items.$ref.replace("#/$defs/", "");
10220
+ return `[{${refName}}]`;
10221
+ }
10222
+ if (prop.items.oneOf) return `[mixed]`;
10223
+ if (prop.items.type === "object" && prop.items.properties) {
10224
+ return formatInlineObject(prop.items, indent);
10225
+ }
10226
+ return `[${prop.items.type || "any"}]`;
10227
+ }
10228
+ return "[]";
10229
+ }
10230
+ if (prop.const) return `"${prop.const}"`;
10231
+ if (prop.type === "object" && prop.properties) {
10232
+ return formatInlineObject(prop, indent);
10233
+ }
10234
+ let t = prop.type || "any";
10235
+ if (prop.minimum !== void 0 || prop.maximum !== void 0) {
10236
+ const parts = [];
10237
+ if (prop.minimum !== void 0) parts.push(`min:${prop.minimum}`);
10238
+ if (prop.maximum !== void 0) parts.push(`max:${prop.maximum}`);
10239
+ t += `(${parts.join(",")})`;
10240
+ }
10241
+ return t;
10242
+ }
10243
+ function formatInlineObject(schema, indent = " ") {
10244
+ const req = new Set(schema.required || []);
10245
+ const props = schema.properties || {};
10246
+ const fields = [];
10247
+ for (const [k, v] of Object.entries(props)) {
10248
+ const r = req.has(k) ? "*" : " ";
10249
+ const t = v.enum ? `enum(${v.enum.join("|")})` : v.const ? `"${v.const}"` : v.type || "any";
10250
+ fields.push(`${indent} ${r} ${k}: ${t}`);
10251
+ }
10252
+ const isArray = schema === schema ? "" : "";
10253
+ return `[object]
10254
+ ${fields.join("\n")}`;
10255
+ }
10256
+ function generateExample(name, def, defs, required) {
10257
+ const props = def.properties || {};
10258
+ const obj = {};
10259
+ for (const key of required) {
10260
+ const prop = props[key];
10261
+ if (!prop) continue;
10262
+ obj[key] = exampleValue(key, prop, defs);
10263
+ }
10264
+ const optionals = Object.keys(props).filter((k) => !required.has(k));
10265
+ let added = 0;
10266
+ for (const key of optionals) {
10267
+ if (added >= 2) break;
10268
+ const prop = props[key];
10269
+ if (prop.type === "string" || prop.enum) {
10270
+ obj[key] = exampleValue(key, prop, defs);
10271
+ added++;
10272
+ }
10273
+ }
10274
+ try {
10275
+ return JSON.stringify(obj);
10276
+ } catch {
10277
+ return null;
10278
+ }
10279
+ }
10280
+ function exampleValue(key, prop, defs) {
10281
+ if (prop.enum) return prop.enum[0];
10282
+ if (prop.const) return prop.const;
10283
+ if (prop.$ref) {
10284
+ const refName = prop.$ref.replace("#/$defs/", "");
10285
+ const refDef = defs[refName];
10286
+ if (refDef?.enum) return refDef.enum[0];
10287
+ return `<${refName}>`;
10288
+ }
10289
+ if (prop.type === "string") return `<${key}>`;
10290
+ if (prop.type === "integer") return prop.minimum || 1;
10291
+ if (prop.type === "boolean") return false;
10292
+ if (prop.type === "array") return [];
10293
+ if (prop.type === "object") return {};
10294
+ return `<${key}>`;
10295
+ }
10296
+ function formatMergeGuide() {
10297
+ const lines = [
10298
+ "spec merge modes:",
10299
+ "",
10300
+ " (default) \u2014 replace",
10301
+ " Arrays are replaced entirely. Objects are deep-merged.",
10302
+ ` hoyeon-cli spec merge <path> --json '{"tasks":[...]}'`,
10303
+ "",
10304
+ " --append \u2014 concatenate arrays",
10305
+ " New array items are appended to existing arrays.",
10306
+ ` hoyeon-cli spec merge <path> --json '{"tasks":[{"id":"T2",...}]}' --append`,
10307
+ "",
10308
+ " --patch \u2014 ID-based merge",
10309
+ ' Array items with matching "id" are updated in place.',
10310
+ " Items with new ids are appended. Non-array fields deep-merge normally.",
10311
+ ` hoyeon-cli spec merge <path> --json '{"tasks":[{"id":"T1","status":"done"}]}' --patch`,
10312
+ "",
10313
+ " --append and --patch are mutually exclusive.",
10314
+ "",
10315
+ " When to use which:",
10316
+ " replace \u2014 rewrite a section completely (e.g. set all tasks at once)",
10317
+ " --append \u2014 add new items without touching existing (e.g. add requirements)",
10318
+ " --patch \u2014 update specific items by id (e.g. update one task's status)"
10319
+ ];
10320
+ return lines.join("\n");
10321
+ }
10322
+ function formatAcceptanceCriteriaGuide() {
10323
+ const lines = [
10324
+ "acceptance_criteria (v5): scenarios[] + checks[]",
10325
+ "",
10326
+ " scenarios: string[]",
10327
+ " List of scenario IDs from requirements[].scenarios[].id that this task fulfills.",
10328
+ " These are referential \u2014 spec check validates that each ID exists in requirements.",
10329
+ ' example: ["R1-S1", "R1-S2", "R2-S1"]',
10330
+ "",
10331
+ " checks: taskCheck[]",
10332
+ " Automated checks to run when verifying the task.",
10333
+ " Each check has:",
10334
+ " * type: enum(static|build|lint|format)",
10335
+ " * run: string (shell command)",
10336
+ ' example: [{"type":"build","run":"cd cli && node build.mjs"},{"type":"static","run":"tsc --noEmit"}]',
10337
+ "",
10338
+ " example acceptance_criteria:",
10339
+ " {",
10340
+ ' "scenarios": ["R1-S1", "R2-S1"],',
10341
+ ' "checks": [',
10342
+ ' {"type": "build", "run": "cd cli && node build.mjs"},',
10343
+ ' {"type": "lint", "run": "eslint src/"}',
10344
+ " ]",
10345
+ " }",
10346
+ "",
10347
+ " Note: spec check validates referential integrity.",
10348
+ " AC.scenarios IDs must exist in requirements[].scenarios[].id.",
10349
+ " Run: hoyeon-cli spec check <path>"
10350
+ ];
10351
+ return lines.join("\n");
10352
+ }
10353
+ function formatVerifyGuide(defs) {
10354
+ const lines = [
10355
+ "verify: oneOf \u2014 choose based on verified_by value:",
10356
+ "",
10357
+ ' verified_by: "machine" \u2192 verifyCommand',
10358
+ ' * type: "command"',
10359
+ " * run: string (shell command)",
10360
+ " * expect: { *exit_code: int, stdout_contains?: string, stderr_empty?: bool }",
10361
+ ' example: {"type":"command","run":"npm test","expect":{"exit_code":0}}',
10362
+ "",
10363
+ ' verified_by: "agent" \u2192 verifyAssertion',
10364
+ ' * type: "assertion"',
10365
+ " * checks: [string] (min 1 item)",
10366
+ ' example: {"type":"assertion","checks":["file exists at src/foo.ts"]}',
10367
+ "",
10368
+ ' verified_by: "human" \u2192 verifyInstruction',
10369
+ ' * type: "instruction"',
10370
+ " * ask: string (question for human)",
10371
+ ' example: {"type":"instruction","ask":"Does the UI look correct?"}'
10372
+ ];
10373
+ return lines.join("\n");
10374
+ }
10375
+ async function handleGuide(args) {
10376
+ const section = args[0];
10377
+ const output = generateGuide(section);
10378
+ process.stdout.write(output + "\n");
10379
+ process.exit(0);
10380
+ }
10381
+ function generateDerivedId(tasks, parentId, trigger) {
10382
+ const prefix = `${parentId}.${trigger}-`;
10383
+ let maxN = 0;
10384
+ for (const t of tasks) {
10385
+ if (t.id.startsWith(prefix)) {
10386
+ const suffix = t.id.slice(prefix.length);
10387
+ const n = parseInt(suffix, 10);
10388
+ if (!isNaN(n) && n > maxN) maxN = n;
10389
+ }
10390
+ }
10391
+ return `${prefix}${maxN + 1}`;
10392
+ }
10393
+ async function handleDerive(args) {
10394
+ const parsed = parseArgs(args);
10395
+ const requiredFlags = ["parent", "source", "trigger", "action", "reason"];
10396
+ for (const flag of requiredFlags) {
10397
+ if (!parsed[flag]) {
10398
+ process.stderr.write(`Error: --${flag} is required
10399
+ `);
10400
+ process.stderr.write("Usage: hoyeon-cli spec derive --parent <id> --source <src> --trigger <trigger> --action <action> --reason <reason> [--attempt <n>] [--file-scope <f1,f2>] [--steps <s1,s2>] <path>\n");
10401
+ process.exit(1);
10402
+ }
10403
+ }
10404
+ const validTriggers = ["adapt", "retry", "code_review", "final_verify"];
10405
+ if (!validTriggers.includes(parsed.trigger)) {
10406
+ process.stderr.write(`Error: --trigger must be one of: ${validTriggers.join(", ")}
10407
+ `);
10408
+ process.exit(1);
10409
+ }
10410
+ const filePath = parsed._[0];
10411
+ if (!filePath) {
10412
+ process.stderr.write("Error: <path> to spec.json is required\n");
10413
+ process.exit(1);
10414
+ }
10415
+ const specPath = resolve(filePath);
10416
+ const specData = loadSpec(specPath);
10417
+ const parentTask = (specData.tasks || []).find((t) => t.id === parsed.parent);
10418
+ if (!parentTask) {
10419
+ process.stderr.write(`Error: parent task '${parsed.parent}' not found in spec
10420
+ `);
10421
+ process.exit(1);
10422
+ }
10423
+ const parentOrigin = parentTask.origin || "planned";
10424
+ if (parentOrigin === "derived" || parentOrigin === "adapted") {
10425
+ process.stderr.write(`Error: Parent must be a planned task (depth-1 enforcement)
10426
+ `);
10427
+ process.exit(1);
10428
+ }
10429
+ const newId = generateDerivedId(specData.tasks || [], parsed.parent, parsed.trigger);
10430
+ const fileScope = parsed["file-scope"] ? parsed["file-scope"].split(",").map((s) => s.trim()).filter(Boolean) : void 0;
10431
+ const steps = parsed.steps ? parsed.steps.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
10432
+ const derivedFrom = {
10433
+ parent: parsed.parent,
10434
+ trigger: parsed.trigger,
10435
+ source: parsed.source,
10436
+ reason: parsed.reason
10437
+ };
10438
+ const newTask = {
10439
+ id: newId,
10440
+ action: parsed.action,
10441
+ type: "work",
10442
+ status: "pending",
10443
+ origin: "derived",
10444
+ derived_from: derivedFrom,
10445
+ depends_on: [parsed.parent]
10446
+ };
10447
+ if (fileScope) newTask.file_scope = fileScope;
10448
+ if (steps) newTask.steps = steps;
10449
+ if (!specData.tasks) specData.tasks = [];
10450
+ specData.tasks = specData.tasks.concat([newTask]);
10451
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10452
+ if (!specData.history) specData.history = [];
10453
+ specData.history.push({
10454
+ ts: now,
10455
+ type: "tasks_changed",
10456
+ task: newId,
10457
+ detail: `derived from ${parsed.parent} via ${parsed.trigger}`
10458
+ });
10459
+ if (specData.meta) specData.meta.updated_at = now;
10460
+ validateSpec(specData);
10461
+ buildPlan(specData.tasks);
10462
+ writeState(specPath, specData);
10463
+ process.stdout.write(JSON.stringify({ created: newId }) + "\n");
10464
+ process.exit(0);
10465
+ }
10466
+ async function handleDrift(args) {
10467
+ const filePath = args[0];
10468
+ if (!filePath) {
10469
+ process.stderr.write("Error: missing <path> argument\n");
10470
+ process.stderr.write("Usage: hoyeon-cli spec drift <path>\n");
10471
+ process.exit(1);
10472
+ }
10473
+ const specData = loadSpec(resolve(filePath));
10474
+ const tasks = specData.tasks || [];
10475
+ const plannedTasks = tasks.filter((t) => !t.origin || t.origin === "planned" || t.origin === "adapted");
10476
+ const derivedTasks = tasks.filter((t) => t.origin === "derived");
10477
+ const plannedCount = plannedTasks.length;
10478
+ const derivedCount = derivedTasks.length;
10479
+ const driftRatio = plannedCount === 0 ? 0 : derivedCount / plannedCount;
10480
+ const byTrigger = {};
10481
+ for (const t of derivedTasks) {
10482
+ const trigger = t.derived_from?.trigger || "unknown";
10483
+ byTrigger[trigger] = (byTrigger[trigger] || 0) + 1;
10484
+ }
10485
+ const bySource = {};
10486
+ for (const t of derivedTasks) {
10487
+ const source = t.derived_from?.source || "unknown";
10488
+ bySource[source] = (bySource[source] || 0) + 1;
10489
+ }
10490
+ const result = {
10491
+ planned: plannedCount,
10492
+ derived: derivedCount,
10493
+ drift_ratio: Math.round(driftRatio * 1e3) / 1e3,
10494
+ by_trigger: byTrigger,
10495
+ by_source: bySource
10496
+ };
10497
+ process.stdout.write(JSON.stringify(result) + "\n");
10498
+ process.exit(0);
10499
+ }
10500
+ async function handleScenario(args) {
10501
+ const scenarioId = args[0];
10502
+ if (!scenarioId || scenarioId.startsWith("--")) {
10503
+ process.stderr.write("Error: <scenario-id> is required\n");
10504
+ process.stderr.write("Usage: hoyeon-cli spec scenario <scenario-id> --get <path>\n");
10505
+ process.exit(1);
10506
+ }
10507
+ const parsed = parseArgs(args.slice(1));
10508
+ if (parsed.get === void 0) {
10509
+ process.stderr.write("Error: --get <path> is required\n");
10510
+ process.stderr.write("Usage: hoyeon-cli spec scenario <scenario-id> --get <path>\n");
10511
+ process.exit(1);
10512
+ }
10513
+ if (typeof parsed.get !== "string") {
10514
+ process.stderr.write("Error: --get requires <path> argument\n");
10515
+ process.stderr.write("Usage: hoyeon-cli spec scenario <scenario-id> --get <path>\n");
10516
+ process.exit(1);
10517
+ }
10518
+ const filePath = parsed.get;
10519
+ const specData = loadSpec(resolve(filePath));
10520
+ let found = null;
10521
+ for (const req of specData.requirements || []) {
10522
+ for (const scenario of req.scenarios || []) {
10523
+ if (scenario.id === scenarioId) {
10524
+ found = scenario;
10525
+ break;
10526
+ }
10527
+ }
10528
+ if (found) break;
10529
+ }
10530
+ if (!found) {
10531
+ process.stderr.write(`Error: scenario '${scenarioId}' not found in spec
10532
+ `);
10533
+ process.exit(1);
10534
+ }
10535
+ process.stdout.write(JSON.stringify(found, null, 2) + "\n");
10536
+ process.exit(0);
10537
+ }
10538
+ function findScenarioById(specData, scenarioId) {
10539
+ for (const req of specData.requirements || []) {
10540
+ for (const scenario of req.scenarios || []) {
10541
+ if (scenario.id === scenarioId) {
10542
+ return { scenario, requirement: req };
10543
+ }
10544
+ }
10545
+ }
10546
+ return null;
10547
+ }
10548
+ async function handleRequirement(args) {
10549
+ const parsed = parseArgs(args);
10550
+ const firstPositional = parsed._[0];
10551
+ const isStatusFlag = parsed.status === true;
10552
+ if (!firstPositional && isStatusFlag) {
10553
+ const filePath = parsed._[0] || parsed._[1];
10554
+ const resolvedPath = typeof parsed.status === "string" ? parsed.status : parsed._[0];
10555
+ if (!resolvedPath) {
10556
+ process.stderr.write("Error: <path> is required\n");
10557
+ process.stderr.write("Usage: hoyeon-cli spec requirement --status <path> [--json]\n");
10558
+ process.exit(1);
10559
+ }
10560
+ const specData = loadSpec(resolve(resolvedPath));
10561
+ const useJson = parsed.json === true;
10562
+ return handleRequirementStatusView(specData, useJson);
10563
+ }
10564
+ if (!firstPositional && typeof parsed.status === "string") {
10565
+ const resolvedPath = parsed.status;
10566
+ if (!resolvedPath) {
10567
+ process.stderr.write("Error: <path> is required\n");
10568
+ process.exit(1);
10569
+ }
10570
+ const specData = loadSpec(resolve(resolvedPath));
10571
+ const useJson = parsed.json === true;
10572
+ return handleRequirementStatusView(specData, useJson);
10573
+ }
10574
+ const scenarioId = firstPositional;
10575
+ if (!scenarioId) {
10576
+ process.stderr.write("Error: <scenario-id> or --status flag is required\n");
10577
+ process.stderr.write("Usage: hoyeon-cli spec requirement --status <path>\n");
10578
+ process.stderr.write(" hoyeon-cli spec requirement <id> --get <path>\n");
10579
+ process.stderr.write(" hoyeon-cli spec requirement <id> --status pass|fail|skipped --task <task_id> <path>\n");
10580
+ process.exit(1);
10581
+ }
10582
+ if (parsed.get !== void 0) {
10583
+ if (typeof parsed.get !== "string") {
10584
+ process.stderr.write("Error: --get requires <path> argument\n");
10585
+ process.exit(1);
10586
+ }
10587
+ const specData = loadSpec(resolve(parsed.get));
10588
+ const found = findScenarioById(specData, scenarioId);
10589
+ if (!found) {
10590
+ process.stderr.write(`Error: scenario '${scenarioId}' not found in spec
10591
+ `);
10592
+ process.exit(1);
10593
+ }
10594
+ process.stdout.write(JSON.stringify(found.scenario, null, 2) + "\n");
10595
+ process.exit(0);
10596
+ }
10597
+ const statusValue = typeof parsed.status === "string" ? parsed.status : null;
10598
+ if (statusValue) {
10599
+ const validStatuses = ["pass", "fail", "skipped"];
10600
+ if (!validStatuses.includes(statusValue)) {
10601
+ process.stderr.write(`Error: --status must be one of: ${validStatuses.join(", ")}
10602
+ `);
10603
+ process.exit(1);
10604
+ }
10605
+ if (!parsed.task) {
10606
+ process.stderr.write("Error: --task <task_id> is required when updating scenario status\n");
10607
+ process.exit(1);
10608
+ }
10609
+ const filePath = parsed._[1];
10610
+ if (!filePath) {
10611
+ process.stderr.write("Error: <path> to spec.json is required\n");
10612
+ process.exit(1);
10613
+ }
10614
+ const specPath = resolve(filePath);
10615
+ const specData = loadSpec(specPath);
10616
+ const found = findScenarioById(specData, scenarioId);
10617
+ if (!found) {
10618
+ process.stderr.write(`Error: scenario '${scenarioId}' not found in spec
10619
+ `);
10620
+ process.exit(1);
10621
+ }
10622
+ found.scenario.status = statusValue;
10623
+ found.scenario.verified_by_task = parsed.task;
10624
+ if (parsed.reason) {
10625
+ found.scenario.verification_reason = parsed.reason;
10626
+ }
10627
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10628
+ if (!specData.history) specData.history = [];
10629
+ specData.history.push({
10630
+ ts: now,
10631
+ type: "scenario_verified",
10632
+ scenario: scenarioId,
10633
+ status: statusValue,
10634
+ task: parsed.task
10635
+ });
10636
+ if (specData.meta) specData.meta.updated_at = now;
10637
+ writeState(specPath, specData);
10638
+ process.stdout.write(`Updated scenario '${scenarioId}': status=${statusValue}, verified_by_task=${parsed.task}
10639
+ `);
10640
+ process.exit(0);
10641
+ }
10642
+ process.stderr.write("Error: could not determine mode. Use --get, --status (flag), or --status <value> --task <id>\n");
10643
+ process.exit(1);
10644
+ }
10645
+ function handleRequirementStatusView(specData, useJson) {
10646
+ const requirements = specData.requirements || [];
10647
+ const requirementRows = requirements.map((req) => {
10648
+ const scenarios = (req.scenarios || []).map((sc) => {
10649
+ const verifiedBy = sc.verified_by || "unknown";
10650
+ const execEnv = sc.execution_env ? `[${sc.execution_env}]` : "";
10651
+ const status = sc.status || "pending";
10652
+ const verifiedByTask = sc.verified_by_task || null;
10653
+ return {
10654
+ id: sc.id,
10655
+ verified_by: verifiedBy,
10656
+ execution_env: sc.execution_env || null,
10657
+ status,
10658
+ verified_by_task: verifiedByTask
10659
+ };
10660
+ });
10661
+ return {
10662
+ id: req.id,
10663
+ behavior: req.behavior,
10664
+ scenarios
10665
+ };
10666
+ });
10667
+ let passCount = 0;
10668
+ let failCount = 0;
10669
+ let pendingCount = 0;
10670
+ let skippedCount = 0;
10671
+ for (const req of requirementRows) {
10672
+ for (const sc of req.scenarios) {
10673
+ if (sc.status === "pass") passCount++;
10674
+ else if (sc.status === "fail") failCount++;
10675
+ else if (sc.status === "skipped") skippedCount++;
10676
+ else pendingCount++;
10677
+ }
10678
+ }
10679
+ const summary = { pass: passCount, fail: failCount, pending: pendingCount, skipped: skippedCount };
10680
+ if (useJson) {
10681
+ process.stdout.write(JSON.stringify({ requirements: requirementRows, summary }, null, 2) + "\n");
10682
+ process.exit(0);
10683
+ }
10684
+ const lines = [];
10685
+ for (const req of requirementRows) {
10686
+ const scCount = req.scenarios.length;
10687
+ lines.push(`${req.id}: ${req.behavior} (${scCount} scenario${scCount !== 1 ? "s" : ""})`);
10688
+ for (const sc of req.scenarios) {
10689
+ const verifiedByLabel = sc.verified_by === "human" ? "Manual" : sc.verified_by === "agent" ? `Agent ${sc.execution_env ? `[${sc.execution_env}]` : ""}`.trim() : sc.verified_by === "machine" ? `Auto ${sc.execution_env ? `[${sc.execution_env}]` : ""}`.trim() : sc.verified_by;
10690
+ const taskLabel = sc.verified_by_task ? ` (${sc.verified_by_task})` : "";
10691
+ lines.push(` ${sc.id}: ${verifiedByLabel.padEnd(16)} ${sc.status}${taskLabel}`);
10692
+ }
10693
+ lines.push("");
10694
+ }
10695
+ const summaryParts = [];
10696
+ if (passCount > 0) summaryParts.push(`${passCount} pass`);
10697
+ if (failCount > 0) summaryParts.push(`${failCount} fail`);
10698
+ if (pendingCount > 0) summaryParts.push(`${pendingCount} pending`);
10699
+ if (skippedCount > 0) summaryParts.push(`${skippedCount} skipped`);
10700
+ lines.push(`Summary: ${summaryParts.join(", ") || "no scenarios"}`);
10701
+ process.stdout.write(lines.join("\n") + "\n");
10702
+ process.exit(0);
10703
+ }
10704
+ async function handleSandboxTasks(args) {
10705
+ const parsed = parseArgs(args);
10706
+ const filePath = parsed._[0];
10707
+ if (!filePath) {
10708
+ process.stderr.write("Error: <path> is required\n");
10709
+ process.stderr.write("Usage: hoyeon-cli spec sandbox-tasks <path> [--json]\n");
10710
+ process.exit(1);
10711
+ }
10712
+ const specPath = resolve(filePath);
10713
+ const specData = loadSpec(specPath);
10714
+ const useJson = parsed.json === true;
10715
+ const sandboxScenarios = [];
10716
+ for (const req of specData.requirements || []) {
10717
+ for (const sc of req.scenarios || []) {
10718
+ if (sc.execution_env === "sandbox") {
10719
+ sandboxScenarios.push({ ...sc, requirement_id: req.id });
10720
+ }
10721
+ }
10722
+ }
10723
+ if (sandboxScenarios.length === 0) {
10724
+ if (useJson) {
10725
+ process.stdout.write(JSON.stringify({ sandbox_scenarios: [], created_tasks: [] }, null, 2) + "\n");
10726
+ } else {
10727
+ process.stdout.write("No sandbox scenarios found. Nothing to do.\n");
10728
+ }
10729
+ process.exit(0);
10730
+ }
10731
+ const existingTasks = specData.tasks || [];
10732
+ const existingTaskIds = new Set(existingTasks.map((t) => t.id));
10733
+ const sandboxScenarioIds = new Set(sandboxScenarios.map((sc) => sc.id));
10734
+ const workTasksReferencingSandbox = existingTasks.filter((t) => {
10735
+ const acScenarios = t.acceptance_criteria?.scenarios || [];
10736
+ return acScenarios.some((sid) => sandboxScenarioIds.has(sid));
10737
+ });
10738
+ const createdTasks = [];
10739
+ if (!existingTaskIds.has("T_SANDBOX")) {
10740
+ const sandboxInfraTask = {
10741
+ id: "T_SANDBOX",
10742
+ action: "Prepare sandbox environment for scenario verification",
10743
+ type: "work",
10744
+ status: "pending",
10745
+ depends_on: workTasksReferencingSandbox.map((t) => t.id)
10746
+ };
10747
+ existingTasks.push(sandboxInfraTask);
10748
+ existingTaskIds.add("T_SANDBOX");
10749
+ createdTasks.push(sandboxInfraTask);
10750
+ }
10751
+ let svCounter = 1;
10752
+ for (const t of existingTasks) {
10753
+ const m = t.id.match(/^T_SV(\d+)$/);
10754
+ if (m) {
10755
+ const n = parseInt(m[1], 10);
10756
+ if (n >= svCounter) svCounter = n + 1;
10757
+ }
10758
+ }
10759
+ const newSvTasks = [];
10760
+ for (const sc of sandboxScenarios) {
10761
+ const svId = `T_SV${svCounter++}`;
10762
+ if (!existingTaskIds.has(svId)) {
10763
+ const svTask = {
10764
+ id: svId,
10765
+ action: `Verify ${sc.id}: ${sc.then}`,
10766
+ type: "work",
10767
+ status: "pending",
10768
+ depends_on: ["T_SANDBOX"]
10769
+ };
10770
+ existingTasks.push(svTask);
10771
+ existingTaskIds.add(svId);
10772
+ createdTasks.push(svTask);
10773
+ newSvTasks.push(svTask);
10774
+ }
10775
+ }
10776
+ specData.tasks = existingTasks;
10777
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10778
+ if (!specData.history) specData.history = [];
10779
+ specData.history.push({
10780
+ ts: now,
10781
+ type: "tasks_changed",
10782
+ detail: `sandbox-tasks: created ${createdTasks.map((t) => t.id).join(", ")}`
10783
+ });
10784
+ if (specData.meta) specData.meta.updated_at = now;
10785
+ writeState(specPath, specData);
10786
+ if (useJson) {
10787
+ process.stdout.write(JSON.stringify({
10788
+ sandbox_scenarios: sandboxScenarios.map((sc) => sc.id),
10789
+ created_tasks: createdTasks.map((t) => t.id)
10790
+ }, null, 2) + "\n");
10791
+ } else {
10792
+ process.stdout.write(`Created ${createdTasks.length} task(s):
10793
+ `);
10794
+ for (const t of createdTasks) {
10795
+ process.stdout.write(` ${t.id}: ${t.action}
10796
+ `);
10797
+ }
10798
+ }
10799
+ process.exit(0);
10800
+ }
9171
10801
  async function spec(args) {
9172
10802
  const subcommand = args[0];
9173
10803
  if (!subcommand || subcommand === "--help" || subcommand === "-h") {
@@ -9192,6 +10822,18 @@ async function spec(args) {
9192
10822
  await handleCheck(args.slice(1));
9193
10823
  } else if (subcommand === "amend") {
9194
10824
  await handleAmend(args.slice(1));
10825
+ } else if (subcommand === "guide") {
10826
+ await handleGuide(args.slice(1));
10827
+ } else if (subcommand === "scenario") {
10828
+ await handleScenario(args.slice(1));
10829
+ } else if (subcommand === "derive") {
10830
+ await handleDerive(args.slice(1));
10831
+ } else if (subcommand === "drift") {
10832
+ await handleDrift(args.slice(1));
10833
+ } else if (subcommand === "requirement") {
10834
+ await handleRequirement(args.slice(1));
10835
+ } else if (subcommand === "sandbox-tasks") {
10836
+ await handleSandboxTasks(args.slice(1));
9195
10837
  } else {
9196
10838
  process.stderr.write(`Error: unknown spec subcommand '${subcommand}'
9197
10839
  `);
@@ -10006,7 +11648,7 @@ async function main() {
10006
11648
  process.exit(0);
10007
11649
  }
10008
11650
  if (args[0] === "--version") {
10009
- const version = true ? "0.11.0" : "dev";
11651
+ const version = true ? "1.0.1" : "dev";
10010
11652
  process.stdout.write(`hoyeon-cli v${version}
10011
11653
  `);
10012
11654
  process.exit(0);