@s_s/harmonia 1.0.0 → 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.
Files changed (114) hide show
  1. package/README.md +396 -2
  2. package/build/cli/setup.d.ts +21 -0
  3. package/build/cli/setup.js +71 -0
  4. package/build/cli/setup.js.map +1 -0
  5. package/build/core/dispatch.d.ts +10 -0
  6. package/build/core/dispatch.js +21 -0
  7. package/build/core/dispatch.js.map +1 -1
  8. package/build/core/docs.d.ts +13 -0
  9. package/build/core/docs.js +32 -0
  10. package/build/core/docs.js.map +1 -1
  11. package/build/core/registry.d.ts +1 -1
  12. package/build/core/registry.js +5 -16
  13. package/build/core/registry.js.map +1 -1
  14. package/build/core/schema.d.ts +38 -0
  15. package/build/core/schema.js +187 -0
  16. package/build/core/schema.js.map +1 -0
  17. package/build/core/state.d.ts +11 -1
  18. package/build/core/state.js +23 -2
  19. package/build/core/state.js.map +1 -1
  20. package/build/core/steps.d.ts +34 -0
  21. package/build/core/steps.js +113 -0
  22. package/build/core/steps.js.map +1 -0
  23. package/build/core/types.d.ts +81 -4
  24. package/build/core/workflow.d.ts +26 -6
  25. package/build/core/workflow.js +88 -11
  26. package/build/core/workflow.js.map +1 -1
  27. package/build/hooks/claude-code.d.ts +20 -0
  28. package/build/hooks/claude-code.js +218 -0
  29. package/build/hooks/claude-code.js.map +1 -0
  30. package/build/hooks/content.d.ts +43 -0
  31. package/build/hooks/content.js +109 -0
  32. package/build/hooks/content.js.map +1 -0
  33. package/build/hooks/install.d.ts +40 -0
  34. package/build/hooks/install.js +63 -0
  35. package/build/hooks/install.js.map +1 -0
  36. package/build/hooks/openclaw.d.ts +24 -0
  37. package/build/hooks/openclaw.js +219 -0
  38. package/build/hooks/openclaw.js.map +1 -0
  39. package/build/hooks/opencode.d.ts +29 -0
  40. package/build/hooks/opencode.js +226 -0
  41. package/build/hooks/opencode.js.map +1 -0
  42. package/build/index.d.ts +4 -7
  43. package/build/index.js +80 -42
  44. package/build/index.js.map +1 -1
  45. package/build/setup/inject.d.ts +22 -18
  46. package/build/setup/inject.js +42 -93
  47. package/build/setup/inject.js.map +1 -1
  48. package/build/setup/templates.d.ts +12 -16
  49. package/build/setup/templates.js +52 -69
  50. package/build/setup/templates.js.map +1 -1
  51. package/build/tools/approve-doc.d.ts +1 -1
  52. package/build/tools/approve-doc.js +4 -4
  53. package/build/tools/approve-doc.js.map +1 -1
  54. package/build/tools/dispatch-role.d.ts +2 -2
  55. package/build/tools/dispatch-role.js +41 -11
  56. package/build/tools/dispatch-role.js.map +1 -1
  57. package/build/tools/doc-tools.d.ts +11 -3
  58. package/build/tools/doc-tools.js +257 -13
  59. package/build/tools/doc-tools.js.map +1 -1
  60. package/build/tools/get-project-status.d.ts +4 -2
  61. package/build/tools/get-project-status.js +165 -50
  62. package/build/tools/get-project-status.js.map +1 -1
  63. package/build/tools/get-role-prompt.d.ts +2 -2
  64. package/build/tools/get-role-prompt.js +4 -4
  65. package/build/tools/get-role-prompt.js.map +1 -1
  66. package/build/tools/override-tools.d.ts +1 -1
  67. package/build/tools/override-tools.js +4 -4
  68. package/build/tools/override-tools.js.map +1 -1
  69. package/build/tools/project-init.d.ts +5 -1
  70. package/build/tools/project-init.js +92 -32
  71. package/build/tools/project-init.js.map +1 -1
  72. package/build/tools/report-dispatch.d.ts +6 -3
  73. package/build/tools/report-dispatch.js +45 -8
  74. package/build/tools/report-dispatch.js.map +1 -1
  75. package/build/tools/set-scale.d.ts +6 -0
  76. package/build/tools/set-scale.js +92 -0
  77. package/build/tools/set-scale.js.map +1 -0
  78. package/build/tools/setup-project.d.ts +1 -1
  79. package/build/tools/setup-project.js +33 -5
  80. package/build/tools/setup-project.js.map +1 -1
  81. package/build/tools/update-phase.d.ts +8 -3
  82. package/build/tools/update-phase.js +85 -20
  83. package/build/tools/update-phase.js.map +1 -1
  84. package/package.json +2 -1
  85. package/workflows/dev/roles/architect.md +1 -1
  86. package/workflows/dev/roles/pm.md +5 -5
  87. package/workflows/dev/roles/tester.md +1 -1
  88. package/workflows/dev/schemas/api-design.json +25 -0
  89. package/workflows/dev/schemas/data-model.json +20 -0
  90. package/workflows/dev/schemas/deploy.json +20 -0
  91. package/workflows/dev/schemas/fsd.json +25 -0
  92. package/workflows/dev/schemas/prd.completeness-check.json +24 -0
  93. package/workflows/dev/schemas/prd.draft.json +15 -0
  94. package/workflows/dev/schemas/prd.final.json +30 -0
  95. package/workflows/dev/schemas/prd.json +30 -0
  96. package/workflows/dev/schemas/prd.requirements.json +25 -0
  97. package/workflows/dev/schemas/project-plan.json +20 -0
  98. package/workflows/dev/schemas/prototype.json +4 -0
  99. package/workflows/dev/schemas/retrospective.json +20 -0
  100. package/workflows/dev/schemas/risk-assessment.json +15 -0
  101. package/workflows/dev/schemas/task-breakdown.coarse.json +15 -0
  102. package/workflows/dev/schemas/task-breakdown.dependencies.json +20 -0
  103. package/workflows/dev/schemas/task-breakdown.detailed.json +10 -0
  104. package/workflows/dev/schemas/task-breakdown.final.json +10 -0
  105. package/workflows/dev/schemas/task-breakdown.json +10 -0
  106. package/workflows/dev/schemas/tech-design.analysis.json +25 -0
  107. package/workflows/dev/schemas/tech-design.api-contract.json +20 -0
  108. package/workflows/dev/schemas/tech-design.draft.json +15 -0
  109. package/workflows/dev/schemas/tech-design.final.json +30 -0
  110. package/workflows/dev/schemas/tech-design.json +30 -0
  111. package/workflows/dev/schemas/test-plan.json +20 -0
  112. package/workflows/dev/schemas/test-report.json +25 -0
  113. package/workflows/dev/schemas/user-stories.json +10 -0
  114. package/workflows/dev/workflow.json +85 -5
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Hook content generation — shared configuration and rule definitions.
3
+ *
4
+ * Hook scripts run on the agent side (shell scripts for Claude Code,
5
+ * TS plugins for OpenCode, handlers for OpenClaw). They need to know:
6
+ * - HARMONIA_DATA_DIR: where to read dispatches.json, state.json, etc.
7
+ *
8
+ * Project-specific info (name, dir) is NOT baked in — hooks are project-agnostic.
9
+ * Boundary guards work on tool names + code file extensions only.
10
+ * Proactive reminders scan all projects under the data directory.
11
+ */
12
+ // ─── Boundary Rules ───
13
+ /**
14
+ * Tool names that PM should not call directly (code modification tools).
15
+ * These are the standard agent tool names across different platforms.
16
+ */
17
+ export const BLOCKED_TOOLS = [
18
+ // File writing tools
19
+ 'Write',
20
+ 'Edit',
21
+ 'MultiEdit',
22
+ 'write',
23
+ 'edit',
24
+ // Bash/shell tools
25
+ 'Bash',
26
+ 'bash',
27
+ 'Terminal',
28
+ 'terminal',
29
+ ];
30
+ /**
31
+ * Shell commands that indicate development work (PM should not run these).
32
+ */
33
+ export const BLOCKED_COMMANDS = [
34
+ 'npm run',
35
+ 'npm test',
36
+ 'npm start',
37
+ 'npm run build',
38
+ 'npx ',
39
+ 'yarn ',
40
+ 'pnpm ',
41
+ 'bun ',
42
+ 'node ',
43
+ 'deno ',
44
+ 'python ',
45
+ 'cargo ',
46
+ 'go run',
47
+ 'go test',
48
+ 'make ',
49
+ 'gcc ',
50
+ 'g++ ',
51
+ 'javac ',
52
+ 'mvn ',
53
+ 'gradle ',
54
+ ];
55
+ /**
56
+ * File extensions that indicate source code (PM should not modify these).
57
+ */
58
+ export const CODE_EXTENSIONS = [
59
+ '.ts',
60
+ '.tsx',
61
+ '.js',
62
+ '.jsx',
63
+ '.mjs',
64
+ '.cjs',
65
+ '.py',
66
+ '.rs',
67
+ '.go',
68
+ '.java',
69
+ '.c',
70
+ '.cpp',
71
+ '.h',
72
+ '.hpp',
73
+ '.cs',
74
+ '.rb',
75
+ '.php',
76
+ '.swift',
77
+ '.kt',
78
+ '.vue',
79
+ '.svelte',
80
+ ];
81
+ /**
82
+ * Harmonia MCP tool names — these are always allowed since PM uses them
83
+ * through Harmonia's own tool system.
84
+ */
85
+ export const HARMONIA_TOOLS = [
86
+ 'project_init',
87
+ 'project_set_scale',
88
+ 'project_status',
89
+ 'phase_update',
90
+ 'role_dispatch',
91
+ 'dispatch_report',
92
+ 'doc_write',
93
+ 'doc_read',
94
+ 'doc_list',
95
+ 'doc_approve',
96
+ 'reject_doc',
97
+ 'guard_set',
98
+ 'guard_get',
99
+ 'review_set_rule',
100
+ 'review_list',
101
+ ];
102
+ // ─── Timeout thresholds (minutes) ───
103
+ /** Dispatch running timeout — warn after this many minutes */
104
+ export const DISPATCH_TIMEOUT_MINUTES = 30;
105
+ /** Phase idle timeout — warn after this many minutes with no tool calls */
106
+ export const PHASE_IDLE_TIMEOUT_MINUTES = 15;
107
+ /** Review pending timeout — warn after this many minutes */
108
+ export const REVIEW_PENDING_TIMEOUT_MINUTES = 10;
109
+ //# sourceMappingURL=content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/hooks/content.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAWH,yBAAyB;AAEzB;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;IACzB,qBAAqB;IACrB,OAAO;IACP,MAAM;IACN,WAAW;IACX,OAAO;IACP,MAAM;IACN,mBAAmB;IACnB,MAAM;IACN,MAAM;IACN,UAAU;IACV,UAAU;CACJ,CAAC;AAEX;;GAEG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC5B,SAAS;IACT,UAAU;IACV,WAAW;IACX,eAAe;IACf,MAAM;IACN,OAAO;IACP,OAAO;IACP,MAAM;IACN,OAAO;IACP,OAAO;IACP,SAAS;IACT,QAAQ;IACR,QAAQ;IACR,SAAS;IACT,OAAO;IACP,MAAM;IACN,MAAM;IACN,QAAQ;IACR,MAAM;IACN,SAAS;CACH,CAAC;AAEX;;GAEG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC3B,KAAK;IACL,MAAM;IACN,KAAK;IACL,MAAM;IACN,MAAM;IACN,MAAM;IACN,KAAK;IACL,KAAK;IACL,KAAK;IACL,OAAO;IACP,IAAI;IACJ,MAAM;IACN,IAAI;IACJ,MAAM;IACN,KAAK;IACL,KAAK;IACL,MAAM;IACN,QAAQ;IACR,KAAK;IACL,MAAM;IACN,SAAS;CACH,CAAC;AAEX;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC1B,cAAc;IACd,mBAAmB;IACnB,gBAAgB;IAChB,cAAc;IACd,eAAe;IACf,iBAAiB;IACjB,WAAW;IACX,UAAU;IACV,UAAU;IACV,aAAa;IACb,YAAY;IACZ,WAAW;IACX,WAAW;IACX,iBAAiB;IACjB,aAAa;CACP,CAAC;AAEX,uCAAuC;AAEvC,8DAA8D;AAC9D,MAAM,CAAC,MAAM,wBAAwB,GAAG,EAAE,CAAC;AAE3C,2EAA2E;AAC3E,MAAM,CAAC,MAAM,0BAA0B,GAAG,EAAE,CAAC;AAE7C,4DAA4D;AAC5D,MAAM,CAAC,MAAM,8BAA8B,GAAG,EAAE,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Hook installation — entry point for installing/uninstalling agent hooks.
3
+ *
4
+ * Detects the host agent type and installs the appropriate hook definitions
5
+ * using agent-kit's installHooks API. Hook content is generated with baked-in
6
+ * data directory path. Hooks are project-agnostic.
7
+ */
8
+ import { type AgentType, type HookInstallResult } from '@s_s/agent-kit';
9
+ import type { HookParams } from './content.js';
10
+ /**
11
+ * Install Harmonia hooks for the detected agent.
12
+ *
13
+ * Generates agent-specific hook content with baked-in data directory,
14
+ * then delegates to agent-kit for file writing and config merging.
15
+ *
16
+ * @param agentType - The host agent type (detected or user-specified)
17
+ * @param params - Parameters to bake into hook content (dataDir only)
18
+ * @returns Installation result from agent-kit
19
+ */
20
+ export declare function installHooks(agentType: AgentType, params: HookParams): Promise<HookInstallResult>;
21
+ /**
22
+ * Uninstall Harmonia hooks for the given agent.
23
+ *
24
+ * Removes hook files and config entries installed by Harmonia.
25
+ */
26
+ export declare function uninstallHooks(agentType: AgentType): Promise<{
27
+ success: boolean;
28
+ removed: string[];
29
+ error?: string;
30
+ }>;
31
+ /**
32
+ * Check if Harmonia hooks are installed for the given agent.
33
+ */
34
+ export declare function hasHooksInstalled(agentType: AgentType): Promise<boolean>;
35
+ /**
36
+ * Create hook definitions for a specific agent type.
37
+ *
38
+ * @internal — exposed for testing
39
+ */
40
+ export declare function createHooksForAgent(agentType: AgentType, params: HookParams): import("@s_s/agent-kit").HookSet<"claude-code"> | import("@s_s/agent-kit").HookSet<"opencode"> | import("@s_s/agent-kit").HookSet<"openclaw">;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Hook installation — entry point for installing/uninstalling agent hooks.
3
+ *
4
+ * Detects the host agent type and installs the appropriate hook definitions
5
+ * using agent-kit's installHooks API. Hook content is generated with baked-in
6
+ * data directory path. Hooks are project-agnostic.
7
+ */
8
+ import { createKit } from '@s_s/agent-kit';
9
+ import { createClaudeCodeHooks } from './claude-code.js';
10
+ import { createOpenCodeHooks } from './opencode.js';
11
+ import { createOpenClawHooks } from './openclaw.js';
12
+ /** Shared kit instance for hook management */
13
+ const kit = createKit('harmonia');
14
+ /**
15
+ * Install Harmonia hooks for the detected agent.
16
+ *
17
+ * Generates agent-specific hook content with baked-in data directory,
18
+ * then delegates to agent-kit for file writing and config merging.
19
+ *
20
+ * @param agentType - The host agent type (detected or user-specified)
21
+ * @param params - Parameters to bake into hook content (dataDir only)
22
+ * @returns Installation result from agent-kit
23
+ */
24
+ export async function installHooks(agentType, params) {
25
+ const hooks = createHooksForAgent(agentType, params);
26
+ return kit.installHooks(agentType, hooks);
27
+ }
28
+ /**
29
+ * Uninstall Harmonia hooks for the given agent.
30
+ *
31
+ * Removes hook files and config entries installed by Harmonia.
32
+ */
33
+ export async function uninstallHooks(agentType) {
34
+ return kit.uninstallHooks(agentType);
35
+ }
36
+ /**
37
+ * Check if Harmonia hooks are installed for the given agent.
38
+ */
39
+ export async function hasHooksInstalled(agentType) {
40
+ return kit.hasHooksInstalled(agentType);
41
+ }
42
+ /**
43
+ * Create hook definitions for a specific agent type.
44
+ *
45
+ * @internal — exposed for testing
46
+ */
47
+ export function createHooksForAgent(agentType, params) {
48
+ switch (agentType) {
49
+ case 'claude-code':
50
+ case 'codex':
51
+ return createClaudeCodeHooks(params);
52
+ case 'opencode':
53
+ return createOpenCodeHooks(params);
54
+ case 'openclaw':
55
+ return createOpenClawHooks(params);
56
+ default: {
57
+ // Exhaustive check — should never reach here
58
+ const _exhaustive = agentType;
59
+ throw new Error(`Unsupported agent type: ${_exhaustive}`);
60
+ }
61
+ }
62
+ }
63
+ //# sourceMappingURL=install.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/hooks/install.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,SAAS,EAA0C,MAAM,gBAAgB,CAAC;AAEnF,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEpD,8CAA8C;AAC9C,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;AAElC;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,SAAoB,EAAE,MAAkB;IACvE,MAAM,KAAK,GAAG,mBAAmB,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACrD,OAAO,GAAG,CAAC,YAAY,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;AAC9C,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAChC,SAAoB;IAEpB,OAAO,GAAG,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;AACzC,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,SAAoB;IACxD,OAAO,GAAG,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;AAC5C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,SAAoB,EAAE,MAAkB;IACxE,QAAQ,SAAS,EAAE,CAAC;QAChB,KAAK,aAAa,CAAC;QACnB,KAAK,OAAO;YACR,OAAO,qBAAqB,CAAC,MAAM,CAAC,CAAC;QACzC,KAAK,UAAU;YACX,OAAO,mBAAmB,CAAC,MAAM,CAAC,CAAC;QACvC,KAAK,UAAU;YACX,OAAO,mBAAmB,CAAC,MAAM,CAAC,CAAC;QACvC,OAAO,CAAC,CAAC,CAAC;YACN,6CAA6C;YAC7C,MAAM,WAAW,GAAU,SAAS,CAAC;YACrC,MAAM,IAAI,KAAK,CAAC,2BAA2B,WAAW,EAAE,CAAC,CAAC;QAC9D,CAAC;IACL,CAAC;AACL,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * OpenClaw hook definitions.
3
+ *
4
+ * OpenClaw hooks use HOOK.md (YAML frontmatter) + handler.ts.
5
+ * agent-kit generates HOOK.md from events/description, user provides handler.ts content.
6
+ *
7
+ * Key capabilities:
8
+ * - `before_tool_call` — can BLOCK tool calls (return { block: true, reason })
9
+ * - `message_received` — fires on each user message (return { inject: '...' } for reminders)
10
+ *
11
+ * OpenClaw only supports a single HookDefinition (array takes first only),
12
+ * so we combine both events into one handler.
13
+ *
14
+ * Project-agnostic: no project name/dir baked in.
15
+ * - Boundary guard uses tool names + code file extensions only
16
+ * - Reminders scan all projects under DATA_DIR
17
+ */
18
+ import type { HookParams } from './content.js';
19
+ /**
20
+ * Create OpenClaw hook definitions using agent-kit's defineHooks.
21
+ *
22
+ * Single definition with two events (OpenClaw only supports one HookDefinition).
23
+ */
24
+ export declare function createOpenClawHooks(params: HookParams): import("@s_s/agent-kit").HookSet<"openclaw">;
@@ -0,0 +1,219 @@
1
+ /**
2
+ * OpenClaw hook definitions.
3
+ *
4
+ * OpenClaw hooks use HOOK.md (YAML frontmatter) + handler.ts.
5
+ * agent-kit generates HOOK.md from events/description, user provides handler.ts content.
6
+ *
7
+ * Key capabilities:
8
+ * - `before_tool_call` — can BLOCK tool calls (return { block: true, reason })
9
+ * - `message_received` — fires on each user message (return { inject: '...' } for reminders)
10
+ *
11
+ * OpenClaw only supports a single HookDefinition (array takes first only),
12
+ * so we combine both events into one handler.
13
+ *
14
+ * Project-agnostic: no project name/dir baked in.
15
+ * - Boundary guard uses tool names + code file extensions only
16
+ * - Reminders scan all projects under DATA_DIR
17
+ */
18
+ import { defineHooks } from '@s_s/agent-kit';
19
+ import { BLOCKED_COMMANDS, CODE_EXTENSIONS, DISPATCH_TIMEOUT_MINUTES, PHASE_IDLE_TIMEOUT_MINUTES, REVIEW_PENDING_TIMEOUT_MINUTES, } from './content.js';
20
+ /**
21
+ * Generate the OpenClaw handler.ts source code.
22
+ *
23
+ * Handles two events:
24
+ * 1. before_tool_call — boundary guard: block code edits and dev commands
25
+ * 2. message_received — proactive reminders: dispatch timeout, idle phase, pending reviews
26
+ */
27
+ function generateOpenClawHandler(params) {
28
+ const codeExtsJson = JSON.stringify(CODE_EXTENSIONS);
29
+ const blockedCmdsJson = JSON.stringify(BLOCKED_COMMANDS);
30
+ return `import { readFileSync, readdirSync, statSync } from 'fs';
31
+ import { resolve, join } from 'path';
32
+
33
+ // ── Baked-in constants (generated by Harmonia setup) ──
34
+ const DATA_DIR = ${JSON.stringify(params.dataDir)};
35
+
36
+ const CODE_EXTENSIONS: readonly string[] = ${codeExtsJson};
37
+ const BLOCKED_COMMANDS: readonly string[] = ${blockedCmdsJson};
38
+ const DISPATCH_TIMEOUT_MINUTES = ${DISPATCH_TIMEOUT_MINUTES};
39
+ const PHASE_IDLE_TIMEOUT_MINUTES = ${PHASE_IDLE_TIMEOUT_MINUTES};
40
+ const REVIEW_PENDING_TIMEOUT_MINUTES = ${REVIEW_PENDING_TIMEOUT_MINUTES};
41
+
42
+ // ── Helpers ──
43
+
44
+ function isCodeFile(filePath: string): boolean {
45
+ return CODE_EXTENSIONS.some((ext) => filePath.endsWith(ext));
46
+ }
47
+
48
+ function hasBlockedCommand(command: string): string | null {
49
+ for (const cmd of BLOCKED_COMMANDS) {
50
+ if (command.includes(cmd)) return cmd;
51
+ }
52
+ return null;
53
+ }
54
+
55
+ function readJsonSafe(filePath: string): any {
56
+ try {
57
+ return JSON.parse(readFileSync(filePath, 'utf-8'));
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ function minutesSince(isoDate: string): number {
64
+ const then = new Date(isoDate).getTime();
65
+ if (isNaN(then)) return 0;
66
+ return Math.floor((Date.now() - then) / 60000);
67
+ }
68
+
69
+ /** List project directories under DATA_DIR */
70
+ function listProjectDirs(): Array<{ name: string; path: string }> {
71
+ try {
72
+ const entries = readdirSync(DATA_DIR);
73
+ const projects: Array<{ name: string; path: string }> = [];
74
+ for (const entry of entries) {
75
+ const full = join(DATA_DIR, entry);
76
+ try {
77
+ if (statSync(full).isDirectory()) {
78
+ projects.push({ name: entry, path: full });
79
+ }
80
+ } catch {
81
+ // skip inaccessible entries
82
+ }
83
+ }
84
+ return projects;
85
+ } catch {
86
+ return [];
87
+ }
88
+ }
89
+
90
+ // ── Boundary guard (before_tool_call) ──
91
+
92
+ function handleBeforeToolCall(event: any): any {
93
+ const toolName = event.tool_name || event.toolName || '';
94
+ const toolInput = event.tool_input || event.toolInput || {};
95
+
96
+ // Guard 1: Write/Edit tools — check file extension
97
+ if (['Write', 'Edit', 'MultiEdit', 'write', 'edit'].includes(toolName)) {
98
+ const filePath = toolInput.file_path || toolInput.filePath || toolInput.path || '';
99
+ if (filePath && isCodeFile(filePath)) {
100
+ return {
101
+ block: true,
102
+ reason: \`PM 不应直接修改代码文件。请通过 role_dispatch 将编码任务分配给 developer。文件: \${filePath}\`,
103
+ };
104
+ }
105
+ }
106
+
107
+ // Guard 2: Bash/Terminal — check for dev commands
108
+ if (['Bash', 'bash', 'Terminal', 'terminal'].includes(toolName)) {
109
+ const command = toolInput.command || toolInput.cmd || '';
110
+ if (command) {
111
+ const blocked = hasBlockedCommand(command);
112
+ if (blocked) {
113
+ return {
114
+ block: true,
115
+ reason: \`PM 不应直接执行开发命令 (\${blocked}...)。请通过 role_dispatch 将任务分配给相应角色(developer/tester)。\`,
116
+ };
117
+ }
118
+ }
119
+ }
120
+
121
+ return event;
122
+ }
123
+
124
+ // ── Proactive reminders (message_received) ──
125
+
126
+ function handleMessageReceived(): any {
127
+ const reminders: string[] = [];
128
+ const projects = listProjectDirs();
129
+
130
+ for (const proj of projects) {
131
+ // Check 1: Running dispatch timeout
132
+ const dispatches = readJsonSafe(resolve(proj.path, 'dispatches.json'));
133
+ if (dispatches && Array.isArray(dispatches)) {
134
+ for (const d of dispatches) {
135
+ if ((d.status === 'running' || d.status === 'dispatched') && d.updatedAt) {
136
+ const elapsed = minutesSince(d.updatedAt);
137
+ if (elapsed >= DISPATCH_TIMEOUT_MINUTES) {
138
+ reminders.push(
139
+ \`- [\${proj.name}] dispatch \${d.id} (\${d.role}) 已运行 \${elapsed} 分钟,建议调用 project_status 检查进度\`,
140
+ );
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ // Check 2: Pending document reviews
147
+ const reviews = readJsonSafe(resolve(proj.path, 'reviews.json'));
148
+ if (reviews && typeof reviews === 'object') {
149
+ const pendingDocs: string[] = [];
150
+ for (const [docId, review] of Object.entries<any>(reviews)) {
151
+ if (review.status === 'pending' && review.submittedAt) {
152
+ const elapsed = minutesSince(review.submittedAt);
153
+ if (elapsed >= REVIEW_PENDING_TIMEOUT_MINUTES) {
154
+ pendingDocs.push(docId);
155
+ }
156
+ }
157
+ }
158
+ if (pendingDocs.length > 0) {
159
+ reminders.push(
160
+ \`- [\${proj.name}] \${pendingDocs.length} 份文档待审核超过 \${REVIEW_PENDING_TIMEOUT_MINUTES} 分钟: \${pendingDocs.join(', ')} — 请尽快处理(doc_approve / reject_doc)\`,
161
+ );
162
+ }
163
+ }
164
+
165
+ // Check 3: Phase idle check
166
+ const state = readJsonSafe(resolve(proj.path, 'state.json'));
167
+ if (state && state.updatedAt && state.currentPhase) {
168
+ const idle = minutesSince(state.updatedAt);
169
+ if (idle >= PHASE_IDLE_TIMEOUT_MINUTES) {
170
+ reminders.push(
171
+ \`- [\${proj.name}] 当前阶段 (\${state.currentPhase}) 已空闲 \${idle} 分钟,建议调用 project_status 检查项目状态\`,
172
+ );
173
+ }
174
+ }
175
+ }
176
+
177
+ if (reminders.length > 0) {
178
+ return {
179
+ inject: \`<harmonia-reminder>
180
+ 以下事项需要你的关注:
181
+
182
+ \${reminders.join('\\n')}
183
+
184
+ 请根据提醒采取相应行动。
185
+ </harmonia-reminder>\`,
186
+ };
187
+ }
188
+
189
+ return {};
190
+ }
191
+
192
+ // ── Main handler ──
193
+
194
+ export default async function handler(event: any) {
195
+ if (event.type === 'before_tool_call') {
196
+ return handleBeforeToolCall(event);
197
+ }
198
+
199
+ if (event.type === 'message_received') {
200
+ return handleMessageReceived();
201
+ }
202
+
203
+ return event;
204
+ }
205
+ `;
206
+ }
207
+ /**
208
+ * Create OpenClaw hook definitions using agent-kit's defineHooks.
209
+ *
210
+ * Single definition with two events (OpenClaw only supports one HookDefinition).
211
+ */
212
+ export function createOpenClawHooks(params) {
213
+ return defineHooks('openclaw', {
214
+ events: ['message_received', 'before_tool_call'],
215
+ content: generateOpenClawHandler(params),
216
+ description: 'Harmonia PM 边界守卫与状态提醒',
217
+ });
218
+ }
219
+ //# sourceMappingURL=openclaw.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openclaw.js","sourceRoot":"","sources":["../../src/hooks/openclaw.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,OAAO,EACH,gBAAgB,EAChB,eAAe,EACf,wBAAwB,EACxB,0BAA0B,EAC1B,8BAA8B,GACjC,MAAM,cAAc,CAAC;AAEtB;;;;;;GAMG;AACH,SAAS,uBAAuB,CAAC,MAAkB;IAC/C,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;IACrD,MAAM,eAAe,GAAG,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC;IAEzD,OAAO;;;;mBAIQ,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC;;6CAEJ,YAAY;8CACX,eAAe;mCAC1B,wBAAwB;qCACtB,0BAA0B;yCACtB,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqKtE,CAAC;AACF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAkB;IAClD,OAAO,WAAW,CAAC,UAAU,EAAE;QAC3B,MAAM,EAAE,CAAC,kBAAkB,EAAE,kBAAkB,CAAC;QAChD,OAAO,EAAE,uBAAuB,CAAC,MAAM,CAAC;QACxC,WAAW,EAAE,uBAAuB;KACvC,CAAC,CAAC;AACP,CAAC"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * OpenCode hook definitions.
3
+ *
4
+ * OpenCode hooks are TypeScript plugins installed to ~/.config/opencode/plugins/.
5
+ * They export a Plugin object with hooks keyed by event name.
6
+ *
7
+ * Limitations vs Claude Code:
8
+ * - `tool.execute.before` can modify args but CANNOT block tool calls.
9
+ * - No true blocking mechanism (permission.ask is too coarse).
10
+ *
11
+ * Strategy: "soft interception + reminder injection"
12
+ * 1. tool.execute.before — detect boundary violations, replace args to neutralize
13
+ * the operation (e.g., prepend echo to bash commands, empty write content)
14
+ * and inject a warning message.
15
+ * 2. experimental.chat.messages.transform — read Harmonia data files, inject
16
+ * reminders about dispatch timeouts, idle phases, pending reviews.
17
+ *
18
+ * Project-agnostic: no project name/dir baked in.
19
+ * - Boundary guard uses tool names + code file extensions only
20
+ * - Reminders scan all projects under DATA_DIR
21
+ */
22
+ import type { HookParams } from './content.js';
23
+ /**
24
+ * Create OpenCode hook definitions using agent-kit's defineHooks.
25
+ *
26
+ * Produces a single plugin file with both tool.execute.before and
27
+ * experimental.chat.messages.transform hooks.
28
+ */
29
+ export declare function createOpenCodeHooks(params: HookParams): import("@s_s/agent-kit").HookSet<"opencode">;