@nanobpm/nano-workforce 0.36.0 → 0.38.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.
Files changed (75) hide show
  1. package/.github/workflows/ci.yml +7 -17
  2. package/AGENTS.md +8 -7
  3. package/CHANGELOG.md +14 -0
  4. package/README.md +6 -27
  5. package/SPEC.md +15 -13
  6. package/app/abandon.test.ts +14 -19
  7. package/app/abandon.ts +2 -2
  8. package/app/baseGuard.test.ts +7 -6
  9. package/app/blackboard.test.ts +25 -35
  10. package/app/blackboard.ts +2 -2
  11. package/app/ensure-pr.test.ts +10 -12
  12. package/app/github.test.ts +9 -8
  13. package/app/github.ts +1 -21
  14. package/app/instance-tracking.test.ts +8 -6
  15. package/app/mergeExclusion.test.ts +11 -20
  16. package/app/mergeProtocol.test.ts +14 -13
  17. package/app/mergeRebaseArm.test.ts +8 -6
  18. package/app/mergeTrain.test.ts +10 -9
  19. package/app/persist-escalation.test.ts +10 -31
  20. package/app/persist-round.test.ts +7 -20
  21. package/app/plan.test.ts +19 -42
  22. package/app/record-plan-review.test.ts +8 -7
  23. package/app/retro.test.ts +24 -48
  24. package/app/reviewWait.test.ts +12 -11
  25. package/app/rounds.test.ts +13 -12
  26. package/app/service.test.ts +14 -27
  27. package/app/service.ts +1 -1
  28. package/app/taskDelta.test.ts +8 -17
  29. package/app/trialMerge.test.ts +4 -3
  30. package/app/version.ts +6 -21
  31. package/app/waves.test.ts +17 -16
  32. package/biome.json +0 -2
  33. package/main.ts +3 -3
  34. package/nano.app.json +2 -25
  35. package/openapi.yaml +649 -0
  36. package/operations/answerFeatureEscalation.test.ts +100 -0
  37. package/operations/answerFeatureEscalation.ts +49 -0
  38. package/operations/appendBlackboard.ts +61 -0
  39. package/{actions → operations}/blackboard.test.ts +28 -30
  40. package/{actions/abandon.test.ts → operations/checkAbandon.test.ts} +12 -26
  41. package/{actions/abandon.ts → operations/checkAbandon.ts} +8 -7
  42. package/operations/getVersion.test.ts +8 -12
  43. package/operations/getVersion.ts +2 -3
  44. package/operations/listActivePrs.test.ts +8 -14
  45. package/operations/listActivePrs.ts +3 -4
  46. package/operations/postMessage.ts +1 -1
  47. package/operations/readBlackboard.ts +29 -0
  48. package/operations/startAndMessage.test.ts +11 -15
  49. package/operations/startConvergenceLoop.ts +17 -11
  50. package/operations/startPlanFanout.ts +13 -7
  51. package/package.json +9 -7
  52. package/pages/epic.page.json +1 -1
  53. package/pages/home.page.json +1 -1
  54. package/scripts/check-agent-prompts.test.ts +15 -11
  55. package/scripts/layout-bpmn.ts +6 -28
  56. package/scripts/pages-contract.test.ts +14 -13
  57. package/scripts/purge-db.ts +1 -1
  58. package/scripts/upgrade-from-pack.ts +1 -1
  59. package/test/assert.ts +74 -0
  60. package/tsconfig.json +4 -1
  61. package/workers/persist-task-escalation/worker.ts +1 -1
  62. package/workers/record-plan-review/worker.test.ts +6 -9
  63. package/workers/record-results/worker.test.ts +5 -8
  64. package/workers/record-trial-merge/worker.test.ts +7 -18
  65. package/workers/record-wave/worker.test.ts +13 -20
  66. package/workers/retro-gather/worker.test.ts +4 -17
  67. package/workers/retro-record/worker.test.ts +8 -27
  68. package/workers/select-wave/worker.test.ts +5 -8
  69. package/actions/blackboard.ts +0 -77
  70. package/actions/feature-answer-hook.ts +0 -45
  71. package/actions/plan-hook.ts +0 -20
  72. package/actions/webhook-submit.ts +0 -22
  73. package/deno.json +0 -24
  74. package/deno.lock +0 -1777
  75. package/openapi.json +0 -248
