@team-attention/hoyeon-cli 1.0.0 → 1.1.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 CHANGED
@@ -402,11 +402,11 @@ var require_codegen = __commonJS({
402
402
  const rhs = this.rhs === void 0 ? "" : ` = ${this.rhs}`;
403
403
  return `${varKind} ${this.name}${rhs};` + _n;
404
404
  }
405
- optimizeNames(names, constants) {
405
+ optimizeNames(names, constants2) {
406
406
  if (!names[this.name.str])
407
407
  return;
408
408
  if (this.rhs)
409
- this.rhs = optimizeExpr(this.rhs, names, constants);
409
+ this.rhs = optimizeExpr(this.rhs, names, constants2);
410
410
  return this;
411
411
  }
412
412
  get names() {
@@ -423,10 +423,10 @@ var require_codegen = __commonJS({
423
423
  render({ _n }) {
424
424
  return `${this.lhs} = ${this.rhs};` + _n;
425
425
  }
426
- optimizeNames(names, constants) {
426
+ optimizeNames(names, constants2) {
427
427
  if (this.lhs instanceof code_1.Name && !names[this.lhs.str] && !this.sideEffects)
428
428
  return;
429
- this.rhs = optimizeExpr(this.rhs, names, constants);
429
+ this.rhs = optimizeExpr(this.rhs, names, constants2);
430
430
  return this;
431
431
  }
432
432
  get names() {
@@ -487,8 +487,8 @@ var require_codegen = __commonJS({
487
487
  optimizeNodes() {
488
488
  return `${this.code}` ? this : void 0;
489
489
  }
490
- optimizeNames(names, constants) {
491
- this.code = optimizeExpr(this.code, names, constants);
490
+ optimizeNames(names, constants2) {
491
+ this.code = optimizeExpr(this.code, names, constants2);
492
492
  return this;
493
493
  }
494
494
  get names() {
@@ -517,12 +517,12 @@ var require_codegen = __commonJS({
517
517
  }
518
518
  return nodes.length > 0 ? this : void 0;
519
519
  }
520
- optimizeNames(names, constants) {
520
+ optimizeNames(names, constants2) {
521
521
  const { nodes } = this;
522
522
  let i = nodes.length;
523
523
  while (i--) {
524
524
  const n = nodes[i];
525
- if (n.optimizeNames(names, constants))
525
+ if (n.optimizeNames(names, constants2))
526
526
  continue;
527
527
  subtractNames(names, n.names);
528
528
  nodes.splice(i, 1);
@@ -575,12 +575,12 @@ var require_codegen = __commonJS({
575
575
  return void 0;
576
576
  return this;
577
577
  }
578
- optimizeNames(names, constants) {
578
+ optimizeNames(names, constants2) {
579
579
  var _a;
580
- this.else = (_a = this.else) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants);
581
- if (!(super.optimizeNames(names, constants) || this.else))
580
+ this.else = (_a = this.else) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants2);
581
+ if (!(super.optimizeNames(names, constants2) || this.else))
582
582
  return;
583
- this.condition = optimizeExpr(this.condition, names, constants);
583
+ this.condition = optimizeExpr(this.condition, names, constants2);
584
584
  return this;
585
585
  }
586
586
  get names() {
@@ -603,10 +603,10 @@ var require_codegen = __commonJS({
603
603
  render(opts) {
604
604
  return `for(${this.iteration})` + super.render(opts);
605
605
  }
606
- optimizeNames(names, constants) {
607
- if (!super.optimizeNames(names, constants))
606
+ optimizeNames(names, constants2) {
607
+ if (!super.optimizeNames(names, constants2))
608
608
  return;
609
- this.iteration = optimizeExpr(this.iteration, names, constants);
609
+ this.iteration = optimizeExpr(this.iteration, names, constants2);
610
610
  return this;
611
611
  }
612
612
  get names() {
@@ -642,10 +642,10 @@ var require_codegen = __commonJS({
642
642
  render(opts) {
643
643
  return `for(${this.varKind} ${this.name} ${this.loop} ${this.iterable})` + super.render(opts);
644
644
  }
645
- optimizeNames(names, constants) {
646
- if (!super.optimizeNames(names, constants))
645
+ optimizeNames(names, constants2) {
646
+ if (!super.optimizeNames(names, constants2))
647
647
  return;
648
- this.iterable = optimizeExpr(this.iterable, names, constants);
648
+ this.iterable = optimizeExpr(this.iterable, names, constants2);
649
649
  return this;
650
650
  }
651
651
  get names() {
@@ -687,11 +687,11 @@ var require_codegen = __commonJS({
687
687
  (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNodes();
688
688
  return this;
689
689
  }
690
- optimizeNames(names, constants) {
690
+ optimizeNames(names, constants2) {
691
691
  var _a, _b;
692
- super.optimizeNames(names, constants);
693
- (_a = this.catch) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants);
694
- (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants);
692
+ super.optimizeNames(names, constants2);
693
+ (_a = this.catch) === null || _a === void 0 ? void 0 : _a.optimizeNames(names, constants2);
694
+ (_b = this.finally) === null || _b === void 0 ? void 0 : _b.optimizeNames(names, constants2);
695
695
  return this;
696
696
  }
697
697
  get names() {
@@ -992,7 +992,7 @@ var require_codegen = __commonJS({
992
992
  function addExprNames(names, from) {
993
993
  return from instanceof code_1._CodeOrName ? addNames(names, from.names) : names;
994
994
  }
995
- function optimizeExpr(expr, names, constants) {
995
+ function optimizeExpr(expr, names, constants2) {
996
996
  if (expr instanceof code_1.Name)
997
997
  return replaceName(expr);
998
998
  if (!canOptimize(expr))
@@ -1007,14 +1007,14 @@ var require_codegen = __commonJS({
1007
1007
  return items;
1008
1008
  }, []));
1009
1009
  function replaceName(n) {
1010
- const c = constants[n.str];
1010
+ const c = constants2[n.str];
1011
1011
  if (c === void 0 || names[n.str] !== 1)
1012
1012
  return n;
1013
1013
  delete names[n.str];
1014
1014
  return c;
1015
1015
  }
1016
1016
  function canOptimize(e) {
1017
- return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants[c.str] !== void 0);
1017
+ return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants2[c.str] !== void 0);
1018
1018
  }
1019
1019
  }
1020
1020
  function subtractNames(names, from) {
@@ -2976,7 +2976,7 @@ var require_compile = __commonJS({
2976
2976
  const schOrFunc = root.refs[ref];
2977
2977
  if (schOrFunc)
2978
2978
  return schOrFunc;
2979
- let _sch = resolve4.call(this, root, ref);
2979
+ let _sch = resolve5.call(this, root, ref);
2980
2980
  if (_sch === void 0) {
2981
2981
  const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
2982
2982
  const { schemaId } = this.opts;
@@ -3003,7 +3003,7 @@ var require_compile = __commonJS({
3003
3003
  function sameSchemaEnv(s1, s2) {
3004
3004
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3005
3005
  }
3006
- function resolve4(root, ref) {
3006
+ function resolve5(root, ref) {
3007
3007
  let sch;
3008
3008
  while (typeof (sch = this.refs[ref]) == "string")
3009
3009
  ref = sch;
@@ -3578,7 +3578,7 @@ var require_fast_uri = __commonJS({
3578
3578
  }
3579
3579
  return uri;
3580
3580
  }
3581
- function resolve4(baseURI, relativeURI, options) {
3581
+ function resolve5(baseURI, relativeURI, options) {
3582
3582
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3583
3583
  const resolved = resolveComponent(parse(baseURI, schemelessOptions), parse(relativeURI, schemelessOptions), schemelessOptions, true);
3584
3584
  schemelessOptions.skipEscape = true;
@@ -3805,7 +3805,7 @@ var require_fast_uri = __commonJS({
3805
3805
  var fastUri = {
3806
3806
  SCHEMES,
3807
3807
  normalize,
3808
- resolve: resolve4,
3808
+ resolve: resolve5,
3809
3809
  resolveComponent,
3810
3810
  equal,
3811
3811
  serialize,
@@ -8495,6 +8495,11 @@ var dev_spec_v5_schema_default = {
8495
8495
  additionalProperties: false,
8496
8496
  properties: {
8497
8497
  id: { type: "string" },
8498
+ category: {
8499
+ type: "string",
8500
+ enum: ["HP", "EP", "BC", "NI", "IT"],
8501
+ description: "Scenario coverage category: HP=Happy Path, EP=Edge/Error Path, BC=Boundary Condition, NI=Non-functional/Infrastructure, IT=Integration"
8502
+ },
8498
8503
  given: { type: "string" },
8499
8504
  when: { type: "string" },
8500
8505
  then: { type: "string" },
@@ -8721,6 +8726,23 @@ var dev_spec_v5_schema_default = {
8721
8726
  scenarios: {
8722
8727
  type: "array",
8723
8728
  items: { $ref: "#/$defs/scenario" }
8729
+ },
8730
+ source: {
8731
+ type: "object",
8732
+ required: ["type"],
8733
+ additionalProperties: false,
8734
+ description: "Traceability: where this requirement originated from",
8735
+ properties: {
8736
+ type: {
8737
+ type: "string",
8738
+ enum: ["goal", "decision", "gap", "implicit", "negative"],
8739
+ description: "Category of origin: goal=from project goal, decision=from a context decision, gap=from a known gap, implicit=inferred, negative=from non-goals"
8740
+ },
8741
+ ref: {
8742
+ type: "string",
8743
+ description: "Optional reference ID (e.g. 'D3', 'G1') pointing to a specific context entry"
8744
+ }
8745
+ }
8724
8746
  }
8725
8747
  }
8726
8748
  },
@@ -9200,6 +9222,7 @@ Usage:
9200
9222
  hoyeon-cli spec status <path> Show task completion status (exit 0=done, 1=incomplete)
9201
9223
  hoyeon-cli spec meta <path> Show spec meta (name, goal, non_goals, mode, etc.)
9202
9224
  hoyeon-cli spec check <path> Check internal consistency
9225
+ hoyeon-cli spec coverage <path> [--layer decisions|requirements|scenarios|tasks] [--json] Check spec coverage (source.ref, decision coverage, scenario min count, orphan scenarios)
9203
9226
  hoyeon-cli spec amend --reason <feedback-id> --spec <path> Amend spec.json based on feedback
9204
9227
  hoyeon-cli spec guide [section] Show schema guide for a section
9205
9228
  hoyeon-cli spec scenario <scenario-id> --get <path> Get scenario details as JSON
@@ -9984,6 +10007,142 @@ async function handleMeta(args) {
9984
10007
  process.stdout.write(JSON.stringify(meta, null, 2) + "\n");
9985
10008
  process.exit(0);
9986
10009
  }
10010
+ function collectScenarioSets(specData) {
10011
+ const allScenarioIds = /* @__PURE__ */ new Set();
10012
+ for (const req of specData.requirements || []) {
10013
+ for (const sc of req.scenarios || []) {
10014
+ if (sc.id) allScenarioIds.add(sc.id);
10015
+ }
10016
+ }
10017
+ const referencedScenarioIds = /* @__PURE__ */ new Set();
10018
+ for (const task of specData.tasks || []) {
10019
+ for (const scenarioRef of task.acceptance_criteria?.scenarios || []) {
10020
+ if (scenarioRef) referencedScenarioIds.add(scenarioRef);
10021
+ }
10022
+ }
10023
+ return { allScenarioIds, referencedScenarioIds };
10024
+ }
10025
+ var VALID_COVERAGE_LAYERS = ["decisions", "requirements", "scenarios", "tasks"];
10026
+ async function handleCoverage(args) {
10027
+ const parsed = parseArgs(args);
10028
+ const filePath = parsed._[0];
10029
+ if (!filePath) {
10030
+ process.stderr.write("Error: missing <path> argument\n");
10031
+ process.stderr.write("Usage: hoyeon-cli spec coverage <path> [--layer decisions|requirements|scenarios|tasks] [--json]\n");
10032
+ process.exit(1);
10033
+ }
10034
+ const layer = parsed.layer;
10035
+ if (layer !== void 0 && !VALID_COVERAGE_LAYERS.includes(layer)) {
10036
+ process.stderr.write(`Error: invalid --layer '${layer}'. Valid values: ${VALID_COVERAGE_LAYERS.join(", ")}
10037
+ `);
10038
+ process.exit(1);
10039
+ }
10040
+ const useJson = parsed.json === true;
10041
+ const specData = loadSpec(resolve(filePath));
10042
+ const gaps = [];
10043
+ const decisions = specData.context?.decisions || specData.decisions || [];
10044
+ const requirements = specData.requirements || [];
10045
+ const decisionIds = new Set(decisions.map((d) => d.id).filter(Boolean));
10046
+ const runDecisions = !layer || layer === "decisions";
10047
+ const runRequirements = !layer || layer === "requirements";
10048
+ const runScenarios = !layer || layer === "scenarios";
10049
+ const runTasks = !layer || layer === "tasks";
10050
+ if (runDecisions && decisionIds.size > 0) {
10051
+ for (const req of requirements) {
10052
+ const ref = req.source?.ref;
10053
+ if (ref === void 0 || ref === null) {
10054
+ gaps.push({
10055
+ layer: "decisions",
10056
+ check: "source.ref-integrity",
10057
+ message: `requirement '${req.id}' has no source.ref (decisions exist \u2014 link required)`
10058
+ });
10059
+ } else if (!decisionIds.has(ref)) {
10060
+ gaps.push({
10061
+ layer: "decisions",
10062
+ check: "source.ref-integrity",
10063
+ message: `requirement '${req.id}' source.ref '${ref}' does not match any decision ID`
10064
+ });
10065
+ }
10066
+ }
10067
+ }
10068
+ if (runDecisions && decisionIds.size > 0 && requirements.length > 0) {
10069
+ const coveredDecisionIds = /* @__PURE__ */ new Set();
10070
+ for (const req of requirements) {
10071
+ const ref = req.source?.ref;
10072
+ if (ref) coveredDecisionIds.add(ref);
10073
+ }
10074
+ for (const decId of decisionIds) {
10075
+ if (!coveredDecisionIds.has(decId)) {
10076
+ gaps.push({
10077
+ layer: "decisions",
10078
+ check: "decision-coverage",
10079
+ message: `decision '${decId}' is not referenced by any requirement source.ref`
10080
+ });
10081
+ }
10082
+ }
10083
+ }
10084
+ if (runRequirements) {
10085
+ for (const req of requirements) {
10086
+ const scenarios = req.scenarios || [];
10087
+ const anyHasCategory = scenarios.some((sc) => sc.category !== void 0);
10088
+ if (anyHasCategory) {
10089
+ const categories = new Set(scenarios.map((sc) => sc.category).filter(Boolean));
10090
+ const missing = [];
10091
+ if (!categories.has("HP")) missing.push("HP");
10092
+ if (!categories.has("EP")) missing.push("EP");
10093
+ if (!categories.has("BC")) missing.push("BC");
10094
+ if (missing.length > 0) {
10095
+ gaps.push({
10096
+ layer: "requirements",
10097
+ check: "scenario-min-count",
10098
+ message: `requirement '${req.id}' is missing scenario categories: ${missing.join(", ")}`
10099
+ });
10100
+ }
10101
+ } else {
10102
+ if (scenarios.length < 3) {
10103
+ gaps.push({
10104
+ layer: "requirements",
10105
+ check: "scenario-min-count",
10106
+ message: `requirement '${req.id}' has ${scenarios.length} scenario(s) but needs at least 3 (count-only mode \u2014 no category field present)`
10107
+ });
10108
+ }
10109
+ }
10110
+ }
10111
+ }
10112
+ if (runScenarios && runTasks) {
10113
+ const { allScenarioIds, referencedScenarioIds } = collectScenarioSets(specData);
10114
+ const tasksWithAC = (specData.tasks || []).filter((t) => t.acceptance_criteria?.scenarios);
10115
+ if (allScenarioIds.size > 0 && tasksWithAC.length > 0) {
10116
+ for (const scenarioId of allScenarioIds) {
10117
+ if (!referencedScenarioIds.has(scenarioId)) {
10118
+ gaps.push({
10119
+ layer: "scenarios",
10120
+ check: "orphan-scenario",
10121
+ message: `scenario '${scenarioId}' is defined but not referenced by any task acceptance_criteria`
10122
+ });
10123
+ }
10124
+ }
10125
+ }
10126
+ }
10127
+ if (useJson) {
10128
+ const result = {
10129
+ coverage: gaps.length === 0 ? "pass" : "fail",
10130
+ gaps
10131
+ };
10132
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
10133
+ process.exit(gaps.length === 0 ? 0 : 1);
10134
+ }
10135
+ if (gaps.length > 0) {
10136
+ process.stderr.write("Coverage gaps found:\n");
10137
+ for (const gap of gaps) {
10138
+ process.stderr.write(` [${gap.layer}/${gap.check}] ${gap.message}
10139
+ `);
10140
+ }
10141
+ process.exit(1);
10142
+ }
10143
+ process.stdout.write("Coverage passed: all coverage checks OK\n");
10144
+ process.exit(0);
10145
+ }
9987
10146
  async function handleCheck(args) {
9988
10147
  const filePath = args[0];
9989
10148
  if (!filePath) {
@@ -10028,12 +10187,7 @@ async function handleCheck(args) {
10028
10187
  }
10029
10188
  }
10030
10189
  }
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
- }
10190
+ const { allScenarioIds, referencedScenarioIds } = collectScenarioSets(specData);
10037
10191
  for (const task of specData.tasks) {
10038
10192
  for (const scenarioRef of task.acceptance_criteria?.scenarios || []) {
10039
10193
  if (!allScenarioIds.has(scenarioRef)) {
@@ -10041,7 +10195,36 @@ async function handleCheck(args) {
10041
10195
  }
10042
10196
  }
10043
10197
  }
10198
+ const decisionIds = new Set((specData.context?.decisions || specData.decisions || []).map((d) => d.id).filter(Boolean));
10199
+ for (const req of specData.requirements || []) {
10200
+ const ref = req.source?.ref;
10201
+ if (ref !== void 0 && ref !== null) {
10202
+ if (!decisionIds.has(ref)) {
10203
+ issues.push(`requirement '${req.id}' source.ref '${ref}' does not match any decision ID`);
10204
+ }
10205
+ }
10206
+ }
10207
+ const tasksWithAC = (specData.tasks || []).filter((t) => t.acceptance_criteria?.scenarios);
10208
+ if (allScenarioIds.size > 0 && tasksWithAC.length > 0) {
10209
+ for (const scenarioId of allScenarioIds) {
10210
+ if (!referencedScenarioIds.has(scenarioId)) {
10211
+ issues.push(`scenario '${scenarioId}' is defined but not referenced by any task acceptance_criteria`);
10212
+ }
10213
+ }
10214
+ }
10044
10215
  const warnings = [];
10216
+ if ((specData.context?.decisions || specData.decisions || []).length > 0 && (specData.requirements || []).length > 0) {
10217
+ const coveredDecisionIds = /* @__PURE__ */ new Set();
10218
+ for (const req of specData.requirements || []) {
10219
+ const ref = req.source?.ref;
10220
+ if (ref) coveredDecisionIds.add(ref);
10221
+ }
10222
+ for (const decId of decisionIds) {
10223
+ if (!coveredDecisionIds.has(decId)) {
10224
+ warnings.push(`decision '${decId}' is not referenced by any requirement source.ref`);
10225
+ }
10226
+ }
10227
+ }
10045
10228
  const fileScopeMap = /* @__PURE__ */ new Map();
10046
10229
  for (const task of specData.tasks) {
10047
10230
  for (const file of task.file_scope || []) {
@@ -10801,6 +10984,8 @@ async function spec(args) {
10801
10984
  await handleStatus(args.slice(1));
10802
10985
  } else if (subcommand === "meta") {
10803
10986
  await handleMeta(args.slice(1));
10987
+ } else if (subcommand === "coverage") {
10988
+ await handleCoverage(args.slice(1));
10804
10989
  } else if (subcommand === "check") {
10805
10990
  await handleCheck(args.slice(1));
10806
10991
  } else if (subcommand === "amend") {
@@ -11596,6 +11781,197 @@ async function feedback(args) {
11596
11781
  }
11597
11782
  }
11598
11783
 
11784
+ // src/handlers/settings-validate.js
11785
+ import { readFileSync as readFileSync4, existsSync as existsSync2, accessSync, constants } from "fs";
11786
+ import { resolve as resolve4, dirname as dirname4, join as join2 } from "path";
11787
+ var SETTINGS_HELP = `
11788
+ Usage:
11789
+ hoyeon-cli settings validate Validate .claude/settings.json hook configuration
11790
+
11791
+ Options:
11792
+ --help, -h Show this help message
11793
+
11794
+ Examples:
11795
+ hoyeon-cli settings validate
11796
+ `;
11797
+ var VALID_EVENT_TYPES = /* @__PURE__ */ new Set([
11798
+ "SessionStart",
11799
+ "SessionEnd",
11800
+ "UserPromptSubmit",
11801
+ "PreToolUse",
11802
+ "PostToolUse",
11803
+ "PostToolUseFailure",
11804
+ "Stop"
11805
+ ]);
11806
+ function findSettingsJson(startDir) {
11807
+ let dir = startDir;
11808
+ while (true) {
11809
+ const candidate = join2(dir, ".claude", "settings.json");
11810
+ if (existsSync2(candidate)) {
11811
+ return candidate;
11812
+ }
11813
+ const parent = dirname4(dir);
11814
+ if (parent === dir) break;
11815
+ dir = parent;
11816
+ }
11817
+ return null;
11818
+ }
11819
+ function collectHookCommands(hooks) {
11820
+ const entries = [];
11821
+ for (const [eventType, matchers] of Object.entries(hooks)) {
11822
+ if (!Array.isArray(matchers)) continue;
11823
+ for (const matcherObj of matchers) {
11824
+ const matcher = matcherObj.matcher ?? "";
11825
+ const hookList = matcherObj.hooks ?? [];
11826
+ for (const hook of hookList) {
11827
+ if (hook.type === "command" && typeof hook.command === "string") {
11828
+ entries.push({ eventType, matcher, command: hook.command });
11829
+ }
11830
+ }
11831
+ }
11832
+ }
11833
+ return entries;
11834
+ }
11835
+ async function handleValidate2() {
11836
+ const startDir = process.cwd();
11837
+ const settingsPath = findSettingsJson(startDir);
11838
+ if (!settingsPath) {
11839
+ process.stderr.write("Error: .claude/settings.json not found (searched from cwd upward)\n");
11840
+ process.exit(1);
11841
+ }
11842
+ const projectRoot = dirname4(dirname4(settingsPath));
11843
+ let settings2;
11844
+ try {
11845
+ settings2 = JSON.parse(readFileSync4(settingsPath, "utf8"));
11846
+ } catch (err) {
11847
+ process.stderr.write(`Error: failed to parse settings.json: ${err.message}
11848
+ `);
11849
+ process.exit(1);
11850
+ }
11851
+ const hooks = settings2.hooks ?? {};
11852
+ const hookEntries = collectHookCommands(hooks);
11853
+ process.stdout.write(`Settings: ${settingsPath}
11854
+ `);
11855
+ process.stdout.write(`Project root: ${projectRoot}
11856
+
11857
+ `);
11858
+ let hasFailure = false;
11859
+ process.stdout.write("Check 1: Hook script path existence\n");
11860
+ const missingPaths = [];
11861
+ for (const { eventType, matcher, command } of hookEntries) {
11862
+ const absPath = resolve4(projectRoot, command);
11863
+ if (!existsSync2(absPath)) {
11864
+ missingPaths.push({ eventType, matcher, command, absPath });
11865
+ }
11866
+ }
11867
+ if (missingPaths.length === 0) {
11868
+ process.stdout.write(" PASS: All hook script paths exist\n");
11869
+ } else {
11870
+ hasFailure = true;
11871
+ process.stdout.write(` FAIL: ${missingPaths.length} missing path(s)
11872
+ `);
11873
+ for (const { eventType, command, absPath } of missingPaths) {
11874
+ process.stdout.write(` [${eventType}] ${command}
11875
+ `);
11876
+ process.stdout.write(` -> ${absPath} (not found)
11877
+ `);
11878
+ }
11879
+ }
11880
+ process.stdout.write("\nCheck 2: Hook script executable bit\n");
11881
+ const nonExecutable = [];
11882
+ for (const { eventType, matcher, command } of hookEntries) {
11883
+ const absPath = resolve4(projectRoot, command);
11884
+ if (!existsSync2(absPath)) continue;
11885
+ try {
11886
+ accessSync(absPath, constants.X_OK);
11887
+ } catch {
11888
+ nonExecutable.push({ eventType, command, absPath });
11889
+ }
11890
+ }
11891
+ if (nonExecutable.length === 0) {
11892
+ process.stdout.write(" PASS: All hook scripts are executable\n");
11893
+ } else {
11894
+ hasFailure = true;
11895
+ process.stdout.write(` FAIL: ${nonExecutable.length} non-executable script(s)
11896
+ `);
11897
+ for (const { eventType, command } of nonExecutable) {
11898
+ process.stdout.write(` [${eventType}] ${command} (not executable)
11899
+ `);
11900
+ }
11901
+ }
11902
+ process.stdout.write("\nCheck 3: Valid event types\n");
11903
+ const invalidEventTypes = [];
11904
+ for (const eventType of Object.keys(hooks)) {
11905
+ if (!VALID_EVENT_TYPES.has(eventType)) {
11906
+ invalidEventTypes.push(eventType);
11907
+ }
11908
+ }
11909
+ if (invalidEventTypes.length === 0) {
11910
+ process.stdout.write(" PASS: All event types are valid\n");
11911
+ } else {
11912
+ hasFailure = true;
11913
+ process.stdout.write(` FAIL: ${invalidEventTypes.length} invalid event type(s)
11914
+ `);
11915
+ for (const et of invalidEventTypes) {
11916
+ process.stdout.write(` '${et}' is not a valid event type
11917
+ `);
11918
+ }
11919
+ process.stdout.write(` Valid event types: ${[...VALID_EVENT_TYPES].join(", ")}
11920
+ `);
11921
+ }
11922
+ process.stdout.write("\nCheck 4: Duplicate scripts in same event+matcher\n");
11923
+ const seen = /* @__PURE__ */ new Map();
11924
+ const duplicates = [];
11925
+ for (const { eventType, matcher, command } of hookEntries) {
11926
+ const key = `${eventType}::${matcher}`;
11927
+ if (!seen.has(key)) {
11928
+ seen.set(key, /* @__PURE__ */ new Set());
11929
+ }
11930
+ const commandSet = seen.get(key);
11931
+ if (commandSet.has(command)) {
11932
+ duplicates.push({ eventType, matcher, command });
11933
+ } else {
11934
+ commandSet.add(command);
11935
+ }
11936
+ }
11937
+ if (duplicates.length === 0) {
11938
+ process.stdout.write(" PASS: No duplicate scripts in same event+matcher\n");
11939
+ } else {
11940
+ hasFailure = true;
11941
+ process.stdout.write(` FAIL: ${duplicates.length} duplicate(s) found
11942
+ `);
11943
+ for (const { eventType, matcher, command } of duplicates) {
11944
+ const matcherLabel = matcher ? `matcher="${matcher}"` : 'matcher=""';
11945
+ process.stdout.write(` [${eventType}][${matcherLabel}] ${command}
11946
+ `);
11947
+ }
11948
+ }
11949
+ process.stdout.write("\n");
11950
+ if (hasFailure) {
11951
+ process.stdout.write("Result: FAIL\n");
11952
+ process.exit(1);
11953
+ } else {
11954
+ process.stdout.write("Result: PASS\n");
11955
+ process.exit(0);
11956
+ }
11957
+ }
11958
+ async function settings(args) {
11959
+ const subcommand = args[0];
11960
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
11961
+ process.stdout.write(SETTINGS_HELP);
11962
+ process.exit(0);
11963
+ }
11964
+ if (subcommand === "validate") {
11965
+ await handleValidate2();
11966
+ } else {
11967
+ process.stderr.write(`Error: unknown settings subcommand '${subcommand}'
11968
+ `);
11969
+ process.stderr.write(`Run 'hoyeon-cli settings --help' for usage.
11970
+ `);
11971
+ process.exit(1);
11972
+ }
11973
+ }
11974
+
11599
11975
  // bin/dev-cli.js
11600
11976
  var USAGE = `
11601
11977
  hoyeon-cli \u2014 Developer workflow CLI
@@ -11622,7 +11998,8 @@ var SUBCOMMANDS = {
11622
11998
  spec,
11623
11999
  state,
11624
12000
  session,
11625
- feedback
12001
+ feedback,
12002
+ settings
11626
12003
  };
11627
12004
  async function main() {
11628
12005
  const args = process.argv.slice(2);
@@ -11631,7 +12008,7 @@ async function main() {
11631
12008
  process.exit(0);
11632
12009
  }
11633
12010
  if (args[0] === "--version") {
11634
- const version = true ? "1.0.0" : "dev";
12011
+ const version = true ? "1.1.0" : "dev";
11635
12012
  process.stdout.write(`hoyeon-cli v${version}
11636
12013
  `);
11637
12014
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-attention/hoyeon-cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "Developer CLI for managing dev workflow state",
6
6
  "bin": {
@@ -14,14 +14,15 @@
14
14
  "publishConfig": {
15
15
  "access": "public"
16
16
  },
17
- "scripts": {
18
- "build": "node build.mjs"
19
- },
20
17
  "dependencies": {
21
18
  "ajv": "^8.0.0",
22
19
  "ajv-formats": "^3.0.0"
23
20
  },
24
21
  "devDependencies": {
25
22
  "esbuild": "^0.27.3"
23
+ },
24
+ "scripts": {
25
+ "build": "node build.mjs",
26
+ "test": "node --test tests/*.test.mjs"
26
27
  }
27
- }
28
+ }
@@ -116,6 +116,11 @@
116
116
  "additionalProperties": false,
117
117
  "properties": {
118
118
  "id": { "type": "string" },
119
+ "category": {
120
+ "type": "string",
121
+ "enum": ["HP", "EP", "BC", "NI", "IT"],
122
+ "description": "Scenario coverage category: HP=Happy Path, EP=Edge/Error Path, BC=Boundary Condition, NI=Non-functional/Infrastructure, IT=Integration"
123
+ },
119
124
  "given": { "type": "string" },
120
125
  "when": { "type": "string" },
121
126
  "then": { "type": "string" },
@@ -342,6 +347,23 @@
342
347
  "scenarios": {
343
348
  "type": "array",
344
349
  "items": { "$ref": "#/$defs/scenario" }
350
+ },
351
+ "source": {
352
+ "type": "object",
353
+ "required": ["type"],
354
+ "additionalProperties": false,
355
+ "description": "Traceability: where this requirement originated from",
356
+ "properties": {
357
+ "type": {
358
+ "type": "string",
359
+ "enum": ["goal", "decision", "gap", "implicit", "negative"],
360
+ "description": "Category of origin: goal=from project goal, decision=from a context decision, gap=from a known gap, implicit=inferred, negative=from non-goals"
361
+ },
362
+ "ref": {
363
+ "type": "string",
364
+ "description": "Optional reference ID (e.g. 'D3', 'G1') pointing to a specific context entry"
365
+ }
366
+ }
345
367
  }
346
368
  }
347
369
  },