@team-attention/hoyeon-cli 1.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -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" },
@@ -8680,6 +8685,21 @@ var dev_spec_v5_schema_default = {
8680
8685
  reason: { type: "string" }
8681
8686
  }
8682
8687
  }
8688
+ },
8689
+ implications: {
8690
+ type: "array",
8691
+ description: "Downstream behaviors/constraints this decision creates. Populated post-decision by orchestrator or L2.5 derivation step.",
8692
+ items: {
8693
+ type: "object",
8694
+ required: ["implication", "type"],
8695
+ additionalProperties: false,
8696
+ properties: {
8697
+ implication: { type: "string", description: "Concrete downstream behavior or constraint" },
8698
+ type: { type: "string", enum: ["deterministic", "context-dependent", "intent-dependent"], description: "deterministic: always true given decision. context-dependent: true given decision + project context. intent-dependent: depends on user preference (requires confirmation)." },
8699
+ status: { type: "string", enum: ["confirmed", "pending", "rejected"], default: "pending", description: "confirmed: user verified. pending: agent-derived, awaiting confirmation. rejected: user overrode." },
8700
+ conditional_on: { type: "string", description: "Other decision ID this implication depends on (e.g., 'D2'). Omit if unconditional." }
8701
+ }
8702
+ }
8683
8703
  }
8684
8704
  }
8685
8705
  }
@@ -9217,6 +9237,7 @@ Usage:
9217
9237
  hoyeon-cli spec status <path> Show task completion status (exit 0=done, 1=incomplete)
9218
9238
  hoyeon-cli spec meta <path> Show spec meta (name, goal, non_goals, mode, etc.)
9219
9239
  hoyeon-cli spec check <path> Check internal consistency
9240
+ hoyeon-cli spec coverage <path> [--layer decisions|requirements|scenarios|tasks] [--json] Check spec coverage (source.ref, decision coverage, scenario min count, orphan scenarios)
9220
9241
  hoyeon-cli spec amend --reason <feedback-id> --spec <path> Amend spec.json based on feedback
9221
9242
  hoyeon-cli spec guide [section] Show schema guide for a section
9222
9243
  hoyeon-cli spec scenario <scenario-id> --get <path> Get scenario details as JSON
@@ -10001,6 +10022,142 @@ async function handleMeta(args) {
10001
10022
  process.stdout.write(JSON.stringify(meta, null, 2) + "\n");
10002
10023
  process.exit(0);
10003
10024
  }
