@team-attention/hoyeon-cli 0.9.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1678 -35
- package/package.json +1 -1
- package/schemas/dev-spec-v4.schema.json +56 -3
- package/schemas/dev-spec-v5.schema.json +772 -0
package/dist/cli.js
CHANGED
|
@@ -6351,14 +6351,14 @@ var require_format = __commonJS({
|
|
|
6351
6351
|
}
|
|
6352
6352
|
}
|
|
6353
6353
|
function validateFormat() {
|
|
6354
|
-
const
|
|
6355
|
-
if (!
|
|
6354
|
+
const formatDef2 = self.formats[schema];
|
|
6355
|
+
if (!formatDef2) {
|
|
6356
6356
|
unknownFormat();
|
|
6357
6357
|
return;
|
|
6358
6358
|
}
|
|
6359
|
-
if (
|
|
6359
|
+
if (formatDef2 === true)
|
|
6360
6360
|
return;
|
|
6361
|
-
const [fmtType, format, fmtRef] = getFormat(
|
|
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
|
|
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})`;
|
|
@@ -7836,7 +7836,17 @@ var dev_spec_v4_schema_default = {
|
|
|
7836
7836
|
created_at: { type: "string" },
|
|
7837
7837
|
updated_at: { type: "string" },
|
|
7838
7838
|
approved_by: { type: "string" },
|
|
7839
|
-
approved_at: { type: "string" }
|
|
7839
|
+
approved_at: { type: "string" },
|
|
7840
|
+
type: {
|
|
7841
|
+
type: "string",
|
|
7842
|
+
enum: ["dev", "plain"],
|
|
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."
|
|
7849
|
+
}
|
|
7840
7850
|
}
|
|
7841
7851
|
},
|
|
7842
7852
|
context: {
|
|
@@ -7953,6 +7963,31 @@ var dev_spec_v4_schema_default = {
|
|
|
7953
7963
|
}
|
|
7954
7964
|
}
|
|
7955
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
|
+
},
|
|
7956
7991
|
task: {
|
|
7957
7992
|
type: "object",
|
|
7958
7993
|
required: ["id", "action", "type"],
|
|
@@ -7964,6 +7999,16 @@ var dev_spec_v4_schema_default = {
|
|
|
7964
7999
|
type: "string",
|
|
7965
8000
|
enum: ["work", "verification"]
|
|
7966
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
|
+
},
|
|
7967
8012
|
risk: {
|
|
7968
8013
|
type: "string",
|
|
7969
8014
|
enum: ["low", "medium", "high"]
|
|
@@ -8052,8 +8097,16 @@ var dev_spec_v4_schema_default = {
|
|
|
8052
8097
|
type: "array",
|
|
8053
8098
|
items: { type: "string" }
|
|
8054
8099
|
},
|
|
8055
|
-
acceptance_criteria: { $ref: "#/$defs/taskAcceptanceCriteria" }
|
|
8056
|
-
|
|
8100
|
+
acceptance_criteria: { $ref: "#/$defs/taskAcceptanceCriteria" },
|
|
8101
|
+
tool: { type: "string" },
|
|
8102
|
+
args: { type: "string" }
|
|
8103
|
+
},
|
|
8104
|
+
allOf: [
|
|
8105
|
+
{
|
|
8106
|
+
if: { properties: { origin: { enum: ["derived", "adapted"] } }, required: ["origin"] },
|
|
8107
|
+
then: { required: ["derived_from"] }
|
|
8108
|
+
}
|
|
8109
|
+
]
|
|
8057
8110
|
},
|
|
8058
8111
|
historyEntry: {
|
|
8059
8112
|
type: "object",
|
|
@@ -8087,22 +8140,796 @@ var dev_spec_v4_schema_default = {
|
|
|
8087
8140
|
type: "object",
|
|
8088
8141
|
additionalProperties: false,
|
|
8089
8142
|
properties: {
|
|
8090
|
-
functional: {
|
|
8091
|
-
type: "array",
|
|
8092
|
-
items: { $ref: "#/$defs/acceptanceCriterionItem" }
|
|
8093
|
-
},
|
|
8094
|
-
static: {
|
|
8095
|
-
type: "array",
|
|
8096
|
-
items: { $ref: "#/$defs/acceptanceCriterionItem" }
|
|
8097
|
-
},
|
|
8098
|
-
runtime: {
|
|
8099
|
-
type: "array",
|
|
8100
|
-
items: { $ref: "#/$defs/acceptanceCriterionItem" }
|
|
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
|
+
}
|
|
8726
|
+
},
|
|
8727
|
+
checkpoint: {
|
|
8728
|
+
type: "object",
|
|
8729
|
+
required: ["enabled"],
|
|
8730
|
+
additionalProperties: false,
|
|
8731
|
+
properties: {
|
|
8732
|
+
enabled: { type: "boolean" },
|
|
8733
|
+
message: { type: "string" },
|
|
8734
|
+
condition: {
|
|
8735
|
+
type: "string",
|
|
8736
|
+
enum: ["always", "on_fulfill", "manual"]
|
|
8737
|
+
}
|
|
8738
|
+
}
|
|
8739
|
+
},
|
|
8740
|
+
derivedFrom: {
|
|
8741
|
+
type: "object",
|
|
8742
|
+
required: ["parent", "trigger"],
|
|
8743
|
+
additionalProperties: false,
|
|
8744
|
+
description: "Provenance record linking a derived task back to its planned parent",
|
|
8745
|
+
properties: {
|
|
8746
|
+
parent: {
|
|
8747
|
+
type: "string",
|
|
8748
|
+
description: "ID of the planned parent task (depth-1 only)"
|
|
8749
|
+
},
|
|
8750
|
+
source: {
|
|
8751
|
+
type: "string",
|
|
8752
|
+
description: "Source context that triggered the derivation (e.g. file path, agent name)"
|
|
8753
|
+
},
|
|
8754
|
+
trigger: {
|
|
8755
|
+
type: "string",
|
|
8756
|
+
enum: ["adapt", "retry", "code_review", "final_verify"],
|
|
8757
|
+
description: "Event that caused this task to be derived"
|
|
8758
|
+
},
|
|
8759
|
+
reason: {
|
|
8760
|
+
type: "string",
|
|
8761
|
+
description: "Human-readable explanation for the derivation"
|
|
8762
|
+
}
|
|
8763
|
+
}
|
|
8764
|
+
},
|
|
8765
|
+
taskCheck: {
|
|
8766
|
+
type: "object",
|
|
8767
|
+
required: ["type", "run"],
|
|
8768
|
+
additionalProperties: false,
|
|
8769
|
+
description: "A static/build/lint/format check to run as part of task acceptance",
|
|
8770
|
+
properties: {
|
|
8771
|
+
type: {
|
|
8772
|
+
type: "string",
|
|
8773
|
+
enum: ["static", "build", "lint", "format"],
|
|
8774
|
+
description: "Category of check: static=type check, build=compilation, lint=linting, format=formatting"
|
|
8775
|
+
},
|
|
8776
|
+
run: {
|
|
8777
|
+
type: "string",
|
|
8778
|
+
description: "Shell command to execute for this check"
|
|
8779
|
+
}
|
|
8780
|
+
}
|
|
8781
|
+
},
|
|
8782
|
+
taskAcceptanceCriteria: {
|
|
8783
|
+
type: "object",
|
|
8784
|
+
required: ["scenarios", "checks"],
|
|
8785
|
+
additionalProperties: false,
|
|
8786
|
+
description: "v5: AC is expressed as scenario references + automated checks. scenarios[] references requirements[].scenarios[].id. checks[] are runnable static/build/lint/format commands.",
|
|
8787
|
+
properties: {
|
|
8788
|
+
scenarios: {
|
|
8789
|
+
type: "array",
|
|
8790
|
+
description: "List of scenario IDs from requirements[].scenarios[].id that this task fulfills",
|
|
8791
|
+
items: { type: "string" }
|
|
8792
|
+
},
|
|
8793
|
+
checks: {
|
|
8794
|
+
type: "array",
|
|
8795
|
+
description: "Automated checks to verify the task (static, build, lint, format)",
|
|
8796
|
+
items: { $ref: "#/$defs/taskCheck" }
|
|
8797
|
+
}
|
|
8798
|
+
}
|
|
8799
|
+
},
|
|
8800
|
+
task: {
|
|
8801
|
+
type: "object",
|
|
8802
|
+
required: ["id", "action", "type"],
|
|
8803
|
+
additionalProperties: false,
|
|
8804
|
+
properties: {
|
|
8805
|
+
id: { type: "string" },
|
|
8806
|
+
action: { type: "string" },
|
|
8807
|
+
type: {
|
|
8808
|
+
type: "string",
|
|
8809
|
+
enum: ["work", "verification"]
|
|
8810
|
+
},
|
|
8811
|
+
origin: {
|
|
8812
|
+
type: "string",
|
|
8813
|
+
enum: ["planned", "derived", "adapted"],
|
|
8814
|
+
default: "planned",
|
|
8815
|
+
description: "How this task was created: planned=authored in spec, derived=created at runtime via spec derive, adapted=manually adjusted from a derived task"
|
|
8816
|
+
},
|
|
8817
|
+
derived_from: {
|
|
8818
|
+
$ref: "#/$defs/derivedFrom",
|
|
8819
|
+
description: "Provenance record \u2014 required when origin is derived or adapted"
|
|
8820
|
+
},
|
|
8821
|
+
risk: {
|
|
8822
|
+
type: "string",
|
|
8823
|
+
enum: ["low", "medium", "high"]
|
|
8824
|
+
},
|
|
8825
|
+
file_scope: {
|
|
8826
|
+
type: "array",
|
|
8827
|
+
items: { type: "string" }
|
|
8828
|
+
},
|
|
8829
|
+
fulfills: {
|
|
8830
|
+
type: "array",
|
|
8831
|
+
items: { type: "string" }
|
|
8832
|
+
},
|
|
8833
|
+
depends_on: {
|
|
8834
|
+
type: "array",
|
|
8835
|
+
items: { type: "string" }
|
|
8836
|
+
},
|
|
8837
|
+
steps: {
|
|
8838
|
+
type: "array",
|
|
8839
|
+
description: "Task steps \u2014 either plain strings (backward compat) or structured objects with done tracking",
|
|
8840
|
+
items: {
|
|
8841
|
+
oneOf: [
|
|
8842
|
+
{ type: "string" },
|
|
8843
|
+
{
|
|
8844
|
+
type: "object",
|
|
8845
|
+
required: ["text"],
|
|
8846
|
+
additionalProperties: false,
|
|
8847
|
+
properties: {
|
|
8848
|
+
text: { type: "string" },
|
|
8849
|
+
done: { type: "boolean", default: false }
|
|
8850
|
+
}
|
|
8851
|
+
}
|
|
8852
|
+
]
|
|
8853
|
+
}
|
|
8854
|
+
},
|
|
8855
|
+
references: {
|
|
8856
|
+
type: "array",
|
|
8857
|
+
items: { $ref: "#/$defs/reference" }
|
|
8858
|
+
},
|
|
8859
|
+
inputs: {
|
|
8860
|
+
type: "array",
|
|
8861
|
+
items: {
|
|
8862
|
+
type: "object",
|
|
8863
|
+
required: ["from_task", "artifact"],
|
|
8864
|
+
additionalProperties: false,
|
|
8865
|
+
properties: {
|
|
8866
|
+
from_task: { type: "string" },
|
|
8867
|
+
artifact: { type: "string" }
|
|
8868
|
+
}
|
|
8869
|
+
}
|
|
8870
|
+
},
|
|
8871
|
+
outputs: {
|
|
8872
|
+
type: "array",
|
|
8873
|
+
items: {
|
|
8874
|
+
type: "object",
|
|
8875
|
+
required: ["id", "path"],
|
|
8876
|
+
additionalProperties: false,
|
|
8877
|
+
properties: {
|
|
8878
|
+
id: { type: "string" },
|
|
8879
|
+
path: { type: "string" }
|
|
8880
|
+
}
|
|
8881
|
+
}
|
|
8882
|
+
},
|
|
8883
|
+
task_constraints: {
|
|
8884
|
+
type: "array",
|
|
8885
|
+
items: { $ref: "#/$defs/taskConstraint" }
|
|
8886
|
+
},
|
|
8887
|
+
checkpoint: {
|
|
8888
|
+
oneOf: [
|
|
8889
|
+
{ $ref: "#/$defs/checkpoint" },
|
|
8890
|
+
{ type: "null" }
|
|
8891
|
+
]
|
|
8892
|
+
},
|
|
8893
|
+
status: {
|
|
8894
|
+
type: "string",
|
|
8895
|
+
enum: ["pending", "in_progress", "done"],
|
|
8896
|
+
default: "pending"
|
|
8897
|
+
},
|
|
8898
|
+
started_at: { type: "string" },
|
|
8899
|
+
completed_at: { type: "string" },
|
|
8900
|
+
summary: { type: "string" },
|
|
8901
|
+
required_tools: {
|
|
8902
|
+
type: "array",
|
|
8903
|
+
items: { type: "string" }
|
|
8904
|
+
},
|
|
8905
|
+
must_not_do: {
|
|
8906
|
+
type: "array",
|
|
8907
|
+
items: { type: "string" }
|
|
8908
|
+
},
|
|
8909
|
+
acceptance_criteria: { $ref: "#/$defs/taskAcceptanceCriteria" },
|
|
8910
|
+
tool: { type: "string" },
|
|
8911
|
+
args: { type: "string" }
|
|
8912
|
+
},
|
|
8913
|
+
allOf: [
|
|
8914
|
+
{
|
|
8915
|
+
if: { properties: { origin: { enum: ["derived", "adapted"] } }, required: ["origin"] },
|
|
8916
|
+
then: { required: ["derived_from"] }
|
|
8917
|
+
}
|
|
8918
|
+
]
|
|
8919
|
+
},
|
|
8920
|
+
historyEntry: {
|
|
8921
|
+
type: "object",
|
|
8922
|
+
required: ["ts", "type"],
|
|
8923
|
+
additionalProperties: false,
|
|
8924
|
+
properties: {
|
|
8925
|
+
ts: { type: "string" },
|
|
8926
|
+
type: {
|
|
8927
|
+
type: "string",
|
|
8928
|
+
enum: ["spec_created", "task_start", "task_done", "tasks_changed", "spec_updated"]
|
|
8101
8929
|
},
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
}
|
|
8930
|
+
task: { type: "string" },
|
|
8931
|
+
summary: { type: "string" },
|
|
8932
|
+
detail: { type: "string" }
|
|
8106
8933
|
}
|
|
8107
8934
|
},
|
|
8108
8935
|
verificationSummary: {
|
|
@@ -8364,6 +9191,8 @@ var SPEC_HELP = `
|
|
|
8364
9191
|
Usage:
|
|
8365
9192
|
hoyeon-cli spec init <name> --goal "..." <path> Create a minimal valid spec.json
|
|
8366
9193
|
hoyeon-cli spec merge <path> --json '{...}' Deep-merge a JSON fragment into spec.json
|
|
9194
|
+
--append: concatenate arrays
|
|
9195
|
+
--patch: ID-based merge (match by id, update in place)
|
|
8367
9196
|
hoyeon-cli spec validate <path> Validate a spec.json file against the schema
|
|
8368
9197
|
hoyeon-cli spec plan <path> [--format text|mermaid|json] Show execution plan with parallel groups
|
|
8369
9198
|
hoyeon-cli spec task <task-id> --status <status> [--summary "..."] <path> Update task status
|
|
@@ -8372,6 +9201,14 @@ Usage:
|
|
|
8372
9201
|
hoyeon-cli spec meta <path> Show spec meta (name, goal, non_goals, mode, etc.)
|
|
8373
9202
|
hoyeon-cli spec check <path> Check internal consistency
|
|
8374
9203
|
hoyeon-cli spec amend --reason <feedback-id> --spec <path> Amend spec.json based on feedback
|
|
9204
|
+
hoyeon-cli spec guide [section] Show schema guide for a section
|
|
9205
|
+
hoyeon-cli spec scenario <scenario-id> --get <path> Get scenario details as JSON
|
|
9206
|
+
hoyeon-cli spec derive --parent <id> --source <src> --trigger <t> --action <a> --reason <r> <path> Create a derived task
|
|
9207
|
+
hoyeon-cli spec drift <path> Show drift ratio (derived vs planned tasks)
|
|
9208
|
+
hoyeon-cli spec requirement --status <path> [--json] Show all requirements/scenarios with verification status
|
|
9209
|
+
hoyeon-cli spec requirement <id> --get <path> Get individual scenario as JSON
|
|
9210
|
+
hoyeon-cli spec requirement <id> --status pass|fail|skipped --task <task_id> [--reason <msg>] <path> Update scenario status
|
|
9211
|
+
hoyeon-cli spec sandbox-tasks <path> [--json] Auto-generate T_SANDBOX + T_SV tasks for sandbox scenarios
|
|
8375
9212
|
|
|
8376
9213
|
Options:
|
|
8377
9214
|
--help, -h Show this help message
|
|
@@ -8387,14 +9224,36 @@ Examples:
|
|
|
8387
9224
|
hoyeon-cli spec meta ./spec.json
|
|
8388
9225
|
hoyeon-cli spec check ./spec.json
|
|
8389
9226
|
hoyeon-cli spec amend --reason fb-001 --spec ./spec.json
|
|
9227
|
+
hoyeon-cli spec requirement --status ./spec.json
|
|
9228
|
+
hoyeon-cli spec requirement R1-S1 --get ./spec.json
|
|
9229
|
+
hoyeon-cli spec requirement R1-S1 --status pass --task T1 ./spec.json
|
|
9230
|
+
hoyeon-cli spec sandbox-tasks ./spec.json
|
|
8390
9231
|
`;
|
|
8391
|
-
function loadSchema() {
|
|
8392
|
-
|
|
9232
|
+
function loadSchema(specData) {
|
|
9233
|
+
if (specData?.meta?.schema_version === "v4") {
|
|
9234
|
+
return dev_spec_v4_schema_default;
|
|
9235
|
+
}
|
|
9236
|
+
return dev_spec_v5_schema_default;
|
|
9237
|
+
}
|
|
9238
|
+
function printGuideHints(errors) {
|
|
9239
|
+
const sections = /* @__PURE__ */ new Set();
|
|
9240
|
+
for (const e of errors) {
|
|
9241
|
+
const path2 = e.instancePath || "";
|
|
9242
|
+
const match = path2.match(/^\/([^/]+)/);
|
|
9243
|
+
if (match) sections.add(match[1]);
|
|
9244
|
+
}
|
|
9245
|
+
if (sections.size > 0) {
|
|
9246
|
+
process.stderr.write("\nHint: check schema with:\n");
|
|
9247
|
+
for (const s of sections) {
|
|
9248
|
+
process.stderr.write(` hoyeon-cli spec guide ${s}
|
|
9249
|
+
`);
|
|
9250
|
+
}
|
|
9251
|
+
}
|
|
8393
9252
|
}
|
|
8394
9253
|
function validateSpec(specData) {
|
|
8395
9254
|
let schema;
|
|
8396
9255
|
try {
|
|
8397
|
-
schema = loadSchema();
|
|
9256
|
+
schema = loadSchema(specData);
|
|
8398
9257
|
} catch (err) {
|
|
8399
9258
|
process.stderr.write(`Error: could not load schema: ${err.message}
|
|
8400
9259
|
`);
|
|
@@ -8411,16 +9270,30 @@ function validateSpec(specData) {
|
|
|
8411
9270
|
process.stderr.write(` ${path2}: ${e.message}
|
|
8412
9271
|
`);
|
|
8413
9272
|
}
|
|
9273
|
+
printGuideHints(validate.errors);
|
|
8414
9274
|
process.exit(1);
|
|
8415
9275
|
}
|
|
8416
9276
|
}
|
|
8417
|
-
function deepMerge(target, source, append = false) {
|
|
9277
|
+
function deepMerge(target, source, append = false, patch = false) {
|
|
8418
9278
|
for (const key of Object.keys(source)) {
|
|
8419
9279
|
if (source[key] === null || source[key] === void 0) {
|
|
8420
9280
|
continue;
|
|
8421
9281
|
}
|
|
8422
9282
|
if (Array.isArray(source[key])) {
|
|
8423
|
-
if (
|
|
9283
|
+
if (patch && Array.isArray(target[key])) {
|
|
9284
|
+
for (const item of source[key]) {
|
|
9285
|
+
if (item && typeof item === "object" && item.id) {
|
|
9286
|
+
const idx = target[key].findIndex((t) => t && t.id === item.id);
|
|
9287
|
+
if (idx >= 0) {
|
|
9288
|
+
target[key][idx] = { ...target[key][idx], ...item };
|
|
9289
|
+
} else {
|
|
9290
|
+
target[key].push(item);
|
|
9291
|
+
}
|
|
9292
|
+
} else {
|
|
9293
|
+
target[key].push(item);
|
|
9294
|
+
}
|
|
9295
|
+
}
|
|
9296
|
+
} else if (append && Array.isArray(target[key])) {
|
|
8424
9297
|
target[key] = target[key].concat(source[key]);
|
|
8425
9298
|
} else {
|
|
8426
9299
|
target[key] = source[key];
|
|
@@ -8429,7 +9302,7 @@ function deepMerge(target, source, append = false) {
|
|
|
8429
9302
|
if (!target[key] || typeof target[key] !== "object" || Array.isArray(target[key])) {
|
|
8430
9303
|
target[key] = {};
|
|
8431
9304
|
}
|
|
8432
|
-
deepMerge(target[key], source[key], append);
|
|
9305
|
+
deepMerge(target[key], source[key], append, patch);
|
|
8433
9306
|
} else {
|
|
8434
9307
|
target[key] = source[key];
|
|
8435
9308
|
}
|
|
@@ -8483,6 +9356,15 @@ async function handleInit(args) {
|
|
|
8483
9356
|
{ ts: now, type: "spec_created" }
|
|
8484
9357
|
]
|
|
8485
9358
|
};
|
|
9359
|
+
if (parsed.type !== void 0) {
|
|
9360
|
+
const validTypes = ["dev", "plain"];
|
|
9361
|
+
if (!validTypes.includes(parsed.type)) {
|
|
9362
|
+
process.stderr.write(`Error: invalid --type '${parsed.type}'. Valid values: ${validTypes.join(", ")}
|
|
9363
|
+
`);
|
|
9364
|
+
process.exit(1);
|
|
9365
|
+
}
|
|
9366
|
+
specData.meta.type = parsed.type;
|
|
9367
|
+
}
|
|
8486
9368
|
if (parsed.depth || parsed.interaction) {
|
|
8487
9369
|
specData.meta.mode = {};
|
|
8488
9370
|
if (parsed.depth) specData.meta.mode.depth = parsed.depth;
|
|
@@ -8512,7 +9394,7 @@ async function handleMerge(args) {
|
|
|
8512
9394
|
}
|
|
8513
9395
|
if (!parsed.json) {
|
|
8514
9396
|
process.stderr.write("Error: --json '{...}' is required\n");
|
|
8515
|
-
process.stderr.write("Usage: hoyeon-cli spec merge <path> --json '{...}' [--append]\n");
|
|
9397
|
+
process.stderr.write("Usage: hoyeon-cli spec merge <path> --json '{...}' [--append] [--patch]\n");
|
|
8516
9398
|
process.exit(1);
|
|
8517
9399
|
}
|
|
8518
9400
|
let fragment;
|
|
@@ -8530,7 +9412,12 @@ async function handleMerge(args) {
|
|
|
8530
9412
|
const specPath = resolve(filePath);
|
|
8531
9413
|
const specData = loadSpec(specPath);
|
|
8532
9414
|
const append = parsed.append === true;
|
|
8533
|
-
|
|
9415
|
+
const patch = parsed.patch === true;
|
|
9416
|
+
if (append && patch) {
|
|
9417
|
+
process.stderr.write("Error: --append and --patch are mutually exclusive\n");
|
|
9418
|
+
process.exit(1);
|
|
9419
|
+
}
|
|
9420
|
+
deepMerge(specData, fragment, append, patch);
|
|
8534
9421
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8535
9422
|
if (!specData.history) specData.history = [];
|
|
8536
9423
|
const mergedKeys = Object.keys(fragment).join(", ");
|
|
@@ -8549,6 +9436,7 @@ async function handleMerge(args) {
|
|
|
8549
9436
|
process.stdout.write(` merged keys: ${mergedKeys}
|
|
8550
9437
|
`);
|
|
8551
9438
|
if (append) process.stdout.write(" mode: append (arrays concatenated)\n");
|
|
9439
|
+
if (patch) process.stdout.write(" mode: patch (ID-based merge)\n");
|
|
8552
9440
|
process.exit(0);
|
|
8553
9441
|
}
|
|
8554
9442
|
async function handleValidate(args) {
|
|
@@ -8577,7 +9465,7 @@ async function handleValidate(args) {
|
|
|
8577
9465
|
}
|
|
8578
9466
|
let schema;
|
|
8579
9467
|
try {
|
|
8580
|
-
schema = loadSchema();
|
|
9468
|
+
schema = loadSchema(data);
|
|
8581
9469
|
} catch (err) {
|
|
8582
9470
|
process.stderr.write(`Error: could not load schema: ${err.message}
|
|
8583
9471
|
`);
|
|
@@ -8605,6 +9493,7 @@ async function handleValidate(args) {
|
|
|
8605
9493
|
process.stderr.write(` ${path2}: ${e.message}
|
|
8606
9494
|
`);
|
|
8607
9495
|
}
|
|
9496
|
+
printGuideHints(validate.errors);
|
|
8608
9497
|
process.exit(1);
|
|
8609
9498
|
}
|
|
8610
9499
|
}
|
|
@@ -8819,12 +9708,16 @@ function formatSlim(spec2, rounds, criticalPath) {
|
|
|
8819
9708
|
parallel: round.length > 1,
|
|
8820
9709
|
tasks: round.map((id) => {
|
|
8821
9710
|
const t = (spec2.tasks || []).find((task) => task.id === id) || {};
|
|
9711
|
+
const isDerived = t.origin === "derived";
|
|
8822
9712
|
return {
|
|
8823
9713
|
id: t.id,
|
|
8824
9714
|
action: t.action,
|
|
8825
9715
|
type: t.type,
|
|
8826
9716
|
status: t.status || "pending",
|
|
8827
|
-
|
|
9717
|
+
derived: isDerived,
|
|
9718
|
+
depends_on: t.depends_on || [],
|
|
9719
|
+
...t.tool ? { tool: t.tool } : {},
|
|
9720
|
+
...t.args ? { args: t.args } : {}
|
|
8828
9721
|
};
|
|
8829
9722
|
})
|
|
8830
9723
|
}))
|
|
@@ -9023,7 +9916,7 @@ async function handleTask(args) {
|
|
|
9023
9916
|
specData.history.push(entry);
|
|
9024
9917
|
let schema;
|
|
9025
9918
|
try {
|
|
9026
|
-
schema = loadSchema();
|
|
9919
|
+
schema = loadSchema(specData);
|
|
9027
9920
|
} catch (err) {
|
|
9028
9921
|
process.stderr.write(`Error: could not load schema: ${err.message}
|
|
9029
9922
|
`);
|
|
@@ -9040,6 +9933,7 @@ async function handleTask(args) {
|
|
|
9040
9933
|
process.stderr.write(` ${path2}: ${e.message}
|
|
9041
9934
|
`);
|
|
9042
9935
|
}
|
|
9936
|
+
printGuideHints(validate.errors);
|
|
9043
9937
|
process.exit(1);
|
|
9044
9938
|
}
|
|
9045
9939
|
writeState(specPath, specData);
|
|
@@ -9060,12 +9954,18 @@ async function handleStatus(args) {
|
|
|
9060
9954
|
const inProgress = tasks.filter((t) => t.status === "in_progress");
|
|
9061
9955
|
const pending = tasks.filter((t) => t.status === "pending" || !t.status);
|
|
9062
9956
|
const remaining = tasks.filter((t) => t.status !== "done");
|
|
9957
|
+
const plannedTasks = tasks.filter((t) => t.origin !== "derived");
|
|
9958
|
+
const derivedTasks = tasks.filter((t) => t.origin === "derived");
|
|
9959
|
+
const plannedDone = plannedTasks.filter((t) => t.status === "done");
|
|
9960
|
+
const derivedDone = derivedTasks.filter((t) => t.status === "done");
|
|
9063
9961
|
const result = {
|
|
9064
9962
|
name: specData.meta?.name || "unknown",
|
|
9065
9963
|
done: done.length,
|
|
9066
9964
|
in_progress: inProgress.length,
|
|
9067
9965
|
pending: pending.length,
|
|
9068
9966
|
total: tasks.length,
|
|
9967
|
+
planned: { done: plannedDone.length, total: plannedTasks.length },
|
|
9968
|
+
derived: { done: derivedDone.length, total: derivedTasks.length },
|
|
9069
9969
|
complete: remaining.length === 0,
|
|
9070
9970
|
remaining: remaining.map((t) => ({ id: t.id, action: t.action, status: t.status || "pending" }))
|
|
9071
9971
|
};
|
|
@@ -9119,6 +10019,28 @@ async function handleCheck(args) {
|
|
|
9119
10019
|
}
|
|
9120
10020
|
}
|
|
9121
10021
|
}
|
|
10022
|
+
for (const task of specData.tasks) {
|
|
10023
|
+
if (task.origin === "derived") {
|
|
10024
|
+
if (!task.derived_from || !task.derived_from.parent) {
|
|
10025
|
+
issues.push(`task '${task.id}' has origin=derived but is missing derived_from.parent`);
|
|
10026
|
+
} else if (!taskIds.has(task.derived_from.parent)) {
|
|
10027
|
+
issues.push(`task '${task.id}' derived_from.parent '${task.derived_from.parent}' does not reference a valid task ID`);
|
|
10028
|
+
}
|
|
10029
|
+
}
|
|
10030
|
+
}
|
|
10031
|
+
const allScenarioIds = /* @__PURE__ */ new Set();
|
|
10032
|
+
for (const req of specData.requirements || []) {
|
|
10033
|
+
for (const sc of req.scenarios || []) {
|
|
10034
|
+
if (sc.id) allScenarioIds.add(sc.id);
|
|
10035
|
+
}
|
|
10036
|
+
}
|
|
10037
|
+
for (const task of specData.tasks) {
|
|
10038
|
+
for (const scenarioRef of task.acceptance_criteria?.scenarios || []) {
|
|
10039
|
+
if (!allScenarioIds.has(scenarioRef)) {
|
|
10040
|
+
issues.push(`task '${task.id}' acceptance_criteria.scenarios references unknown scenario '${scenarioRef}'`);
|
|
10041
|
+
}
|
|
10042
|
+
}
|
|
10043
|
+
}
|
|
9122
10044
|
const warnings = [];
|
|
9123
10045
|
const fileScopeMap = /* @__PURE__ */ new Map();
|
|
9124
10046
|
for (const task of specData.tasks) {
|
|
@@ -9150,6 +10072,715 @@ async function handleCheck(args) {
|
|
|
9150
10072
|
process.stdout.write("Spec check passed: internal consistency OK\n");
|
|
9151
10073
|
process.exit(0);
|
|
9152
10074
|
}
|
|
10075
|
+
function generateGuide(section) {
|
|
10076
|
+
const schema = loadSchema();
|
|
10077
|
+
const defs = schema.$defs || {};
|
|
10078
|
+
const SECTIONS = {
|
|
10079
|
+
meta: { ref: "meta", desc: "Spec metadata (name, goal, mode, etc.)" },
|
|
10080
|
+
context: { ref: "context", desc: "Request context, interview decisions, research, assumptions" },
|
|
10081
|
+
tasks: { ref: "task", desc: "Task DAG (work items + verification)", isArray: true },
|
|
10082
|
+
requirements: { ref: "requirement", desc: "Requirements with scenarios and verification", isArray: true },
|
|
10083
|
+
constraints: { ref: "constraint", desc: "Must-not-do / preserve constraints", isArray: true },
|
|
10084
|
+
history: { ref: "historyEntry", desc: "Spec change history entries", isArray: true },
|
|
10085
|
+
verification: { ref: "verificationSummary", desc: "A/H/S verification classification summary" },
|
|
10086
|
+
external: { ref: "externalDependencies", desc: "Human-only pre/post-work dependencies" },
|
|
10087
|
+
scenario: { ref: "scenario", desc: "Requirement scenario (given/when/then + verify)" },
|
|
10088
|
+
verify: { ref: null, desc: "Verify types: command, assertion, instruction", custom: "verify" },
|
|
10089
|
+
merge: { ref: null, desc: "Merge modes: replace (default), --append, --patch", custom: "merge" },
|
|
10090
|
+
"acceptance-criteria": { ref: null, desc: "v5 AC structure: scenarios[] + checks[]", custom: "acceptance-criteria" }
|
|
10091
|
+
};
|
|
10092
|
+
if (!section || section === "list") {
|
|
10093
|
+
const lines = ["Available guide sections:"];
|
|
10094
|
+
for (const [name, info2] of Object.entries(SECTIONS)) {
|
|
10095
|
+
lines.push(` ${name.padEnd(16)} ${info2.desc}`);
|
|
10096
|
+
}
|
|
10097
|
+
lines.push("");
|
|
10098
|
+
lines.push("Usage: hoyeon-cli spec guide <section>");
|
|
10099
|
+
lines.push(" hoyeon-cli spec guide full (all sections)");
|
|
10100
|
+
lines.push(" hoyeon-cli spec guide root (top-level structure)");
|
|
10101
|
+
return lines.join("\n");
|
|
10102
|
+
}
|
|
10103
|
+
if (section === "root") {
|
|
10104
|
+
return formatRoot(schema);
|
|
10105
|
+
}
|
|
10106
|
+
if (section === "full") {
|
|
10107
|
+
const lines = [formatRoot(schema), ""];
|
|
10108
|
+
for (const [name, info2] of Object.entries(SECTIONS)) {
|
|
10109
|
+
const def2 = defs[info2.ref];
|
|
10110
|
+
if (def2) {
|
|
10111
|
+
lines.push(`--- ${name} ---`);
|
|
10112
|
+
lines.push(formatDef(name, def2, defs, info2.isArray));
|
|
10113
|
+
lines.push("");
|
|
10114
|
+
}
|
|
10115
|
+
}
|
|
10116
|
+
return lines.join("\n");
|
|
10117
|
+
}
|
|
10118
|
+
const info = SECTIONS[section];
|
|
10119
|
+
if (!info) {
|
|
10120
|
+
return `Error: unknown section '${section}'. Run 'hoyeon-cli spec guide' to see available sections.`;
|
|
10121
|
+
}
|
|
10122
|
+
if (info.custom === "verify") {
|
|
10123
|
+
return formatVerifyGuide(defs);
|
|
10124
|
+
}
|
|
10125
|
+
if (info.custom === "merge") {
|
|
10126
|
+
return formatMergeGuide();
|
|
10127
|
+
}
|
|
10128
|
+
if (info.custom === "acceptance-criteria") {
|
|
10129
|
+
return formatAcceptanceCriteriaGuide();
|
|
10130
|
+
}
|
|
10131
|
+
const def = defs[info.ref];
|
|
10132
|
+
if (!def) {
|
|
10133
|
+
return `Error: schema definition '${info.ref}' not found.`;
|
|
10134
|
+
}
|
|
10135
|
+
return formatDef(section, def, defs, info.isArray);
|
|
10136
|
+
}
|
|
10137
|
+
function formatRoot(schema) {
|
|
10138
|
+
const lines = ["spec.json top-level structure:"];
|
|
10139
|
+
lines.push(` required: ${(schema.required || []).join(", ")}`);
|
|
10140
|
+
lines.push(" fields:");
|
|
10141
|
+
for (const [key, val] of Object.entries(schema.properties || {})) {
|
|
10142
|
+
if (key === "$schema") continue;
|
|
10143
|
+
const req = (schema.required || []).includes(key) ? "*" : " ";
|
|
10144
|
+
const desc = val.description || "";
|
|
10145
|
+
lines.push(` ${req} ${key}${desc ? ` \u2014 ${desc}` : ""}`);
|
|
10146
|
+
}
|
|
10147
|
+
lines.push("");
|
|
10148
|
+
lines.push(" * = required");
|
|
10149
|
+
return lines.join("\n");
|
|
10150
|
+
}
|
|
10151
|
+
function formatDef(name, def, defs, isArray) {
|
|
10152
|
+
const lines = [];
|
|
10153
|
+
if (isArray) {
|
|
10154
|
+
lines.push(`${name}: array of objects`);
|
|
10155
|
+
} else {
|
|
10156
|
+
lines.push(`${name}: object`);
|
|
10157
|
+
}
|
|
10158
|
+
const required = new Set(def.required || []);
|
|
10159
|
+
if (required.size > 0) {
|
|
10160
|
+
lines.push(` required: ${[...required].join(", ")}`);
|
|
10161
|
+
}
|
|
10162
|
+
const props = def.properties || {};
|
|
10163
|
+
for (const [key, prop] of Object.entries(props)) {
|
|
10164
|
+
const req = required.has(key) ? "*" : " ";
|
|
10165
|
+
const typeStr = resolveType(prop, defs, " ");
|
|
10166
|
+
lines.push(` ${req} ${key}: ${typeStr}`);
|
|
10167
|
+
}
|
|
10168
|
+
const example = generateExample(name, def, defs, required);
|
|
10169
|
+
if (example) {
|
|
10170
|
+
lines.push("");
|
|
10171
|
+
if (isArray) {
|
|
10172
|
+
lines.push(` example merge: --json '{"${name}":[${example}]}'`);
|
|
10173
|
+
} else {
|
|
10174
|
+
lines.push(` example merge: --json '{"${name}":${example}}'`);
|
|
10175
|
+
}
|
|
10176
|
+
}
|
|
10177
|
+
return lines.join("\n");
|
|
10178
|
+
}
|
|
10179
|
+
function resolveType(prop, defs, indent) {
|
|
10180
|
+
if (prop.$ref) {
|
|
10181
|
+
const refName = prop.$ref.replace("#/$defs/", "");
|
|
10182
|
+
const refDef = defs[refName];
|
|
10183
|
+
if (refDef) {
|
|
10184
|
+
if (refDef.enum) return `enum(${refDef.enum.join("|")})`;
|
|
10185
|
+
if (refDef.type === "object") return `{${refName}}`;
|
|
10186
|
+
return refDef.type || refName;
|
|
10187
|
+
}
|
|
10188
|
+
return refName;
|
|
10189
|
+
}
|
|
10190
|
+
if (prop.oneOf) {
|
|
10191
|
+
const types = prop.oneOf.map((o) => {
|
|
10192
|
+
if (o.$ref) return `{${o.$ref.replace("#/$defs/", "")}}`;
|
|
10193
|
+
if (o.type) return o.type;
|
|
10194
|
+
return "?";
|
|
10195
|
+
});
|
|
10196
|
+
return types.join(" | ");
|
|
10197
|
+
}
|
|
10198
|
+
if (prop.enum) return `enum(${prop.enum.join("|")})`;
|
|
10199
|
+
if (prop.type === "array") {
|
|
10200
|
+
if (prop.items) {
|
|
10201
|
+
if (prop.items.$ref) {
|
|
10202
|
+
const refName = prop.items.$ref.replace("#/$defs/", "");
|
|
10203
|
+
return `[{${refName}}]`;
|
|
10204
|
+
}
|
|
10205
|
+
if (prop.items.oneOf) return `[mixed]`;
|
|
10206
|
+
if (prop.items.type === "object" && prop.items.properties) {
|
|
10207
|
+
return formatInlineObject(prop.items, indent);
|
|
10208
|
+
}
|
|
10209
|
+
return `[${prop.items.type || "any"}]`;
|
|
10210
|
+
}
|
|
10211
|
+
return "[]";
|
|
10212
|
+
}
|
|
10213
|
+
if (prop.const) return `"${prop.const}"`;
|
|
10214
|
+
if (prop.type === "object" && prop.properties) {
|
|
10215
|
+
return formatInlineObject(prop, indent);
|
|
10216
|
+
}
|
|
10217
|
+
let t = prop.type || "any";
|
|
10218
|
+
if (prop.minimum !== void 0 || prop.maximum !== void 0) {
|
|
10219
|
+
const parts = [];
|
|
10220
|
+
if (prop.minimum !== void 0) parts.push(`min:${prop.minimum}`);
|
|
10221
|
+
if (prop.maximum !== void 0) parts.push(`max:${prop.maximum}`);
|
|
10222
|
+
t += `(${parts.join(",")})`;
|
|
10223
|
+
}
|
|
10224
|
+
return t;
|
|
10225
|
+
}
|
|
10226
|
+
function formatInlineObject(schema, indent = " ") {
|
|
10227
|
+
const req = new Set(schema.required || []);
|
|
10228
|
+
const props = schema.properties || {};
|
|
10229
|
+
const fields = [];
|
|
10230
|
+
for (const [k, v] of Object.entries(props)) {
|
|
10231
|
+
const r = req.has(k) ? "*" : " ";
|
|
10232
|
+
const t = v.enum ? `enum(${v.enum.join("|")})` : v.const ? `"${v.const}"` : v.type || "any";
|
|
10233
|
+
fields.push(`${indent} ${r} ${k}: ${t}`);
|
|
10234
|
+
}
|
|
10235
|
+
const isArray = schema === schema ? "" : "";
|
|
10236
|
+
return `[object]
|
|
10237
|
+
${fields.join("\n")}`;
|
|
10238
|
+
}
|
|
10239
|
+
function generateExample(name, def, defs, required) {
|
|
10240
|
+
const props = def.properties || {};
|
|
10241
|
+
const obj = {};
|
|
10242
|
+
for (const key of required) {
|
|
10243
|
+
const prop = props[key];
|
|
10244
|
+
if (!prop) continue;
|
|
10245
|
+
obj[key] = exampleValue(key, prop, defs);
|
|
10246
|
+
}
|
|
10247
|
+
const optionals = Object.keys(props).filter((k) => !required.has(k));
|
|
10248
|
+
let added = 0;
|
|
10249
|
+
for (const key of optionals) {
|
|
10250
|
+
if (added >= 2) break;
|
|
10251
|
+
const prop = props[key];
|
|
10252
|
+
if (prop.type === "string" || prop.enum) {
|
|
10253
|
+
obj[key] = exampleValue(key, prop, defs);
|
|
10254
|
+
added++;
|
|
10255
|
+
}
|
|
10256
|
+
}
|
|
10257
|
+
try {
|
|
10258
|
+
return JSON.stringify(obj);
|
|
10259
|
+
} catch {
|
|
10260
|
+
return null;
|
|
10261
|
+
}
|
|
10262
|
+
}
|
|
10263
|
+
function exampleValue(key, prop, defs) {
|
|
10264
|
+
if (prop.enum) return prop.enum[0];
|
|
10265
|
+
if (prop.const) return prop.const;
|
|
10266
|
+
if (prop.$ref) {
|
|
10267
|
+
const refName = prop.$ref.replace("#/$defs/", "");
|
|
10268
|
+
const refDef = defs[refName];
|
|
10269
|
+
if (refDef?.enum) return refDef.enum[0];
|
|
10270
|
+
return `<${refName}>`;
|
|
10271
|
+
}
|
|
10272
|
+
if (prop.type === "string") return `<${key}>`;
|
|
10273
|
+
if (prop.type === "integer") return prop.minimum || 1;
|
|
10274
|
+
if (prop.type === "boolean") return false;
|
|
10275
|
+
if (prop.type === "array") return [];
|
|
10276
|
+
if (prop.type === "object") return {};
|
|
10277
|
+
return `<${key}>`;
|
|
10278
|
+
}
|
|
10279
|
+
function formatMergeGuide() {
|
|
10280
|
+
const lines = [
|
|
10281
|
+
"spec merge modes:",
|
|
10282
|
+
"",
|
|
10283
|
+
" (default) \u2014 replace",
|
|
10284
|
+
" Arrays are replaced entirely. Objects are deep-merged.",
|
|
10285
|
+
` hoyeon-cli spec merge <path> --json '{"tasks":[...]}'`,
|
|
10286
|
+
"",
|
|
10287
|
+
" --append \u2014 concatenate arrays",
|
|
10288
|
+
" New array items are appended to existing arrays.",
|
|
10289
|
+
` hoyeon-cli spec merge <path> --json '{"tasks":[{"id":"T2",...}]}' --append`,
|
|
10290
|
+
"",
|
|
10291
|
+
" --patch \u2014 ID-based merge",
|
|
10292
|
+
' Array items with matching "id" are updated in place.',
|
|
10293
|
+
" Items with new ids are appended. Non-array fields deep-merge normally.",
|
|
10294
|
+
` hoyeon-cli spec merge <path> --json '{"tasks":[{"id":"T1","status":"done"}]}' --patch`,
|
|
10295
|
+
"",
|
|
10296
|
+
" --append and --patch are mutually exclusive.",
|
|
10297
|
+
"",
|
|
10298
|
+
" When to use which:",
|
|
10299
|
+
" replace \u2014 rewrite a section completely (e.g. set all tasks at once)",
|
|
10300
|
+
" --append \u2014 add new items without touching existing (e.g. add requirements)",
|
|
10301
|
+
" --patch \u2014 update specific items by id (e.g. update one task's status)"
|
|
10302
|
+
];
|
|
10303
|
+
return lines.join("\n");
|
|
10304
|
+
}
|
|
10305
|
+
function formatAcceptanceCriteriaGuide() {
|
|
10306
|
+
const lines = [
|
|
10307
|
+
"acceptance_criteria (v5): scenarios[] + checks[]",
|
|
10308
|
+
"",
|
|
10309
|
+
" scenarios: string[]",
|
|
10310
|
+
" List of scenario IDs from requirements[].scenarios[].id that this task fulfills.",
|
|
10311
|
+
" These are referential \u2014 spec check validates that each ID exists in requirements.",
|
|
10312
|
+
' example: ["R1-S1", "R1-S2", "R2-S1"]',
|
|
10313
|
+
"",
|
|
10314
|
+
" checks: taskCheck[]",
|
|
10315
|
+
" Automated checks to run when verifying the task.",
|
|
10316
|
+
" Each check has:",
|
|
10317
|
+
" * type: enum(static|build|lint|format)",
|
|
10318
|
+
" * run: string (shell command)",
|
|
10319
|
+
' example: [{"type":"build","run":"cd cli && node build.mjs"},{"type":"static","run":"tsc --noEmit"}]',
|
|
10320
|
+
"",
|
|
10321
|
+
" example acceptance_criteria:",
|
|
10322
|
+
" {",
|
|
10323
|
+
' "scenarios": ["R1-S1", "R2-S1"],',
|
|
10324
|
+
' "checks": [',
|
|
10325
|
+
' {"type": "build", "run": "cd cli && node build.mjs"},',
|
|
10326
|
+
' {"type": "lint", "run": "eslint src/"}',
|
|
10327
|
+
" ]",
|
|
10328
|
+
" }",
|
|
10329
|
+
"",
|
|
10330
|
+
" Note: spec check validates referential integrity.",
|
|
10331
|
+
" AC.scenarios IDs must exist in requirements[].scenarios[].id.",
|
|
10332
|
+
" Run: hoyeon-cli spec check <path>"
|
|
10333
|
+
];
|
|
10334
|
+
return lines.join("\n");
|
|
10335
|
+
}
|
|
10336
|
+
function formatVerifyGuide(defs) {
|
|
10337
|
+
const lines = [
|
|
10338
|
+
"verify: oneOf \u2014 choose based on verified_by value:",
|
|
10339
|
+
"",
|
|
10340
|
+
' verified_by: "machine" \u2192 verifyCommand',
|
|
10341
|
+
' * type: "command"',
|
|
10342
|
+
" * run: string (shell command)",
|
|
10343
|
+
" * expect: { *exit_code: int, stdout_contains?: string, stderr_empty?: bool }",
|
|
10344
|
+
' example: {"type":"command","run":"npm test","expect":{"exit_code":0}}',
|
|
10345
|
+
"",
|
|
10346
|
+
' verified_by: "agent" \u2192 verifyAssertion',
|
|
10347
|
+
' * type: "assertion"',
|
|
10348
|
+
" * checks: [string] (min 1 item)",
|
|
10349
|
+
' example: {"type":"assertion","checks":["file exists at src/foo.ts"]}',
|
|
10350
|
+
"",
|
|
10351
|
+
' verified_by: "human" \u2192 verifyInstruction',
|
|
10352
|
+
' * type: "instruction"',
|
|
10353
|
+
" * ask: string (question for human)",
|
|
10354
|
+
' example: {"type":"instruction","ask":"Does the UI look correct?"}'
|
|
10355
|
+
];
|
|
10356
|
+
return lines.join("\n");
|
|
10357
|
+
}
|
|
10358
|
+
async function handleGuide(args) {
|
|
10359
|
+
const section = args[0];
|
|
10360
|
+
const output = generateGuide(section);
|
|
10361
|
+
process.stdout.write(output + "\n");
|
|
10362
|
+
process.exit(0);
|
|
10363
|
+
}
|
|
10364
|
+
function generateDerivedId(tasks, parentId, trigger) {
|
|
10365
|
+
const prefix = `${parentId}.${trigger}-`;
|
|
10366
|
+
let maxN = 0;
|
|
10367
|
+
for (const t of tasks) {
|
|
10368
|
+
if (t.id.startsWith(prefix)) {
|
|
10369
|
+
const suffix = t.id.slice(prefix.length);
|
|
10370
|
+
const n = parseInt(suffix, 10);
|
|
10371
|
+
if (!isNaN(n) && n > maxN) maxN = n;
|
|
10372
|
+
}
|
|
10373
|
+
}
|
|
10374
|
+
return `${prefix}${maxN + 1}`;
|
|
10375
|
+
}
|
|
10376
|
+
async function handleDerive(args) {
|
|
10377
|
+
const parsed = parseArgs(args);
|
|
10378
|
+
const requiredFlags = ["parent", "source", "trigger", "action", "reason"];
|
|
10379
|
+
for (const flag of requiredFlags) {
|
|
10380
|
+
if (!parsed[flag]) {
|
|
10381
|
+
process.stderr.write(`Error: --${flag} is required
|
|
10382
|
+
`);
|
|
10383
|
+
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");
|
|
10384
|
+
process.exit(1);
|
|
10385
|
+
}
|
|
10386
|
+
}
|
|
10387
|
+
const validTriggers = ["adapt", "retry", "code_review", "final_verify"];
|
|
10388
|
+
if (!validTriggers.includes(parsed.trigger)) {
|
|
10389
|
+
process.stderr.write(`Error: --trigger must be one of: ${validTriggers.join(", ")}
|
|
10390
|
+
`);
|
|
10391
|
+
process.exit(1);
|
|
10392
|
+
}
|
|
10393
|
+
const filePath = parsed._[0];
|
|
10394
|
+
if (!filePath) {
|
|
10395
|
+
process.stderr.write("Error: <path> to spec.json is required\n");
|
|
10396
|
+
process.exit(1);
|
|
10397
|
+
}
|
|
10398
|
+
const specPath = resolve(filePath);
|
|
10399
|
+
const specData = loadSpec(specPath);
|
|
10400
|
+
const parentTask = (specData.tasks || []).find((t) => t.id === parsed.parent);
|
|
10401
|
+
if (!parentTask) {
|
|
10402
|
+
process.stderr.write(`Error: parent task '${parsed.parent}' not found in spec
|
|
10403
|
+
`);
|
|
10404
|
+
process.exit(1);
|
|
10405
|
+
}
|
|
10406
|
+
const parentOrigin = parentTask.origin || "planned";
|
|
10407
|
+
if (parentOrigin === "derived" || parentOrigin === "adapted") {
|
|
10408
|
+
process.stderr.write(`Error: Parent must be a planned task (depth-1 enforcement)
|
|
10409
|
+
`);
|
|
10410
|
+
process.exit(1);
|
|
10411
|
+
}
|
|
10412
|
+
const newId = generateDerivedId(specData.tasks || [], parsed.parent, parsed.trigger);
|
|
10413
|
+
const fileScope = parsed["file-scope"] ? parsed["file-scope"].split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
10414
|
+
const steps = parsed.steps ? parsed.steps.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
10415
|
+
const derivedFrom = {
|
|
10416
|
+
parent: parsed.parent,
|
|
10417
|
+
trigger: parsed.trigger,
|
|
10418
|
+
source: parsed.source,
|
|
10419
|
+
reason: parsed.reason
|
|
10420
|
+
};
|
|
10421
|
+
const newTask = {
|
|
10422
|
+
id: newId,
|
|
10423
|
+
action: parsed.action,
|
|
10424
|
+
type: "work",
|
|
10425
|
+
status: "pending",
|
|
10426
|
+
origin: "derived",
|
|
10427
|
+
derived_from: derivedFrom,
|
|
10428
|
+
depends_on: [parsed.parent]
|
|
10429
|
+
};
|
|
10430
|
+
if (fileScope) newTask.file_scope = fileScope;
|
|
10431
|
+
if (steps) newTask.steps = steps;
|
|
10432
|
+
if (!specData.tasks) specData.tasks = [];
|
|
10433
|
+
specData.tasks = specData.tasks.concat([newTask]);
|
|
10434
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10435
|
+
if (!specData.history) specData.history = [];
|
|
10436
|
+
specData.history.push({
|
|
10437
|
+
ts: now,
|
|
10438
|
+
type: "tasks_changed",
|
|
10439
|
+
task: newId,
|
|
10440
|
+
detail: `derived from ${parsed.parent} via ${parsed.trigger}`
|
|
10441
|
+
});
|
|
10442
|
+
if (specData.meta) specData.meta.updated_at = now;
|
|
10443
|
+
validateSpec(specData);
|
|
10444
|
+
buildPlan(specData.tasks);
|
|
10445
|
+
writeState(specPath, specData);
|
|
10446
|
+
process.stdout.write(JSON.stringify({ created: newId }) + "\n");
|
|
10447
|
+
process.exit(0);
|
|
10448
|
+
}
|
|
10449
|
+
async function handleDrift(args) {
|
|
10450
|
+
const filePath = args[0];
|
|
10451
|
+
if (!filePath) {
|
|
10452
|
+
process.stderr.write("Error: missing <path> argument\n");
|
|
10453
|
+
process.stderr.write("Usage: hoyeon-cli spec drift <path>\n");
|
|
10454
|
+
process.exit(1);
|
|
10455
|
+
}
|
|
10456
|
+
const specData = loadSpec(resolve(filePath));
|
|
10457
|
+
const tasks = specData.tasks || [];
|
|
10458
|
+
const plannedTasks = tasks.filter((t) => !t.origin || t.origin === "planned" || t.origin === "adapted");
|
|
10459
|
+
const derivedTasks = tasks.filter((t) => t.origin === "derived");
|
|
10460
|
+
const plannedCount = plannedTasks.length;
|
|
10461
|
+
const derivedCount = derivedTasks.length;
|
|
10462
|
+
const driftRatio = plannedCount === 0 ? 0 : derivedCount / plannedCount;
|
|
10463
|
+
const byTrigger = {};
|
|
10464
|
+
for (const t of derivedTasks) {
|
|
10465
|
+
const trigger = t.derived_from?.trigger || "unknown";
|
|
10466
|
+
byTrigger[trigger] = (byTrigger[trigger] || 0) + 1;
|
|
10467
|
+
}
|
|
10468
|
+
const bySource = {};
|
|
10469
|
+
for (const t of derivedTasks) {
|
|
10470
|
+
const source = t.derived_from?.source || "unknown";
|
|
10471
|
+
bySource[source] = (bySource[source] || 0) + 1;
|
|
10472
|
+
}
|
|
10473
|
+
const result = {
|
|
10474
|
+
planned: plannedCount,
|
|
10475
|
+
derived: derivedCount,
|
|
10476
|
+
drift_ratio: Math.round(driftRatio * 1e3) / 1e3,
|
|
10477
|
+
by_trigger: byTrigger,
|
|
10478
|
+
by_source: bySource
|
|
10479
|
+
};
|
|
10480
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
10481
|
+
process.exit(0);
|
|
10482
|
+
}
|
|
10483
|
+
async function handleScenario(args) {
|
|
10484
|
+
const scenarioId = args[0];
|
|
10485
|
+
if (!scenarioId || scenarioId.startsWith("--")) {
|
|
10486
|
+
process.stderr.write("Error: <scenario-id> is required\n");
|
|
10487
|
+
process.stderr.write("Usage: hoyeon-cli spec scenario <scenario-id> --get <path>\n");
|
|
10488
|
+
process.exit(1);
|
|
10489
|
+
}
|
|
10490
|
+
const parsed = parseArgs(args.slice(1));
|
|
10491
|
+
if (parsed.get === void 0) {
|
|
10492
|
+
process.stderr.write("Error: --get <path> is required\n");
|
|
10493
|
+
process.stderr.write("Usage: hoyeon-cli spec scenario <scenario-id> --get <path>\n");
|
|
10494
|
+
process.exit(1);
|
|
10495
|
+
}
|
|
10496
|
+
if (typeof parsed.get !== "string") {
|
|
10497
|
+
process.stderr.write("Error: --get requires <path> argument\n");
|
|
10498
|
+
process.stderr.write("Usage: hoyeon-cli spec scenario <scenario-id> --get <path>\n");
|
|
10499
|
+
process.exit(1);
|
|
10500
|
+
}
|
|
10501
|
+
const filePath = parsed.get;
|
|
10502
|
+
const specData = loadSpec(resolve(filePath));
|
|
10503
|
+
let found = null;
|
|
10504
|
+
for (const req of specData.requirements || []) {
|
|
10505
|
+
for (const scenario of req.scenarios || []) {
|
|
10506
|
+
if (scenario.id === scenarioId) {
|
|
10507
|
+
found = scenario;
|
|
10508
|
+
break;
|
|
10509
|
+
}
|
|
10510
|
+
}
|
|
10511
|
+
if (found) break;
|
|
10512
|
+
}
|
|
10513
|
+
if (!found) {
|
|
10514
|
+
process.stderr.write(`Error: scenario '${scenarioId}' not found in spec
|
|
10515
|
+
`);
|
|
10516
|
+
process.exit(1);
|
|
10517
|
+
}
|
|
10518
|
+
process.stdout.write(JSON.stringify(found, null, 2) + "\n");
|
|
10519
|
+
process.exit(0);
|
|
10520
|
+
}
|
|
10521
|
+
function findScenarioById(specData, scenarioId) {
|
|
10522
|
+
for (const req of specData.requirements || []) {
|
|
10523
|
+
for (const scenario of req.scenarios || []) {
|
|
10524
|
+
if (scenario.id === scenarioId) {
|
|
10525
|
+
return { scenario, requirement: req };
|
|
10526
|
+
}
|
|
10527
|
+
}
|
|
10528
|
+
}
|
|
10529
|
+
return null;
|
|
10530
|
+
}
|
|
10531
|
+
async function handleRequirement(args) {
|
|
10532
|
+
const parsed = parseArgs(args);
|
|
10533
|
+
const firstPositional = parsed._[0];
|
|
10534
|
+
const isStatusFlag = parsed.status === true;
|
|
10535
|
+
if (!firstPositional && isStatusFlag) {
|
|
10536
|
+
const filePath = parsed._[0] || parsed._[1];
|
|
10537
|
+
const resolvedPath = typeof parsed.status === "string" ? parsed.status : parsed._[0];
|
|
10538
|
+
if (!resolvedPath) {
|
|
10539
|
+
process.stderr.write("Error: <path> is required\n");
|
|
10540
|
+
process.stderr.write("Usage: hoyeon-cli spec requirement --status <path> [--json]\n");
|
|
10541
|
+
process.exit(1);
|
|
10542
|
+
}
|
|
10543
|
+
const specData = loadSpec(resolve(resolvedPath));
|
|
10544
|
+
const useJson = parsed.json === true;
|
|
10545
|
+
return handleRequirementStatusView(specData, useJson);
|
|
10546
|
+
}
|
|
10547
|
+
if (!firstPositional && typeof parsed.status === "string") {
|
|
10548
|
+
const resolvedPath = parsed.status;
|
|
10549
|
+
if (!resolvedPath) {
|
|
10550
|
+
process.stderr.write("Error: <path> is required\n");
|
|
10551
|
+
process.exit(1);
|
|
10552
|
+
}
|
|
10553
|
+
const specData = loadSpec(resolve(resolvedPath));
|
|
10554
|
+
const useJson = parsed.json === true;
|
|
10555
|
+
return handleRequirementStatusView(specData, useJson);
|
|
10556
|
+
}
|
|
10557
|
+
const scenarioId = firstPositional;
|
|
10558
|
+
if (!scenarioId) {
|
|
10559
|
+
process.stderr.write("Error: <scenario-id> or --status flag is required\n");
|
|
10560
|
+
process.stderr.write("Usage: hoyeon-cli spec requirement --status <path>\n");
|
|
10561
|
+
process.stderr.write(" hoyeon-cli spec requirement <id> --get <path>\n");
|
|
10562
|
+
process.stderr.write(" hoyeon-cli spec requirement <id> --status pass|fail|skipped --task <task_id> <path>\n");
|
|
10563
|
+
process.exit(1);
|
|
10564
|
+
}
|
|
10565
|
+
if (parsed.get !== void 0) {
|
|
10566
|
+
if (typeof parsed.get !== "string") {
|
|
10567
|
+
process.stderr.write("Error: --get requires <path> argument\n");
|
|
10568
|
+
process.exit(1);
|
|
10569
|
+
}
|
|
10570
|
+
const specData = loadSpec(resolve(parsed.get));
|
|
10571
|
+
const found = findScenarioById(specData, scenarioId);
|
|
10572
|
+
if (!found) {
|
|
10573
|
+
process.stderr.write(`Error: scenario '${scenarioId}' not found in spec
|
|
10574
|
+
`);
|
|
10575
|
+
process.exit(1);
|
|
10576
|
+
}
|
|
10577
|
+
process.stdout.write(JSON.stringify(found.scenario, null, 2) + "\n");
|
|
10578
|
+
process.exit(0);
|
|
10579
|
+
}
|
|
10580
|
+
const statusValue = typeof parsed.status === "string" ? parsed.status : null;
|
|
10581
|
+
if (statusValue) {
|
|
10582
|
+
const validStatuses = ["pass", "fail", "skipped"];
|
|
10583
|
+
if (!validStatuses.includes(statusValue)) {
|
|
10584
|
+
process.stderr.write(`Error: --status must be one of: ${validStatuses.join(", ")}
|
|
10585
|
+
`);
|
|
10586
|
+
process.exit(1);
|
|
10587
|
+
}
|
|
10588
|
+
if (!parsed.task) {
|
|
10589
|
+
process.stderr.write("Error: --task <task_id> is required when updating scenario status\n");
|
|
10590
|
+
process.exit(1);
|
|
10591
|
+
}
|
|
10592
|
+
const filePath = parsed._[1];
|
|
10593
|
+
if (!filePath) {
|
|
10594
|
+
process.stderr.write("Error: <path> to spec.json is required\n");
|
|
10595
|
+
process.exit(1);
|
|
10596
|
+
}
|
|
10597
|
+
const specPath = resolve(filePath);
|
|
10598
|
+
const specData = loadSpec(specPath);
|
|
10599
|
+
const found = findScenarioById(specData, scenarioId);
|
|
10600
|
+
if (!found) {
|
|
10601
|
+
process.stderr.write(`Error: scenario '${scenarioId}' not found in spec
|
|
10602
|
+
`);
|
|
10603
|
+
process.exit(1);
|
|
10604
|
+
}
|
|
10605
|
+
found.scenario.status = statusValue;
|
|
10606
|
+
found.scenario.verified_by_task = parsed.task;
|
|
10607
|
+
if (parsed.reason) {
|
|
10608
|
+
found.scenario.verification_reason = parsed.reason;
|
|
10609
|
+
}
|
|
10610
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10611
|
+
if (!specData.history) specData.history = [];
|
|
10612
|
+
specData.history.push({
|
|
10613
|
+
ts: now,
|
|
10614
|
+
type: "scenario_verified",
|
|
10615
|
+
scenario: scenarioId,
|
|
10616
|
+
status: statusValue,
|
|
10617
|
+
task: parsed.task
|
|
10618
|
+
});
|
|
10619
|
+
if (specData.meta) specData.meta.updated_at = now;
|
|
10620
|
+
writeState(specPath, specData);
|
|
10621
|
+
process.stdout.write(`Updated scenario '${scenarioId}': status=${statusValue}, verified_by_task=${parsed.task}
|
|
10622
|
+
`);
|
|
10623
|
+
process.exit(0);
|
|
10624
|
+
}
|
|
10625
|
+
process.stderr.write("Error: could not determine mode. Use --get, --status (flag), or --status <value> --task <id>\n");
|
|
10626
|
+
process.exit(1);
|
|
10627
|
+
}
|
|
10628
|
+
function handleRequirementStatusView(specData, useJson) {
|
|
10629
|
+
const requirements = specData.requirements || [];
|
|
10630
|
+
const requirementRows = requirements.map((req) => {
|
|
10631
|
+
const scenarios = (req.scenarios || []).map((sc) => {
|
|
10632
|
+
const verifiedBy = sc.verified_by || "unknown";
|
|
10633
|
+
const execEnv = sc.execution_env ? `[${sc.execution_env}]` : "";
|
|
10634
|
+
const status = sc.status || "pending";
|
|
10635
|
+
const verifiedByTask = sc.verified_by_task || null;
|
|
10636
|
+
return {
|
|
10637
|
+
id: sc.id,
|
|
10638
|
+
verified_by: verifiedBy,
|
|
10639
|
+
execution_env: sc.execution_env || null,
|
|
10640
|
+
status,
|
|
10641
|
+
verified_by_task: verifiedByTask
|
|
10642
|
+
};
|
|
10643
|
+
});
|
|
10644
|
+
return {
|
|
10645
|
+
id: req.id,
|
|
10646
|
+
behavior: req.behavior,
|
|
10647
|
+
scenarios
|
|
10648
|
+
};
|
|
10649
|
+
});
|
|
10650
|
+
let passCount = 0;
|
|
10651
|
+
let failCount = 0;
|
|
10652
|
+
let pendingCount = 0;
|
|
10653
|
+
let skippedCount = 0;
|
|
10654
|
+
for (const req of requirementRows) {
|
|
10655
|
+
for (const sc of req.scenarios) {
|
|
10656
|
+
if (sc.status === "pass") passCount++;
|
|
10657
|
+
else if (sc.status === "fail") failCount++;
|
|
10658
|
+
else if (sc.status === "skipped") skippedCount++;
|
|
10659
|
+
else pendingCount++;
|
|
10660
|
+
}
|
|
10661
|
+
}
|
|
10662
|
+
const summary = { pass: passCount, fail: failCount, pending: pendingCount, skipped: skippedCount };
|
|
10663
|
+
if (useJson) {
|
|
10664
|
+
process.stdout.write(JSON.stringify({ requirements: requirementRows, summary }, null, 2) + "\n");
|
|
10665
|
+
process.exit(0);
|
|
10666
|
+
}
|
|
10667
|
+
const lines = [];
|
|
10668
|
+
for (const req of requirementRows) {
|
|
10669
|
+
const scCount = req.scenarios.length;
|
|
10670
|
+
lines.push(`${req.id}: ${req.behavior} (${scCount} scenario${scCount !== 1 ? "s" : ""})`);
|
|
10671
|
+
for (const sc of req.scenarios) {
|
|
10672
|
+
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;
|
|
10673
|
+
const taskLabel = sc.verified_by_task ? ` (${sc.verified_by_task})` : "";
|
|
10674
|
+
lines.push(` ${sc.id}: ${verifiedByLabel.padEnd(16)} ${sc.status}${taskLabel}`);
|
|
10675
|
+
}
|
|
10676
|
+
lines.push("");
|
|
10677
|
+
}
|
|
10678
|
+
const summaryParts = [];
|
|
10679
|
+
if (passCount > 0) summaryParts.push(`${passCount} pass`);
|
|
10680
|
+
if (failCount > 0) summaryParts.push(`${failCount} fail`);
|
|
10681
|
+
if (pendingCount > 0) summaryParts.push(`${pendingCount} pending`);
|
|
10682
|
+
if (skippedCount > 0) summaryParts.push(`${skippedCount} skipped`);
|
|
10683
|
+
lines.push(`Summary: ${summaryParts.join(", ") || "no scenarios"}`);
|
|
10684
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
10685
|
+
process.exit(0);
|
|
10686
|
+
}
|
|
10687
|
+
async function handleSandboxTasks(args) {
|
|
10688
|
+
const parsed = parseArgs(args);
|
|
10689
|
+
const filePath = parsed._[0];
|
|
10690
|
+
if (!filePath) {
|
|
10691
|
+
process.stderr.write("Error: <path> is required\n");
|
|
10692
|
+
process.stderr.write("Usage: hoyeon-cli spec sandbox-tasks <path> [--json]\n");
|
|
10693
|
+
process.exit(1);
|
|
10694
|
+
}
|
|
10695
|
+
const specPath = resolve(filePath);
|
|
10696
|
+
const specData = loadSpec(specPath);
|
|
10697
|
+
const useJson = parsed.json === true;
|
|
10698
|
+
const sandboxScenarios = [];
|
|
10699
|
+
for (const req of specData.requirements || []) {
|
|
10700
|
+
for (const sc of req.scenarios || []) {
|
|
10701
|
+
if (sc.execution_env === "sandbox") {
|
|
10702
|
+
sandboxScenarios.push({ ...sc, requirement_id: req.id });
|
|
10703
|
+
}
|
|
10704
|
+
}
|
|
10705
|
+
}
|
|
10706
|
+
if (sandboxScenarios.length === 0) {
|
|
10707
|
+
if (useJson) {
|
|
10708
|
+
process.stdout.write(JSON.stringify({ sandbox_scenarios: [], created_tasks: [] }, null, 2) + "\n");
|
|
10709
|
+
} else {
|
|
10710
|
+
process.stdout.write("No sandbox scenarios found. Nothing to do.\n");
|
|
10711
|
+
}
|
|
10712
|
+
process.exit(0);
|
|
10713
|
+
}
|
|
10714
|
+
const existingTasks = specData.tasks || [];
|
|
10715
|
+
const existingTaskIds = new Set(existingTasks.map((t) => t.id));
|
|
10716
|
+
const sandboxScenarioIds = new Set(sandboxScenarios.map((sc) => sc.id));
|
|
10717
|
+
const workTasksReferencingSandbox = existingTasks.filter((t) => {
|
|
10718
|
+
const acScenarios = t.acceptance_criteria?.scenarios || [];
|
|
10719
|
+
return acScenarios.some((sid) => sandboxScenarioIds.has(sid));
|
|
10720
|
+
});
|
|
10721
|
+
const createdTasks = [];
|
|
10722
|
+
if (!existingTaskIds.has("T_SANDBOX")) {
|
|
10723
|
+
const sandboxInfraTask = {
|
|
10724
|
+
id: "T_SANDBOX",
|
|
10725
|
+
action: "Prepare sandbox environment for scenario verification",
|
|
10726
|
+
type: "work",
|
|
10727
|
+
status: "pending",
|
|
10728
|
+
depends_on: workTasksReferencingSandbox.map((t) => t.id)
|
|
10729
|
+
};
|
|
10730
|
+
existingTasks.push(sandboxInfraTask);
|
|
10731
|
+
existingTaskIds.add("T_SANDBOX");
|
|
10732
|
+
createdTasks.push(sandboxInfraTask);
|
|
10733
|
+
}
|
|
10734
|
+
let svCounter = 1;
|
|
10735
|
+
for (const t of existingTasks) {
|
|
10736
|
+
const m = t.id.match(/^T_SV(\d+)$/);
|
|
10737
|
+
if (m) {
|
|
10738
|
+
const n = parseInt(m[1], 10);
|
|
10739
|
+
if (n >= svCounter) svCounter = n + 1;
|
|
10740
|
+
}
|
|
10741
|
+
}
|
|
10742
|
+
const newSvTasks = [];
|
|
10743
|
+
for (const sc of sandboxScenarios) {
|
|
10744
|
+
const svId = `T_SV${svCounter++}`;
|
|
10745
|
+
if (!existingTaskIds.has(svId)) {
|
|
10746
|
+
const svTask = {
|
|
10747
|
+
id: svId,
|
|
10748
|
+
action: `Verify ${sc.id}: ${sc.then}`,
|
|
10749
|
+
type: "work",
|
|
10750
|
+
status: "pending",
|
|
10751
|
+
depends_on: ["T_SANDBOX"]
|
|
10752
|
+
};
|
|
10753
|
+
existingTasks.push(svTask);
|
|
10754
|
+
existingTaskIds.add(svId);
|
|
10755
|
+
createdTasks.push(svTask);
|
|
10756
|
+
newSvTasks.push(svTask);
|
|
10757
|
+
}
|
|
10758
|
+
}
|
|
10759
|
+
specData.tasks = existingTasks;
|
|
10760
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10761
|
+
if (!specData.history) specData.history = [];
|
|
10762
|
+
specData.history.push({
|
|
10763
|
+
ts: now,
|
|
10764
|
+
type: "tasks_changed",
|
|
10765
|
+
detail: `sandbox-tasks: created ${createdTasks.map((t) => t.id).join(", ")}`
|
|
10766
|
+
});
|
|
10767
|
+
if (specData.meta) specData.meta.updated_at = now;
|
|
10768
|
+
writeState(specPath, specData);
|
|
10769
|
+
if (useJson) {
|
|
10770
|
+
process.stdout.write(JSON.stringify({
|
|
10771
|
+
sandbox_scenarios: sandboxScenarios.map((sc) => sc.id),
|
|
10772
|
+
created_tasks: createdTasks.map((t) => t.id)
|
|
10773
|
+
}, null, 2) + "\n");
|
|
10774
|
+
} else {
|
|
10775
|
+
process.stdout.write(`Created ${createdTasks.length} task(s):
|
|
10776
|
+
`);
|
|
10777
|
+
for (const t of createdTasks) {
|
|
10778
|
+
process.stdout.write(` ${t.id}: ${t.action}
|
|
10779
|
+
`);
|
|
10780
|
+
}
|
|
10781
|
+
}
|
|
10782
|
+
process.exit(0);
|
|
10783
|
+
}
|
|
9153
10784
|
async function spec(args) {
|
|
9154
10785
|
const subcommand = args[0];
|
|
9155
10786
|
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
@@ -9174,6 +10805,18 @@ async function spec(args) {
|
|
|
9174
10805
|
await handleCheck(args.slice(1));
|
|
9175
10806
|
} else if (subcommand === "amend") {
|
|
9176
10807
|
await handleAmend(args.slice(1));
|
|
10808
|
+
} else if (subcommand === "guide") {
|
|
10809
|
+
await handleGuide(args.slice(1));
|
|
10810
|
+
} else if (subcommand === "scenario") {
|
|
10811
|
+
await handleScenario(args.slice(1));
|
|
10812
|
+
} else if (subcommand === "derive") {
|
|
10813
|
+
await handleDerive(args.slice(1));
|
|
10814
|
+
} else if (subcommand === "drift") {
|
|
10815
|
+
await handleDrift(args.slice(1));
|
|
10816
|
+
} else if (subcommand === "requirement") {
|
|
10817
|
+
await handleRequirement(args.slice(1));
|
|
10818
|
+
} else if (subcommand === "sandbox-tasks") {
|
|
10819
|
+
await handleSandboxTasks(args.slice(1));
|
|
9177
10820
|
} else {
|
|
9178
10821
|
process.stderr.write(`Error: unknown spec subcommand '${subcommand}'
|
|
9179
10822
|
`);
|
|
@@ -9988,7 +11631,7 @@ async function main() {
|
|
|
9988
11631
|
process.exit(0);
|
|
9989
11632
|
}
|
|
9990
11633
|
if (args[0] === "--version") {
|
|
9991
|
-
const version = true ? "0.
|
|
11634
|
+
const version = true ? "1.0.0" : "dev";
|
|
9992
11635
|
process.stdout.write(`hoyeon-cli v${version}
|
|
9993
11636
|
`);
|
|
9994
11637
|
process.exit(0);
|