@@ -1,5 +1,6 @@
1
1
  // Unit tests for the structured scope/impl-change report (D5, issue #55 / #49).
2
- import { assert, assertEquals } from "jsr:@std/assert@1";
2
+ import { test } from "node:test";
3
+ import { assert, assertEquals } from "#test-assert";
3
4
  import type { DataLayer } from "@nanobpm/urban";
4
5
  import {
5
6
  aggregateEpicDeltas,
@@ -11,49 +12,39 @@ import {
11
12
 
12
13
  // A tiny in-memory stand-in for the record gateway (insert/find/findOne/update/delete), mirroring
13
14
  // the fake-app style used across the app tests (see app/blackboard.test.ts).
14
- // deno-lint-ignore no-explicit-any
15
15
  function memData(): { data: DataLayer; stores: Record<string, any[]> } {
16
- // deno-lint-ignore no-explicit-any
17
16
  const stores: Record<string, any[]> = {};
18
17
  const seq: Record<string, number> = {};
19
18
  function tbl(name: string, pk = "id") {
20
- // deno-lint-ignore no-explicit-any
21
19
  const rows = (stores[name] ??= [] as any[]);
22
- // deno-lint-ignore no-explicit-any
23
20
  const match = (r: any, where: any) => Object.entries(where).every(([k, v]) => r[k] === v);
24
21
  return {
25
- // deno-lint-ignore no-explicit-any require-await
26
22
  async insert(row: any) {
27
23
  const id = (seq[name] = (seq[name] ?? 0) + 1);
28
24
  rows.push(pk === "id" ? { id, ...row } : { ...row });
29
25
  return pk === "id" ? id : row[pk];
30
26
  },
31
- // deno-lint-ignore no-explicit-any require-await
32
27
  async find(where: any = {}) {
33
28
  return rows.filter((r) => match(r, where));
34
29
  },
35
- // deno-lint-ignore no-explicit-any require-await
36
30
  async findOne(where: any = {}) {
37
31
  return rows.find((r) => match(r, where));
38
32
  },
39
- // deno-lint-ignore no-explicit-any require-await
40
33
  async update(id: any, patch: any) {
41
34
  const r = rows.find((row) => row[pk] === id);
42
35
  if (r) Object.assign(r, patch);
43
36
  },
44
- // deno-lint-ignore no-explicit-any require-await
45
37
  async delete(id: any) {
46
38
  const i = rows.findIndex((row) => row[pk] === id);
47
39
  if (i >= 0) rows.splice(i, 1);
48
40
  },
49
41
  };
50
42
  }
51
- // deno-lint-ignore no-explicit-any
52
43
  const data = { table: (n: string, pk?: string) => tbl(n, pk) } as any as DataLayer;
53
44
  return { data, stores };
54
45
  }
55
46
 
56
- Deno.test("parseTaskDelta: trims, dedupes arrays, and drops empties", () => {
47
+ test("parseTaskDelta: trims, dedupes arrays, and drops empties", () => {
57
48
  const d = parseTaskDelta({
58
49
  contractChange: " new signature ",
59
50
  newlyTouches: ["a.rs", " a.rs ", "", "b.rs"],
@@ -67,14 +58,14 @@ Deno.test("parseTaskDelta: trims, dedupes arrays, and drops empties", () => {
67
58
  assertEquals(d.constraint, undefined, "a blank constraint is dropped");
68
59
  });
69
60
 
70
- Deno.test("parseTaskDelta: a delta with nothing actionable is null", () => {
61
+ test("parseTaskDelta: a delta with nothing actionable is null", () => {
71
62
  assertEquals(parseTaskDelta(undefined), null);
72
63
  assertEquals(parseTaskDelta({}), null);
73
64
  assertEquals(parseTaskDelta({ newlyTouches: [], affectsTasks: [], contractChange: " " }), null);
74
65
  assertEquals(parseTaskDelta("not an object"), null);
75
66
  });
76
67
 
77
- Deno.test("recordTaskDelta: upserts per (plan, task) — a resume overwrites, not duplicates", async () => {
68
+ test("recordTaskDelta: upserts per (plan, task) — a resume overwrites, not duplicates", async () => {
78
69
  const { data, stores } = memData();
79
70
  const first = await recordTaskDelta(data, "o/r#1", "gap-2", {
80
71
  newlyTouches: ["a.rs"],
@@ -99,7 +90,7 @@ Deno.test("recordTaskDelta: upserts per (plan, task) — a resume overwrites, no
99
90
  assertEquals(entry.wave, 1);
100
91
  });
101
92
 
102
- Deno.test("readTaskDeltas: scoped to a plan, in write order, arrays decoded", async () => {
93
+ test("readTaskDeltas: scoped to a plan, in write order, arrays decoded", async () => {
103
94
  const { data } = memData();
104
95
  await recordTaskDelta(data, "o/r#1", "gap-2", { newlyTouches: ["a.rs"], affectsTasks: [] });
105
96
  await recordTaskDelta(data, "o/r#1", "gap-8", { newlyTouches: [], affectsTasks: ["gap-2"], constraint: "x" });
@@ -111,7 +102,7 @@ Deno.test("readTaskDeltas: scoped to a plan, in write order, arrays decoded", as
111
102
  assertEquals(entries[1].affectsTasks, ["gap-2"]);
112
103
  });
113
104
 
114
- Deno.test("aggregateEpicDeltas: unions touched files + affected tasks, lists changes/constraints", async () => {
105
+ test("aggregateEpicDeltas: unions touched files + affected tasks, lists changes/constraints", async () => {
115
106
  const { data } = memData();
116
107
  await recordTaskDelta(data, "p", "gap-2", {
117
108
  newlyTouches: ["engine/state.rs"],
@@ -132,7 +123,7 @@ Deno.test("aggregateEpicDeltas: unions touched files + affected tasks, lists cha
132
123
  assertEquals(report.deltas.length, 2);
133
124
  });
134
125
 
135
- Deno.test("clearTaskDeltas: drops a plan's whole set (re-plan cleanup), leaving other plans intact", async () => {
126
+ test("clearTaskDeltas: drops a plan's whole set (re-plan cleanup), leaving other plans intact", async () => {
136
127
  const { data, stores } = memData();
137
128
  await recordTaskDelta(data, "p", "gap-2", { newlyTouches: ["a.rs"], affectsTasks: [] });
138
129
  await recordTaskDelta(data, "p", "gap-8", { newlyTouches: ["b.rs"], affectsTasks: [] });
@@ -1,13 +1,14 @@
1
- import { assertEquals } from "jsr:@std/assert@1";
1
+ import { test } from "node:test";
2
+ import { assertEquals } from "#test-assert";
2
3
  import { shouldRunTrialMerge, trialMergeDecision } from "./trialMerge.ts";
3
4
 
4
- Deno.test("trialMergeDecision only escalates clean-merge suite failures", () => {
5
+ test("trialMergeDecision only escalates clean-merge suite failures", () => {
5
6
  assertEquals(trialMergeDecision("clean"), "proceed");
6
7
  assertEquals(trialMergeDecision("merge-conflict"), "proceed");
7
8
  assertEquals(trialMergeDecision("suite-failed"), "escalate");
8
9
  });
9
10
 
10
- Deno.test("shouldRunTrialMerge skips lone heads and mergify queues", () => {
11
+ test("shouldRunTrialMerge skips lone heads and mergify queues", () => {
11
12
  assertEquals(shouldRunTrialMerge(0, { land: { method: "gh-merge" } }), false);
12
13
  assertEquals(shouldRunTrialMerge(1, { land: { method: "gh-merge" } }), false);
13
14
  assertEquals(shouldRunTrialMerge(2, { land: { method: "mergify-queue" } }), false);
package/app/version.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  // inspecting the working tree at runtime. This module gathers that identity — the app's package
6
6
  // version, the resolved `@nanobpm/urban` version, the git commit (read from `.git`, handling both
7
7
  // an ordinary `.git` directory and the `gitdir:` file pointer used by worktrees/submodules, with
8
- // an env override for detached deploys), plus the Node/Deno runtime, pid and start time — so an operator
8
+ // an env override for detached deploys), plus the Node runtime, pid and start time — so an operator
9
9
  // debugging a stuck instance can confirm the process is on the code they think it is.
10
10
  //
11
11
  // Every probe is best-effort: a missing file or unavailable `.git` yields `null` for that field
@@ -39,20 +39,11 @@ function readJson(path: string): Record<string, unknown> | null {
39
39
  }
40
40
 
41
41
  /**
42
- * Read an env var across runtimes: Node exposes `process.env`; Deno may not populate it, so fall
43
- * back to `Deno.env.get` (guarded — reading env can throw without `--allow-env`).
42
+ * Read an env var from `process.env`, trimmed; returns null when unset or blank.
44
43
  */
45
44
  export function envVar(name: string): string | null {
46
- const fromProcess = globalThis.process?.env?.[name];
45
+ const fromProcess = process.env[name];
47
46
  if (typeof fromProcess === "string" && fromProcess.trim()) return fromProcess.trim();
48
- // biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
49
- const deno = (globalThis as { Deno?: { env?: { get?(k: string): string | undefined } } }).Deno;
50
- try {
51
- const fromDeno = deno?.env?.get?.(name);
52
- if (typeof fromDeno === "string" && fromDeno.trim()) return fromDeno.trim();
53
- } catch {
54
- // Env access denied — treat as unset.
55
- }
56
47
  return null;
57
48
  }
58
49
 
@@ -159,13 +150,8 @@ function gitBranch(): string | null {
159
150
  }
160
151
 
161
152
  function runtime(): string {
162
- const proc = globalThis.process;
163
- // Deno exposes `Deno.version.deno`; Node exposes `process.version` (e.g. "v24.15.0").
164
- // biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
165
- const deno = (globalThis as { Deno?: { version?: { deno?: string } } }).Deno;
166
- if (deno?.version?.deno) return `deno ${deno.version.deno}`;
167
- if (proc?.version) return `node ${proc.version}`;
168
- return "unknown";
153
+ // Node exposes `process.version` (e.g. "v24.15.0").
154
+ return process.version ? `node ${process.version}` : "unknown";
169
155
  }
170
156
 
171
157
  export interface VersionInfo {
@@ -185,7 +171,6 @@ export interface VersionInfo {
185
171
  * tree ONCE at module load rather than re-reading files (`.git`, package.jsons) on every request.
186
172
  */
187
173
  const STATIC: Omit<VersionInfo, "uptimeSeconds"> = (() => {
188
- const proc = globalThis.process;
189
174
  const pkg = appPackage();
190
175
  return Object.freeze({
191
176
  name: appName(pkg),
@@ -194,7 +179,7 @@ const STATIC: Omit<VersionInfo, "uptimeSeconds"> = (() => {
194
179
  gitSha: gitSha(),
195
180
  gitBranch: gitBranch(),
196
181
  runtime: runtime(),
197
- pid: typeof proc?.pid === "number" ? proc.pid : null,
182
+ pid: typeof process.pid === "number" ? process.pid : null,
198
183
  startedAt: STARTED_AT.toISOString(),
199
184
  });
200
185
  })();
package/app/waves.test.ts CHANGED
@@ -1,20 +1,21 @@
1
- // Red/green regression for the plan levelizer (issue #20). Run with `deno test`.
1
+ // Red/green regression for the plan levelizer (issue #20). Run with `node --test`.
2
2
  //
3
- // One `Deno.test` = one named property of computeWaves. These encode the wave
3
+ // One `test` = one named property of computeWaves. These encode the wave
4
4
  // contract: independent tasks share wave 0 (all-parallel), a chain steps 0,1,2…
5
5
  // (all-sequential), a diamond re-converges, and a malformed graph is rejected
6
6
  // rather than silently mis-levelized.
7
- import { assertEquals, assertThrows } from "jsr:@std/assert@1";
7
+ import { test } from "node:test";
8
+ import { assertEquals, assertThrows } from "#test-assert";
8
9
  import { computeWaves, WaveError, type WaveGateTask, type WaveTask, waveMergeTargets } from "./waves.ts";
9
10
 
10
- Deno.test("no dependencies → every task in wave 0 (fully parallel)", () => {
11
+ test("no dependencies → every task in wave 0 (fully parallel)", () => {
11
12
  const tasks: WaveTask[] = [{ id: "a" }, { id: "b" }, { id: "c" }];
12
13
  const { waves, waveCount } = computeWaves(tasks);
13
14
  assertEquals(waveCount, 1);
14
15
  assertEquals(waves, [["a", "b", "c"]]);
15
16
  });
16
17
 
17
- Deno.test("linear chain → one task per wave (fully sequential)", () => {
18
+ test("linear chain → one task per wave (fully sequential)", () => {
18
19
  const tasks: WaveTask[] = [
19
20
  { id: "a" },
20
21
  { id: "b", dependsOn: ["a"] },
@@ -26,7 +27,7 @@ Deno.test("linear chain → one task per wave (fully sequential)", () => {
26
27
  assertEquals([waveOf.get("a"), waveOf.get("b"), waveOf.get("c")], [0, 1, 2]);
27
28
  });
28
29
 
29
- Deno.test("diamond → longest-path level; join waits for both arms", () => {
30
+ test("diamond → longest-path level; join waits for both arms", () => {
30
31
  const tasks: WaveTask[] = [
31
32
  { id: "a" },
32
33
  { id: "b", dependsOn: ["a"] },
@@ -38,7 +39,7 @@ Deno.test("diamond → longest-path level; join waits for both arms", () => {
38
39
  assertEquals(waves, [["a"], ["b", "c"], ["d"]]);
39
40
  });
40
41
 
41
- Deno.test("mixed graph → level is 1 + max(dep level), not 1 + min", () => {
42
+ test("mixed graph → level is 1 + max(dep level), not 1 + min", () => {
42
43
  // e depends on a (wave 0) and d (wave 2) → must land in wave 3, behind the deeper dep.
43
44
  const tasks: WaveTask[] = [
44
45
  { id: "a" },
@@ -52,20 +53,20 @@ Deno.test("mixed graph → level is 1 + max(dep level), not 1 + min", () => {
52
53
  assertEquals(waveOf.get("e"), 4);
53
54
  });
54
55
 
55
- Deno.test("empty plan → zero waves", () => {
56
+ test("empty plan → zero waves", () => {
56
57
  const { waves, waveCount } = computeWaves([]);
57
58
  assertEquals(waveCount, 0);
58
59
  assertEquals(waves, []);
59
60
  });
60
61
 
61
- Deno.test("blank / whitespace dependsOn entries are ignored", () => {
62
+ test("blank / whitespace dependsOn entries are ignored", () => {
62
63
  const tasks: WaveTask[] = [{ id: "a", dependsOn: ["", " "] }];
63
64
  const { waves, waveCount } = computeWaves(tasks);
64
65
  assertEquals(waveCount, 1);
65
66
  assertEquals(waves, [["a"]]);
66
67
  });
67
68
 
68
- Deno.test("dependency cycle → WaveError", () => {
69
+ test("dependency cycle → WaveError", () => {
69
70
  const tasks: WaveTask[] = [
70
71
  { id: "a", dependsOn: ["b"] },
71
72
  { id: "b", dependsOn: ["a"] },
@@ -73,11 +74,11 @@ Deno.test("dependency cycle → WaveError", () => {
73
74
  assertThrows(() => computeWaves(tasks), WaveError, "cycle");
74
75
  });
75
76
 
76
- Deno.test("self-dependency → WaveError", () => {
77
+ test("self-dependency → WaveError", () => {
77
78
  assertThrows(() => computeWaves([{ id: "a", dependsOn: ["a"] }]), WaveError, "itself");
78
79
  });
79
80
 
80
- Deno.test("unknown dependency id → WaveError", () => {
81
+ test("unknown dependency id → WaveError", () => {
81
82
  assertThrows(
82
83
  () => computeWaves([{ id: "a", dependsOn: ["ghost"] }]),
83
84
  WaveError,
@@ -85,7 +86,7 @@ Deno.test("unknown dependency id → WaveError", () => {
85
86
  );
86
87
  });
87
88
 
88
- Deno.test("duplicate task id → WaveError", () => {
89
+ test("duplicate task id → WaveError", () => {
89
90
  assertThrows(
90
91
  () => computeWaves([{ id: "a" }, { id: "a" }]),
91
92
  WaveError,
@@ -98,7 +99,7 @@ Deno.test("duplicate task id → WaveError", () => {
98
99
  // opened/waiting-with-a-PR tasks of the gate wave, ignore other waves, and treat blocked/skipped
99
100
  // and keyless tasks as nothing-to-wait-on so no non-mergeable PR state can wedge the barrier.
100
101
 
101
- Deno.test("waveMergeTargets → only opened PRs of the gate wave", () => {
102
+ test("waveMergeTargets → only opened PRs of the gate wave", () => {
102
103
  const tasks: WaveGateTask[] = [
103
104
  { wave: 0, status: "opened", pr_key: "o/r#1" },
104
105
  { wave: 0, status: "opened", pr_key: "o/r#2" },
@@ -107,7 +108,7 @@ Deno.test("waveMergeTargets → only opened PRs of the gate wave", () => {
107
108
  assertEquals(waveMergeTargets(tasks, 0), ["o/r#1", "o/r#2"]);
108
109
  });
109
110
 
110
- Deno.test("waveMergeTargets → mergeable waiting tasks are waited on; failed/keyless tasks are not", () => {
111
+ test("waveMergeTargets → mergeable waiting tasks are waited on; failed/keyless tasks are not", () => {
111
112
  const tasks: WaveGateTask[] = [
112
113
  { wave: 0, status: "opened", pr_key: "o/r#1" },
113
114
  { wave: 0, status: "blocked", pr_key: null },
@@ -119,7 +120,7 @@ Deno.test("waveMergeTargets → mergeable waiting tasks are waited on; failed/ke
119
120
  assertEquals(waveMergeTargets(tasks, 0), ["o/r#1", "o/r#2"]);
120
121
  });
121
122
 
122
- Deno.test("waveMergeTargets → a wave with no opened PRs clears vacuously (empty)", () => {
123
+ test("waveMergeTargets → a wave with no opened PRs clears vacuously (empty)", () => {
123
124
  const tasks: WaveGateTask[] = [
124
125
  { wave: 0, status: "blocked", pr_key: null },
125
126
  { wave: 0, status: "skipped", pr_key: null },
package/biome.json CHANGED
@@ -4,7 +4,6 @@
4
4
  "includes": [
5
5
  "app/**/*.ts",
6
6
  "operations/**/*.ts",
7
- "actions/**/*.ts",
8
7
  "workers/**/*.ts",
9
8
  "pages/**/*.ts",
10
9
  "components/**/*.ts",
@@ -20,7 +19,6 @@
20
19
  "includes": [
21
20
  "app/**/*.ts",
22
21
  "operations/**/*.ts",
23
- "actions/**/*.ts",
24
22
  "workers/**/*.ts",
25
23
  "pages/**/*.ts",
26
24
  "components/**/*.ts",
package/main.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  // nano-workforce — Urban App entrypoint (ADR 0055).
2
2
  //
3
3
  // The whole app is declared in `nano.app.json` (models, sqlite datasource, app-hosted record
4
- // workers, the schema-driven pages surface, and the action overrides) and materialized by the
4
+ // workers, the schema-driven pages surface, and the OpenAPI control surface) and materialized by the
5
5
  // `@nanobpm/urban` runtime via `runFromEnv`:
6
6
  // • deploys the BPMN + hosts the `pr.*` record workers (workers/*/worker.ts),
7
7
  // • serves the schema-driven page runtime (ADR 0042) from `pages/home.page.json`,
8
- // • mounts the app-specific action overrides (actions/*.ts) that wrap the generic
9
- // start/cancel/message actions, plus the `/hooks/submit` webhook.
8
+ // • mounts the OpenAPI operations (openapi.yaml → operations/*.ts) the start/cancel/message
9
+ // control endpoints plus the webhook operations under `/app/api/hooks/*` (ADR 0059).
10
10
  //
11
11
  // The only thing that isn't declarative is the review-ready poller: it does arbitrary GitHub
12
12
  // polling and then correlates a `review-ready` message. A cron trigger can only fire an engine
package/nano.app.json CHANGED
@@ -140,31 +140,8 @@
140
140
  "icon": "assets/icon.svg"
141
141
  },
142
142
  "api": {
143
- "spec": "openapi.json",
143
+ "spec": "openapi.yaml",
144
144
  "dir": "operations",
145
145
  "validateResponses": "dev"
146
- },
147
- "actions": [
148
- {
149
- "path": "/hooks/submit",
150
- "module": "actions/webhook-submit.ts"
151
- },
152
- {
153
- "path": "/hooks/plan",
154
- "module": "actions/plan-hook.ts"
155
- },
156
- {
157
- "path": "/hooks/feature-answer",
158
- "module": "actions/feature-answer-hook.ts"
159
- },
160
- {
161
- "path": "/hooks/blackboard",
162
- "module": "actions/blackboard.ts"
163
- },
164
- {
165
- "path": "/hooks/abandon",
166
- "module": "actions/abandon.ts",
167
- "method": "GET"
168
- }
169
- ]
146
+ }
170
147
  }