10025
+ function collectScenarioSets(specData) {
10026
+ const allScenarioIds = /* @__PURE__ */ new Set();
10027
+ for (const req of specData.requirements || []) {
10028
+ for (const sc of req.scenarios || []) {
10029
+ if (sc.id) allScenarioIds.add(sc.id);
10030
+ }
10031
+ }
10032
+ const referencedScenarioIds = /* @__PURE__ */ new Set();
10033
+ for (const task of specData.tasks || []) {
10034
+ for (const scenarioRef of task.acceptance_criteria?.scenarios || []) {
10035
+ if (scenarioRef) referencedScenarioIds.add(scenarioRef);
10036
+ }
10037
+ }
10038
+ return { allScenarioIds, referencedScenarioIds };
10039
+ }
10040
+ var VALID_COVERAGE_LAYERS = ["decisions", "requirements", "scenarios", "tasks"];
10041
+ async function handleCoverage(args) {
10042
+ const parsed = parseArgs(args);
10043
+ const filePath = parsed._[0];
10044
+ if (!filePath) {
10045
+ process.stderr.write("Error: missing <path> argument\n");
10046
+ process.stderr.write("Usage: hoyeon-cli spec coverage <path> [--layer decisions|requirements|scenarios|tasks] [--json]\n");
10047
+ process.exit(1);
10048
+ }
10049
+ const layer = parsed.layer;
10050
+ if (layer !== void 0 && !VALID_COVERAGE_LAYERS.includes(layer)) {
10051
+ process.stderr.write(`Error: invalid --layer '${layer}'. Valid values: ${VALID_COVERAGE_LAYERS.join(", ")}
10052
+ `);
10053
+ process.exit(1);
10054
+ }
10055
+ const useJson = parsed.json === true;
10056
+ const specData = loadSpec(resolve(filePath));
10057
+ const gaps = [];
10058
+ const decisions = specData.context?.decisions || specData.decisions || [];
10059
+ const requirements = specData.requirements || [];
10060
+ const decisionIds = new Set(decisions.map((d) => d.id).filter(Boolean));
10061
+ const runDecisions = !layer || layer === "decisions";
10062
+ const runRequirements = !layer || layer === "requirements";
10063
+ const runScenarios = !layer || layer === "scenarios";
10064
+ const runTasks = !layer || layer === "tasks";
10065
+ if (runDecisions && decisionIds.size > 0) {
10066
+ for (const req of requirements) {
10067
+ const ref = req.source?.ref;
10068
+ if (ref === void 0 || ref === null) {
10069
+ gaps.push({
10070
+ layer: "decisions",
10071
+ check: "source.ref-integrity",
10072
+ message: `requirement '${req.id}' has no source.ref (decisions exist \u2014 link required)`
10073
+ });
10074
+ } else if (!decisionIds.has(ref)) {
10075
+ gaps.push({
10076
+ layer: "decisions",
10077
+ check: "source.ref-integrity",
10078
+ message: `requirement '${req.id}' source.ref '${ref}' does not match any decision ID`
10079
+ });
10080
+ }
10081
+ }
10082
+ }
10083
+ if (runDecisions && decisionIds.size > 0 && requirements.length > 0) {
10084
+ const coveredDecisionIds = /* @__PURE__ */ new Set();
10085
+ for (const req of requirements) {
10086
+ const ref = req.source?.ref;
10087
+ if (ref) coveredDecisionIds.add(ref);
10088
+ }
10089
+ for (const decId of decisionIds) {
10090
+ if (!coveredDecisionIds.has(decId)) {
10091
+ gaps.push({
10092
+ layer: "decisions",
10093
+ check: "decision-coverage",
10094
+ message: `decision '${decId}' is not referenced by any requirement source.ref`
10095
+ });
10096
+ }
10097
+ }
10098
+ }
10099
+ if (runRequirements) {
10100
+ for (const req of requirements) {
10101
+ const scenarios = req.scenarios || [];
10102
+ const anyHasCategory = scenarios.some((sc) => sc.category !== void 0);
10103
+ if (anyHasCategory) {
10104
+ const categories = new Set(scenarios.map((sc) => sc.category).filter(Boolean));
10105
+ const missing = [];
10106
+ if (!categories.has("HP")) missing.push("HP");
10107
+ if (!categories.has("EP")) missing.push("EP");
10108
+ if (!categories.has("BC")) missing.push("BC");
10109
+ if (missing.length > 0) {
10110
+ gaps.push({
10111
+ layer: "requirements",
10112
+ check: "scenario-min-count",
10113
+ message: `requirement '${req.id}' is missing scenario categories: ${missing.join(", ")}`
10114
+ });
10115
+ }
10116
+ } else {
10117
+ if (scenarios.length < 3) {
10118
+ gaps.push({
10119
+ layer: "requirements",
10120
+ check: "scenario-min-count",
10121
+ message: `requirement '${req.id}' has ${scenarios.length} scenario(s) but needs at least 3 (count-only mode \u2014 no category field present)`
10122
+ });
10123
+ }
10124
+ }
10125
+ }
10126
+ }
10127
+ if (runScenarios && runTasks) {
10128
+ const { allScenarioIds, referencedScenarioIds } = collectScenarioSets(specData);
10129
+ const tasksWithAC = (specData.tasks || []).filter((t) => t.acceptance_criteria?.scenarios);
10130
+ if (allScenarioIds.size > 0 && tasksWithAC.length > 0) {
10131
+ for (const scenarioId of allScenarioIds) {
10132
+ if (!referencedScenarioIds.has(scenarioId)) {
10133
+ gaps.push({
10134
+ layer: "scenarios",
10135
+ check: "orphan-scenario",
10136
+ message: `scenario '${scenarioId}' is defined but not referenced by any task acceptance_criteria`
10137
+ });
10138
+ }
10139
+ }
10140
+ }
10141
+ }
10142
+ if (useJson) {
10143
+ const result = {
10144
+ coverage: gaps.length === 0 ? "pass" : "fail",
10145
+ gaps
10146
+ };
10147
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
10148
+ process.exit(gaps.length === 0 ? 0 : 1);
10149
+ }
10150
+ if (gaps.length > 0) {
10151
+ process.stderr.write("Coverage gaps found:\n");
10152
+ for (const gap of gaps) {
10153
+ process.stderr.write(` [${gap.layer}/${gap.check}] ${gap.message}
10154
+ `);
10155
+ }
10156
+ process.exit(1);
10157
+ }
10158
+ process.stdout.write("Coverage passed: all coverage checks OK\n");
10159
+ process.exit(0);
10160
+ }
10004
10161
  async function handleCheck(args) {
10005
10162
  const filePath = args[0];
10006
10163
  if (!filePath) {
@@ -10045,12 +10202,7 @@ async function handleCheck(args) {
10045
10202
  }
10046
10203
  }
10047
10204
  }
10048
- const allScenarioIds = /* @__PURE__ */ new Set();
10049
- for (const req of specData.requirements || []) {
10050
- for (const sc of req.scenarios || []) {
10051
- if (sc.id) allScenarioIds.add(sc.id);
10052
- }
10053
- }
10205
+ const { allScenarioIds, referencedScenarioIds } = collectScenarioSets(specData);
10054
10206
  for (const task of specData.tasks) {
10055
10207
  for (const scenarioRef of task.acceptance_criteria?.scenarios || []) {
10056
10208
  if (!allScenarioIds.has(scenarioRef)) {
@@ -10058,7 +10210,36 @@ async function handleCheck(args) {
10058
10210
  }
10059
10211
  }
10060
10212
  }
10213
+ const decisionIds = new Set((specData.context?.decisions || specData.decisions || []).map((d) => d.id).filter(Boolean));
10214
+ for (const req of specData.requirements || []) {
10215
+ const ref = req.source?.ref;
10216
+ if (ref !== void 0 && ref !== null) {
10217
+ if (!decisionIds.has(ref)) {
10218
+ issues.push(`requirement '${req.id}' source.ref '${ref}' does not match any decision ID`);
10219
+ }
10220
+ }
10221
+ }
10222
+ const tasksWithAC = (specData.tasks || []).filter((t) => t.acceptance_criteria?.scenarios);
10223
+ if (allScenarioIds.size > 0 && tasksWithAC.length > 0) {
10224
+ for (const scenarioId of allScenarioIds) {
10225
+ if (!referencedScenarioIds.has(scenarioId)) {
10226
+ issues.push(`scenario '${scenarioId}' is defined but not referenced by any task acceptance_criteria`);
10227
+ }
10228
+ }
10229
+ }
10061
10230
  const warnings = [];
10231
+ if ((specData.context?.decisions || specData.decisions || []).length > 0 && (specData.requirements || []).length > 0) {
10232
+ const coveredDecisionIds = /* @__PURE__ */ new Set();
10233
+ for (const req of specData.requirements || []) {
10234
+ const ref = req.source?.ref;
10235
+ if (ref) coveredDecisionIds.add(ref);
10236
+ }
10237
+ for (const decId of decisionIds) {
10238
+ if (!coveredDecisionIds.has(decId)) {
10239
+ warnings.push(`decision '${decId}' is not referenced by any requirement source.ref`);
10240
+ }
10241
+ }
10242
+ }
10062
10243
  const fileScopeMap = /* @__PURE__ */ new Map();
10063
10244
  for (const task of specData.tasks) {
10064
10245
  for (const file of task.file_scope || []) {
@@ -10818,6 +10999,8 @@ async function spec(args) {
10818
10999
  await handleStatus(args.slice(1));
10819
11000
  } else if (subcommand === "meta") {
10820
11001
  await handleMeta(args.slice(1));
11002
+ } else if (subcommand === "coverage") {
11003
+ await handleCoverage(args.slice(1));
10821
11004
  } else if (subcommand === "check") {
10822
11005
  await handleCheck(args.slice(1));
10823
11006
  } else if (subcommand === "amend") {
@@ -11613,6 +11796,197 @@ async function feedback(args) {
11613
11796
  }
11614
11797
  }
11615
11798
 
11799
+ // src/handlers/settings-validate.js
11800
+ import { readFileSync as readFileSync4, existsSync as existsSync2, accessSync, constants } from "fs";
11801
+ import { resolve as resolve4, dirname as dirname4, join as join2 } from "path";
11802
+ var SETTINGS_HELP = `
11803
+ Usage:
11804
+ hoyeon-cli settings validate Validate .claude/settings.json hook configuration
11805
+
11806
+ Options:
11807
+ --help, -h Show this help message
11808
+
11809
+ Examples:
11810
+ hoyeon-cli settings validate
11811
+ `;
11812
+ var VALID_EVENT_TYPES = /* @__PURE__ */ new Set([
11813
+ "SessionStart",
11814
+ "SessionEnd",
11815
+ "UserPromptSubmit",
11816
+ "PreToolUse",
11817
+ "PostToolUse",
11818
+ "PostToolUseFailure",
11819
+ "Stop"
11820
+ ]);
11821
+ function findSettingsJson(startDir) {
11822
+ let dir = startDir;
11823
+ while (true) {
11824
+ const candidate = join2(dir, ".claude", "settings.json");
11825
+ if (existsSync2(candidate)) {
11826
+ return candidate;
11827
+ }
11828
+ const parent = dirname4(dir);
11829
+ if (parent === dir) break;
11830
+ dir = parent;
11831
+ }
11832
+ return null;
11833
+ }
11834
+ function collectHookCommands(hooks) {
11835
+ const entries = [];
11836
+ for (const [eventType, matchers] of Object.entries(hooks)) {
11837
+ if (!Array.isArray(matchers)) continue;
11838
+ for (const matcherObj of matchers) {
11839
+ const matcher = matcherObj.matcher ?? "";
11840
+ const hookList = matcherObj.hooks ?? [];
11841
+ for (const hook of hookList) {
11842
+ if (hook.type === "command" && typeof hook.command === "string") {
11843
+ entries.push({ eventType, matcher, command: hook.command });
11844
+ }
11845
+ }
11846
+ }
11847
+ }
11848
+ return entries;
11849
+ }
11850
+ async function handleValidate2() {
11851
+ const startDir = process.cwd();
11852
+ const settingsPath = findSettingsJson(startDir);
11853
+ if (!settingsPath) {
11854
+ process.stderr.write("Error: .claude/settings.json not found (searched from cwd upward)\n");
11855
+ process.exit(1);
11856
+ }
11857
+ const projectRoot = dirname4(dirname4(settingsPath));
11858
+ let settings2;
11859
+ try {
11860
+ settings2 = JSON.parse(readFileSync4(settingsPath, "utf8"));
11861
+ } catch (err) {
11862
+ process.stderr.write(`Error: failed to parse settings.json: ${err.message}
11863
+ `);
11864
+ process.exit(1);
11865
+ }
11866
+ const hooks = settings2.hooks ?? {};
11867
+ const hookEntries = collectHookCommands(hooks);
11868
+ process.stdout.write(`Settings: ${settingsPath}
11869
+ `);
11870
+ process.stdout.write(`Project root: ${projectRoot}
11871
+
11872
+ `);
11873
+ let hasFailure = false;
11874
+ process.stdout.write("Check 1: Hook script path existence\n");
11875
+ const missingPaths = [];
11876
+ for (const { eventType, matcher, command } of hookEntries) {
11877
+ const absPath = resolve4(projectRoot, command);
11878
+ if (!existsSync2(absPath)) {
11879
+ missingPaths.push({ eventType, matcher, command, absPath });
11880
+ }
11881
+ }
11882
+ if (missingPaths.length === 0) {
11883
+ process.stdout.write(" PASS: All hook script paths exist\n");
11884
+ } else {
11885
+ hasFailure = true;
11886
+ process.stdout.write(` FAIL: ${missingPaths.length} missing path(s)
11887
+ `);
11888
+ for (const { eventType, command, absPath } of missingPaths) {
11889
+ process.stdout.write(` [${eventType}] ${command}
11890
+ `);
11891
+ process.stdout.write(` -> ${absPath} (not found)
11892
+ `);
11893
+ }
11894
+ }
11895
+ process.stdout.write("\nCheck 2: Hook script executable bit\n");
11896
+ const nonExecutable = [];
11897
+ for (const { eventType, matcher, command } of hookEntries) {
11898
+ const absPath = resolve4(projectRoot, command);
11899
+ if (!existsSync2(absPath)) continue;
11900
+ try {
11901
+ accessSync(absPath, constants.X_OK);
11902
+ } catch {
11903
+ nonExecutable.push({ eventType, command, absPath });
11904
+ }
11905
+ }
11906
+ if (nonExecutable.length === 0) {
11907
+ process.stdout.write(" PASS: All hook scripts are executable\n");
11908
+ } else {
11909
+ hasFailure = true;
11910
+ process.stdout.write(` FAIL: ${nonExecutable.length} non-executable script(s)
11911
+ `);
11912
+ for (const { eventType, command } of nonExecutable) {
11913
+ process.stdout.write(` [${eventType}] ${command} (not executable)
11914
+ `);
11915
+ }
11916
+ }
11917
+ process.stdout.write("\nCheck 3: Valid event types\n");
11918
+ const invalidEventTypes = [];
11919
+ for (const eventType of Object.keys(hooks)) {
11920
+ if (!VALID_EVENT_TYPES.has(eventType)) {
11921
+ invalidEventTypes.push(eventType);
11922
+ }
11923
+ }
11924
+ if (invalidEventTypes.length === 0) {
11925
+ process.stdout.write(" PASS: All event types are valid\n");
11926
+ } else {
11927
+ hasFailure = true;
11928
+ process.stdout.write(` FAIL: ${invalidEventTypes.length} invalid event type(s)
11929
+ `);
11930
+ for (const et of invalidEventTypes) {
11931
+ process.stdout.write(` '${et}' is not a valid event type
11932
+ `);
11933
+ }
11934
+ process.stdout.write(` Valid event types: ${[...VALID_EVENT_TYPES].join(", ")}
11935
+ `);
11936
+ }
11937
+ process.stdout.write("\nCheck 4: Duplicate scripts in same event+matcher\n");
11938
+ const seen = /* @__PURE__ */ new Map();
11939
+ const duplicates = [];
11940
+ for (const { eventType, matcher, command } of hookEntries) {
11941
+ const key = `${eventType}::${matcher}`;
11942
+ if (!seen.has(key)) {
11943
+ seen.set(key, /* @__PURE__ */ new Set());
11944
+ }
11945
+ const commandSet = seen.get(key);
11946
+ if (commandSet.has(command)) {
11947
+ duplicates.push({ eventType, matcher, command });
11948
+ } else {
11949
+ commandSet.add(command);
11950
+ }
11951
+ }
11952
+ if (duplicates.length === 0) {
11953
+ process.stdout.write(" PASS: No duplicate scripts in same event+matcher\n");
11954
+ } else {
11955
+ hasFailure = true;
11956
+ process.stdout.write(` FAIL: ${duplicates.length} duplicate(s) found
11957
+ `);
11958
+ for (const { eventType, matcher, command } of duplicates) {
11959
+ const matcherLabel = matcher ? `matcher="${matcher}"` : 'matcher=""';
11960
+ process.stdout.write(` [${eventType}][${matcherLabel}] ${command}
11961
+ `);
11962
+ }
11963
+ }
11964
+ process.stdout.write("\n");
11965
+ if (hasFailure) {
11966
+ process.stdout.write("Result: FAIL\n");
11967
+ process.exit(1);
11968
+ } else {
11969
+ process.stdout.write("Result: PASS\n");
11970
+ process.exit(0);
11971
+ }
11972
+ }
11973
+ async function settings(args) {
11974
+ const subcommand = args[0];
11975
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
11976
+ process.stdout.write(SETTINGS_HELP);
11977
+ process.exit(0);
11978
+ }
11979
+ if (subcommand === "validate") {
11980
+ await handleValidate2();
11981
+ } else {
11982
+ process.stderr.write(`Error: unknown settings subcommand '${subcommand}'
11983
+ `);
11984
+ process.stderr.write(`Run 'hoyeon-cli settings --help' for usage.
11985
+ `);
11986
+ process.exit(1);
11987
+ }
11988
+ }
11989
+
11616
11990
  // bin/dev-cli.js
11617
11991
  var USAGE = `
11618
11992
  hoyeon-cli \u2014 Developer workflow CLI
@@ -11639,7 +12013,8 @@ var SUBCOMMANDS = {
11639
12013
  spec,
11640
12014
  state,
11641
12015
  session,
11642
- feedback
12016
+ feedback,
12017
+ settings
11643
12018
  };
11644
12019
  async function main() {
11645
12020
  const args = process.argv.slice(2);
@@ -11648,7 +12023,7 @@ async function main() {
11648
12023
  process.exit(0);
11649
12024
  }
11650
12025
  if (args[0] === "--version") {
11651
- const version = true ? "1.0.1" : "dev";
12026
+ const version = true ? "1.1.1" : "dev";
11652
12027
  process.stdout.write(`hoyeon-cli v${version}
11653
12028
  `);
11654
12029
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-attention/hoyeon-cli",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
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" },
@@ -301,6 +306,21 @@
301
306
  "reason": { "type": "string" }
302
307
  }
303
308
  }
309
+ },
310
+ "implications": {
311
+ "type": "array",
312
+ "description": "Downstream behaviors/constraints this decision creates. Populated post-decision by orchestrator or L2.5 derivation step.",
313
+ "items": {
314
+ "type": "object",
315
+ "required": ["implication", "type"],
316
+ "additionalProperties": false,
317
+ "properties": {
318
+ "implication": { "type": "string", "description": "Concrete downstream behavior or constraint" },
319
+ "type": { "type": "string", "enum": ["deterministic", "context-dependent", "intent-dependent"], "description": "deterministic: always true given decision. context-dependent: true given decision + project context. intent-dependent: depends on user preference (requires confirmation)." },
320
+ "status": { "type": "string", "enum": ["confirmed", "pending", "rejected"], "default": "pending", "description": "confirmed: user verified. pending: agent-derived, awaiting confirmation. rejected: user overrode." },
321
+ "conditional_on": { "type": "string", "description": "Other decision ID this implication depends on (e.g., 'D2'). Omit if unconditional." }
322
+ }
323
+ }
304
324
  }
305
325
  }
306
326
  }