forge-workflow 0.0.3 → 0.0.5

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 (209) hide show
  1. package/.claude/commands/dev.md +340 -314
  2. package/.claude/commands/plan.md +521 -478
  3. package/.claude/commands/premerge.md +176 -179
  4. package/.claude/commands/research.md +42 -42
  5. package/.claude/commands/review.md +442 -442
  6. package/.claude/commands/rollback.md +721 -721
  7. package/.claude/commands/ship.md +164 -134
  8. package/.claude/commands/sonarcloud.md +152 -152
  9. package/.claude/commands/status.md +48 -77
  10. package/.claude/commands/validate.md +282 -237
  11. package/.claude/commands/verify.md +221 -221
  12. package/.claude/rules/greptile-review-process.md +285 -285
  13. package/.claude/rules/workflow.md +105 -105
  14. package/.claude/scripts/greptile-resolve.sh +526 -526
  15. package/.claude/scripts/load-env.sh +32 -32
  16. package/.cline/workflows/dev.md +337 -311
  17. package/.cline/workflows/plan.md +518 -475
  18. package/.cline/workflows/premerge.md +173 -176
  19. package/.cline/workflows/research.md +39 -39
  20. package/.cline/workflows/review.md +439 -439
  21. package/.cline/workflows/rollback.md +718 -718
  22. package/.cline/workflows/ship.md +161 -131
  23. package/.cline/workflows/sonarcloud.md +146 -146
  24. package/.cline/workflows/status.md +45 -74
  25. package/.cline/workflows/validate.md +279 -234
  26. package/.cline/workflows/verify.md +218 -218
  27. package/.codex/config.toml +11 -11
  28. package/.codex/skills/dev/SKILL.md +340 -314
  29. package/.codex/skills/plan/SKILL.md +521 -478
  30. package/.codex/skills/premerge/SKILL.md +176 -179
  31. package/.codex/skills/research/SKILL.md +42 -42
  32. package/.codex/skills/review/SKILL.md +442 -442
  33. package/.codex/skills/rollback/SKILL.md +721 -721
  34. package/.codex/skills/ship/SKILL.md +164 -134
  35. package/.codex/skills/sonarcloud/SKILL.md +149 -149
  36. package/.codex/skills/status/SKILL.md +48 -77
  37. package/.codex/skills/validate/SKILL.md +282 -237
  38. package/.codex/skills/verify/SKILL.md +221 -221
  39. package/.cursor/commands/dev.md +337 -311
  40. package/.cursor/commands/plan.md +518 -475
  41. package/.cursor/commands/premerge.md +173 -176
  42. package/.cursor/commands/research.md +39 -39
  43. package/.cursor/commands/review.md +439 -439
  44. package/.cursor/commands/rollback.md +718 -718
  45. package/.cursor/commands/ship.md +161 -131
  46. package/.cursor/commands/sonarcloud.md +146 -146
  47. package/.cursor/commands/status.md +45 -74
  48. package/.cursor/commands/validate.md +279 -234
  49. package/.cursor/commands/verify.md +218 -218
  50. package/.cursor/rules/permissions-guidance.mdc +37 -37
  51. package/.forge/hooks/check-tdd.js +240 -240
  52. package/.github/PLUGIN_TEMPLATE.json +32 -32
  53. package/.github/prompts/dev.prompt.md +342 -316
  54. package/.github/prompts/plan.prompt.md +523 -480
  55. package/.github/prompts/premerge.prompt.md +178 -181
  56. package/.github/prompts/research.prompt.md +44 -44
  57. package/.github/prompts/review.prompt.md +444 -444
  58. package/.github/prompts/rollback.prompt.md +723 -723
  59. package/.github/prompts/ship.prompt.md +166 -136
  60. package/.github/prompts/sonarcloud.prompt.md +151 -151
  61. package/.github/prompts/status.prompt.md +50 -79
  62. package/.github/prompts/validate.prompt.md +284 -239
  63. package/.github/prompts/verify.prompt.md +223 -223
  64. package/.github/workflows/beads-to-github.yml +56 -0
  65. package/.github/workflows/github-to-beads.yml +97 -0
  66. package/.kilocode/workflows/dev.md +341 -315
  67. package/.kilocode/workflows/plan.md +522 -479
  68. package/.kilocode/workflows/premerge.md +177 -180
  69. package/.kilocode/workflows/research.md +43 -43
  70. package/.kilocode/workflows/review.md +443 -443
  71. package/.kilocode/workflows/rollback.md +722 -722
  72. package/.kilocode/workflows/ship.md +165 -135
  73. package/.kilocode/workflows/sonarcloud.md +150 -150
  74. package/.kilocode/workflows/status.md +49 -78
  75. package/.kilocode/workflows/validate.md +283 -238
  76. package/.kilocode/workflows/verify.md +222 -222
  77. package/.mcp.json.example +12 -12
  78. package/.opencode/commands/dev.md +340 -314
  79. package/.opencode/commands/plan.md +521 -478
  80. package/.opencode/commands/premerge.md +176 -179
  81. package/.opencode/commands/research.md +42 -42
  82. package/.opencode/commands/review.md +442 -442
  83. package/.opencode/commands/rollback.md +721 -721
  84. package/.opencode/commands/ship.md +164 -134
  85. package/.opencode/commands/sonarcloud.md +149 -149
  86. package/.opencode/commands/status.md +48 -77
  87. package/.opencode/commands/validate.md +282 -237
  88. package/.opencode/commands/verify.md +221 -221
  89. package/.roo/commands/dev.md +341 -315
  90. package/.roo/commands/plan.md +522 -479
  91. package/.roo/commands/premerge.md +177 -180
  92. package/.roo/commands/research.md +43 -43
  93. package/.roo/commands/review.md +443 -443
  94. package/.roo/commands/rollback.md +722 -722
  95. package/.roo/commands/ship.md +165 -135
  96. package/.roo/commands/sonarcloud.md +150 -150
  97. package/.roo/commands/status.md +49 -78
  98. package/.roo/commands/validate.md +283 -238
  99. package/.roo/commands/verify.md +222 -222
  100. package/AGENTS.md +175 -169
  101. package/CLAUDE.md +100 -99
  102. package/LICENSE +21 -21
  103. package/README.md +429 -414
  104. package/bin/forge-cmd.js +313 -313
  105. package/bin/{forge-validate.js → forge-preflight.js} +309 -303
  106. package/bin/forge.js +4596 -4232
  107. package/docs/AGENT_INSTALL_PROMPT.md +342 -342
  108. package/docs/BEADS_GITHUB_SYNC.md +251 -0
  109. package/docs/ENHANCED_ONBOARDING.md +602 -602
  110. package/docs/EXAMPLES.md +482 -482
  111. package/docs/GREPTILE_SETUP.md +400 -400
  112. package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
  113. package/docs/ROADMAP.md +359 -359
  114. package/docs/SETUP.md +663 -632
  115. package/docs/TOOLCHAIN.md +630 -630
  116. package/docs/VALIDATION.md +363 -363
  117. package/install.sh +40 -1058
  118. package/lefthook.yml +39 -39
  119. package/lib/agents/README.md +198 -198
  120. package/lib/agents/claude.plugin.json +28 -28
  121. package/lib/agents/cline.plugin.json +22 -22
  122. package/lib/agents/codex.plugin.json +19 -19
  123. package/lib/agents/copilot.plugin.json +24 -24
  124. package/lib/agents/cursor.plugin.json +25 -25
  125. package/lib/agents/kilocode.plugin.json +22 -22
  126. package/lib/agents/opencode.plugin.json +20 -20
  127. package/lib/agents/roo.plugin.json +23 -23
  128. package/lib/agents-config.js +2112 -2112
  129. package/lib/beads-health-check.js +143 -0
  130. package/lib/beads-setup.js +341 -0
  131. package/lib/beads-sync-scaffold.js +260 -0
  132. package/lib/commands/dev.js +513 -513
  133. package/lib/commands/plan.js +692 -692
  134. package/lib/commands/recommend.js +119 -119
  135. package/lib/commands/ship.js +377 -377
  136. package/lib/commands/status.js +378 -378
  137. package/lib/commands/validate.js +602 -602
  138. package/lib/context-merge.js +359 -359
  139. package/lib/dep-guard/analyzer.js +294 -294
  140. package/lib/dep-guard/behavior-detector.js +98 -98
  141. package/lib/dep-guard/contract-detector.js +162 -162
  142. package/lib/dep-guard/import-detector.js +498 -498
  143. package/lib/dep-guard/path-utils.js +13 -13
  144. package/lib/dep-guard/rubric.js +120 -120
  145. package/lib/dep-guard/task-parser.js +318 -318
  146. package/lib/detect-agent.js +191 -0
  147. package/lib/detect-worktree.js +47 -0
  148. package/lib/file-hash.js +26 -0
  149. package/lib/husky-migration.js +450 -0
  150. package/lib/lefthook-check.js +65 -0
  151. package/lib/pat-setup.js +207 -0
  152. package/lib/plugin-catalog.js +350 -350
  153. package/lib/plugin-manager.js +166 -166
  154. package/lib/plugin-recommender.js +141 -141
  155. package/lib/project-discovery.js +491 -491
  156. package/lib/setup-action-log.js +139 -0
  157. package/lib/setup-summary-renderer.js +106 -0
  158. package/lib/setup-utils.js +96 -0
  159. package/lib/setup.js +192 -118
  160. package/lib/smart-merge.js +64 -0
  161. package/lib/symlink-utils.js +81 -0
  162. package/lib/workflow-profiles.js +197 -197
  163. package/package.json +131 -129
  164. package/scripts/beads-context.sh +291 -0
  165. package/scripts/beads-context.test.js +563 -0
  166. package/scripts/behavioral-judge.sh +378 -0
  167. package/scripts/benchmark.js +85 -0
  168. package/scripts/branch-protection.js +183 -0
  169. package/scripts/check-agents.js +172 -0
  170. package/scripts/commitlint.js +42 -0
  171. package/scripts/conflict-detect.sh +323 -0
  172. package/scripts/dep-guard-analyze.js +71 -0
  173. package/scripts/dep-guard.sh +811 -0
  174. package/scripts/eval_win.py +249 -0
  175. package/scripts/file-index.sh +399 -0
  176. package/scripts/github-beads-sync/comment.mjs +64 -0
  177. package/scripts/github-beads-sync/config.mjs +148 -0
  178. package/scripts/github-beads-sync/github-api.mjs +131 -0
  179. package/scripts/github-beads-sync/index.mjs +332 -0
  180. package/scripts/github-beads-sync/label-mapper.mjs +54 -0
  181. package/scripts/github-beads-sync/mapping.mjs +78 -0
  182. package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
  183. package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
  184. package/scripts/github-beads-sync/run-bd.mjs +159 -0
  185. package/scripts/github-beads-sync/sanitize.mjs +121 -0
  186. package/scripts/github-beads-sync.config.json +26 -0
  187. package/scripts/improve-command.js +375 -0
  188. package/scripts/lib/eval-runner.js +229 -0
  189. package/scripts/lib/eval-schema.js +135 -0
  190. package/scripts/lib/eval-storage.js +78 -0
  191. package/scripts/lib/grading.js +203 -0
  192. package/scripts/lib/transcript-parser.js +63 -0
  193. package/scripts/lint.js +47 -0
  194. package/scripts/migrate-to-bun-test.js +412 -0
  195. package/scripts/run-command-eval.js +236 -0
  196. package/scripts/smart-status.sh +782 -0
  197. package/scripts/sync-commands.js +571 -0
  198. package/scripts/sync-utils.sh +460 -0
  199. package/scripts/test-dashboard.js +123 -0
  200. package/scripts/test.js +44 -0
  201. package/scripts/validate.sh +94 -0
  202. package/skills/parallel-deep-research/SKILL.md +108 -108
  203. package/skills/parallel-deep-research/evals/README.md +27 -27
  204. package/skills/parallel-deep-research/evals/evals.json +62 -62
  205. package/skills/sonarcloud-analysis/SKILL.md +171 -171
  206. package/skills/sonarcloud-analysis/evals/README.md +27 -27
  207. package/skills/sonarcloud-analysis/evals/evals.json +50 -50
  208. package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
  209. package/docs/WORKFLOW.md +0 -400
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @module check-agents
5
+ *
6
+ * CLI command that validates all agent configs are complete and in sync.
7
+ *
8
+ * 1. Runs syncCommands({ check: true }) to verify agent command files match canonical source
9
+ * 2. Reads lib/agents/*.plugin.json to verify each agent with capabilities.commands: true
10
+ * has its command directory populated with expected files
11
+ * 3. Reports: missing files, out-of-sync files, stale files
12
+ * 4. Exits 0 if all clean, exits non-zero if issues found
13
+ *
14
+ * Usage:
15
+ * node scripts/check-agents.js
16
+ *
17
+ * Exports:
18
+ * checkAgents(repoRoot) -> { errors: string[], warnings: string[] }
19
+ */
20
+
21
+ const { syncCommands } = require('./sync-commands');
22
+ const path = require('path');
23
+ const fs = require('fs');
24
+
25
+ /**
26
+ * Validate all agent configs are complete and in sync.
27
+ *
28
+ * @param {string} repoRoot - Absolute path to the repository root
29
+ * @returns {{ errors: string[], warnings: string[] }}
30
+ */
31
+ function checkAgents(repoRoot) {
32
+ /** @type {string[]} */
33
+ const errors = [];
34
+ /** @type {string[]} */
35
+ const warnings = [];
36
+
37
+ // ---- 1. Run sync check ----
38
+ let syncResult;
39
+ try {
40
+ syncResult = syncCommands({ check: true, dryRun: false, repoRoot });
41
+ } catch (err) {
42
+ errors.push(`Sync check failed: ${err.message}`);
43
+ return { errors, warnings };
44
+ }
45
+
46
+ if (syncResult.empty) {
47
+ errors.push('No command files found in .claude/commands/ — cannot verify sync.');
48
+ return { errors, warnings };
49
+ } else {
50
+ if (syncResult.manifestMissing) {
51
+ warnings.push(
52
+ 'Sync manifest (.forge/sync-manifest.json) not found — stale file detection skipped. ' +
53
+ 'Run "node scripts/sync-commands.js" to generate it.'
54
+ );
55
+ }
56
+
57
+ if (syncResult.outOfSync && syncResult.outOfSync.length > 0) {
58
+ for (const entry of syncResult.outOfSync) {
59
+ const relPath = path.join(entry.dir, entry.filename);
60
+ const exists = fs.existsSync(entry.filePath);
61
+ const status = exists ? 'modified' : 'missing';
62
+ errors.push(`Out of sync [${entry.agent}]: ${relPath} (${status})`);
63
+ }
64
+ }
65
+
66
+ if (syncResult.staleFiles && syncResult.staleFiles.length > 0) {
67
+ for (const filePath of syncResult.staleFiles) {
68
+ const relPath = path.relative(repoRoot, filePath);
69
+ errors.push(`Stale file (no longer generated, should be removed): ${relPath}`);
70
+ }
71
+ }
72
+ }
73
+
74
+ // ---- 2. Validate plugin catalog ----
75
+ const pluginDir = path.join(repoRoot, 'lib', 'agents');
76
+ /** @type {object[]} */
77
+ const plugins = [];
78
+
79
+ if (!fs.existsSync(pluginDir)) {
80
+ warnings.push('No plugin directory found at lib/agents/ — plugin catalog validation skipped.');
81
+ } else {
82
+ const pluginFiles = fs.readdirSync(pluginDir).filter((f) => f.endsWith('.plugin.json'));
83
+
84
+ if (pluginFiles.length === 0) {
85
+ warnings.push('No plugin.json files found in lib/agents/ — plugin catalog validation skipped.');
86
+ }
87
+
88
+ for (const file of pluginFiles) {
89
+ try {
90
+ const plugin = JSON.parse(fs.readFileSync(path.join(pluginDir, file), 'utf8'));
91
+ plugins.push(plugin);
92
+ } catch (_err) {
93
+ errors.push(`Failed to parse plugin file: lib/agents/${file}`);
94
+ }
95
+ }
96
+
97
+ // For each plugin with commands: true, verify its command directory has files
98
+ for (const plugin of plugins) {
99
+ if (!plugin.capabilities || !plugin.capabilities.commands) {
100
+ continue;
101
+ }
102
+
103
+ // Find the command directory from the plugin's directories config
104
+ const dirs = plugin.directories || {};
105
+ // Plugins use different keys: commands, workflows, prompts, skills
106
+ const commandDir = dirs.commands || dirs.workflows || dirs.prompts;
107
+
108
+ if (!commandDir) {
109
+ // Plugin declares commands: true but has no command directory configured
110
+ // Codex is the only agent that uses skills as its command mechanism
111
+ if (plugin.id === 'codex' && dirs.skills) {
112
+ // Codex CLI: commands live in .codex/skills/<name>/SKILL.md — skip standard check
113
+ continue;
114
+ }
115
+ warnings.push(
116
+ `Plugin "${plugin.name}" (${plugin.id}) has commands: true but no command directory configured.`
117
+ );
118
+ continue;
119
+ }
120
+
121
+ const absCmdDir = path.join(repoRoot, commandDir);
122
+
123
+ if (!fs.existsSync(absCmdDir)) {
124
+ errors.push(
125
+ `Plugin "${plugin.name}" (${plugin.id}) has commands: true but directory "${commandDir}" does not exist.`
126
+ );
127
+ continue;
128
+ }
129
+
130
+ // Check the directory has at least one file
131
+ const files = fs.readdirSync(absCmdDir).filter((f) => !f.startsWith('.'));
132
+ if (files.length === 0) {
133
+ errors.push(
134
+ `Plugin "${plugin.name}" (${plugin.id}) has commands: true but directory "${commandDir}" is empty.`
135
+ );
136
+ }
137
+ }
138
+ }
139
+
140
+ return { errors, warnings };
141
+ }
142
+
143
+ // ---- CLI entry point ----
144
+
145
+ if (require.main === module) {
146
+ const repoRoot = path.resolve(__dirname, '..');
147
+ const result = checkAgents(repoRoot);
148
+
149
+ // Print warnings
150
+ for (const warning of result.warnings) {
151
+ console.warn(`Warning: ${warning}`);
152
+ }
153
+
154
+ // Print errors
155
+ for (const error of result.errors) {
156
+ console.error(`Error: ${error}`);
157
+ }
158
+
159
+ // Summary
160
+ if (result.errors.length === 0 && result.warnings.length === 0) {
161
+ console.log('All agent checks passed.');
162
+ process.exit(0);
163
+ } else if (result.errors.length === 0) {
164
+ console.log(`\nAll agent checks passed with ${result.warnings.length} warning(s).`);
165
+ process.exit(0);
166
+ } else {
167
+ console.error(`\n${result.errors.length} error(s), ${result.warnings.length} warning(s) found.`);
168
+ process.exit(1);
169
+ }
170
+ }
171
+
172
+ module.exports = { checkAgents };
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Cross-platform commitlint runner for lefthook commit-msg hook.
4
+ * Replaces direct npx invocation which fails on Windows without shell: true.
5
+ * Works on Windows CMD, PowerShell, macOS, Linux.
6
+ */
7
+
8
+ const { spawnSync } = require('node:child_process');
9
+
10
+ // On Windows, npx is npx.cmd — shell: true resolves .cmd extensions
11
+ const isWindows = process.platform === 'win32';
12
+
13
+ const commitMsgFile = process.argv[2];
14
+ if (!commitMsgFile) {
15
+ console.error('❌ No commit message file provided');
16
+ process.exit(1);
17
+ }
18
+
19
+ // Detect package manager: prefer bun (fast, works with bun.lock), fall back to npx
20
+ // npx fails on Windows when npm 11's arborist encounters bun.lock instead of package-lock.json
21
+ const hasBunLock = require('node:fs').existsSync(
22
+ require('node:path').join(__dirname, '../bun.lock')
23
+ );
24
+ const runner = hasBunLock ? 'bunx' : 'npx';
25
+
26
+ const result = spawnSync(
27
+ runner,
28
+ ['commitlint', '--edit', commitMsgFile],
29
+ { stdio: 'inherit', shell: isWindows }
30
+ );
31
+
32
+ if (result.error) {
33
+ console.error('');
34
+ console.error(`❌ Failed to run commitlint: ${result.error.message}`);
35
+ console.error(' Is Node.js/npm installed and on PATH?');
36
+ console.error('');
37
+ process.exit(1);
38
+ }
39
+
40
+ if (result.status !== 0) {
41
+ process.exit(result.status);
42
+ }
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env bash
2
+ # conflict-detect.sh — Detect file/module overlaps between in-progress issues.
3
+ #
4
+ # Usage:
5
+ # conflict-detect.sh --issue <id> Check overlaps for a given issue
6
+ # conflict-detect.sh --files <file1,file2,...> Check arbitrary files against index
7
+ # conflict-detect.sh --issue <id> --detail Drill down to file-level overlap
8
+ # conflict-detect.sh --files <f1,f2> --detail Drill down to file-level overlap
9
+ #
10
+ # Exit codes:
11
+ # 0 = no conflicts found
12
+ # 1 = conflicts found
13
+ # 2 = usage/validation error (malformed input)
14
+ #
15
+ # OWASP A03: All inputs validated; shell variables quoted; jq --arg used.
16
+
17
+ set -euo pipefail
18
+
19
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
+
21
+ # Source file-index helpers (file_index_read, file_index_get)
22
+ source "$SCRIPT_DIR/file-index.sh"
23
+
24
+ # Source sync-utils (get_session_identity, sanitize_config_value)
25
+ # Only source if available (tests may not need all utils)
26
+ if [[ -f "$SCRIPT_DIR/sync-utils.sh" ]]; then
27
+ source "$SCRIPT_DIR/sync-utils.sh"
28
+ fi
29
+
30
+ # ── Input Validation ─────────────────────────────────────────────────────
31
+
32
+ # Validate issue_id: alphanumeric + hyphens only
33
+ _validate_issue_id_arg() {
34
+ local id="$1"
35
+ if [[ ! "$id" =~ ^[a-zA-Z0-9-]+$ ]]; then
36
+ echo "Error: invalid issue_id format: must be alphanumeric + hyphens only" >&2
37
+ return 1
38
+ fi
39
+ }
40
+
41
+ # Validate a file path: no shell injection patterns
42
+ # Allows: alphanumeric, dots, hyphens, underscores, forward slashes
43
+ _validate_file_path() {
44
+ local path="$1"
45
+ if [[ ! "$path" =~ ^[a-zA-Z0-9./_@[:space:]-]+$ ]]; then
46
+ echo "Error: invalid file path — contains disallowed characters: $path" >&2
47
+ return 1
48
+ fi
49
+ }
50
+
51
+ # ── Stale Sync Check ─────────────────────────────────────────────────────
52
+
53
+ # Check if .beads/.last-sync is older than 15 minutes (900 seconds).
54
+ # Prints a warning to stderr if stale.
55
+ _check_stale_sync() {
56
+ local root="${FILE_INDEX_ROOT:-.}"
57
+ local last_sync_file="$root/.beads/.last-sync"
58
+
59
+ if [[ ! -f "$last_sync_file" ]]; then
60
+ # No sync file — skip warning (first run)
61
+ return 0
62
+ fi
63
+
64
+ local last_sync_ts
65
+ last_sync_ts="$(cat "$last_sync_file" 2>/dev/null | tr -d '[:space:]')"
66
+
67
+ if [[ -z "$last_sync_ts" ]]; then
68
+ return 0
69
+ fi
70
+
71
+ # .last-sync stores Unix epoch seconds (written by sync-utils.sh)
72
+ local last_epoch now_epoch
73
+ if [[ "$last_sync_ts" =~ ^[0-9]+$ ]]; then
74
+ last_epoch="$last_sync_ts"
75
+ else
76
+ # Fallback: try parsing as ISO 8601 (GNU date, then BSD date)
77
+ last_epoch="$(date -d "$last_sync_ts" +%s 2>/dev/null || date -jf "%Y-%m-%dT%H:%M:%SZ" "$last_sync_ts" +%s 2>/dev/null || echo 0)"
78
+ fi
79
+ now_epoch="$(date +%s)"
80
+
81
+ local diff=$(( now_epoch - last_epoch ))
82
+
83
+ if [[ "$diff" -gt 900 ]]; then
84
+ echo "WARNING: File index may be stale (last sync: ${last_sync_ts}, ${diff}s ago > 900s threshold)" >&2
85
+ fi
86
+ }
87
+
88
+ # ── Module Extraction ────────────────────────────────────────────────────
89
+
90
+ # Given a file path, derive its module directory (parent directory + /).
91
+ # e.g., "src/lib/status.ts" -> "src/lib/"
92
+ _derive_module() {
93
+ local filepath="$1"
94
+ local dir
95
+ dir="$(dirname "$filepath")"
96
+ # Ensure trailing slash
97
+ if [[ "$dir" == "." ]]; then
98
+ printf '%s' "./"
99
+ else
100
+ printf '%s/' "$dir"
101
+ fi
102
+ }
103
+
104
+ # ── Core Conflict Detection ──────────────────────────────────────────────
105
+
106
+ # Check for conflicts between a set of modules/files and all entries in the index.
107
+ # Arguments:
108
+ # $1 — JSON array of modules to check
109
+ # $2 — JSON array of files to check
110
+ # $3 — issue_id to exclude from comparison (empty string for --files mode)
111
+ # $4 — "true" or "false" for detail mode
112
+ # Outputs conflict report to stdout.
113
+ # Returns 0 if no conflicts, 1 if conflicts found.
114
+ _detect_conflicts() {
115
+ local check_modules="$1"
116
+ local check_files="$2"
117
+ local exclude_issue="$3"
118
+ local detail="$4"
119
+
120
+ # Read all active entries from file index, filter out stale entries (>48 hours)
121
+ local all_entries
122
+ local now_epoch
123
+ now_epoch="$(date +%s)"
124
+ all_entries="$(file_index_read 2>/dev/null | jq -c --arg now "$now_epoch" '
125
+ [.[] | select(
126
+ if .updated_at then
127
+ ((.updated_at[:19] + "Z") | fromdateiso8601) as $ts |
128
+ (($now | tonumber) - $ts) < 172800
129
+ else true end
130
+ )]
131
+ ' 2>/dev/null)" || all_entries="[]"
132
+
133
+ if [[ "$all_entries" == "[]" ]] || [[ -z "$all_entries" ]]; then
134
+ echo "No conflicts found. File index is empty."
135
+ return 0
136
+ fi
137
+
138
+ local conflicts_found=0
139
+ local output=""
140
+
141
+ # Iterate over each entry in the index
142
+ local entry_count
143
+ entry_count="$(printf '%s' "$all_entries" | jq '. | length')"
144
+
145
+ local i=0
146
+ while [[ "$i" -lt "$entry_count" ]]; do
147
+ local entry
148
+ entry="$(printf '%s' "$all_entries" | jq -c ".[$i]")"
149
+
150
+ local entry_issue_id
151
+ entry_issue_id="$(printf '%s' "$entry" | jq -r '.issue_id')"
152
+
153
+ # Skip the issue we're checking (don't conflict with yourself)
154
+ if [[ -n "$exclude_issue" ]] && [[ "$entry_issue_id" == "$exclude_issue" ]]; then
155
+ i=$((i + 1))
156
+ continue
157
+ fi
158
+
159
+ local entry_developer
160
+ entry_developer="$(printf '%s' "$entry" | jq -r '.developer')"
161
+
162
+ local entry_modules
163
+ entry_modules="$(printf '%s' "$entry" | jq -c '.modules')"
164
+
165
+ local entry_files
166
+ entry_files="$(printf '%s' "$entry" | jq -c '.files')"
167
+
168
+ # Check module overlap
169
+ local module_overlaps
170
+ module_overlaps="$(jq -n -c \
171
+ --argjson a "$check_modules" \
172
+ --argjson b "$entry_modules" \
173
+ '[$a[] as $m | $b[] | select(. == $m)] | unique')"
174
+
175
+ local module_overlap_count
176
+ module_overlap_count="$(printf '%s' "$module_overlaps" | jq '. | length')"
177
+
178
+ if [[ "$module_overlap_count" -gt 0 ]]; then
179
+ conflicts_found=1
180
+ local overlapping_modules_str
181
+ overlapping_modules_str="$(printf '%s' "$module_overlaps" | jq -r '.[]' | tr '\n' ', ' | sed 's/,$//')"
182
+
183
+ output+="CONFLICT: Issue ${entry_issue_id} (developer: ${entry_developer})"$'\n'
184
+ output+=" Overlapping modules: ${overlapping_modules_str}"$'\n'
185
+
186
+ # Detail mode: show file-level overlap
187
+ if [[ "$detail" == "true" ]]; then
188
+ local file_overlaps
189
+ file_overlaps="$(jq -n -c \
190
+ --argjson a "$check_files" \
191
+ --argjson b "$entry_files" \
192
+ '[$a[] as $f | $b[] | select(. == $f)] | unique')"
193
+
194
+ local file_overlap_count
195
+ file_overlap_count="$(printf '%s' "$file_overlaps" | jq '. | length')"
196
+
197
+ if [[ "$file_overlap_count" -gt 0 ]]; then
198
+ local overlapping_files_str
199
+ overlapping_files_str="$(printf '%s' "$file_overlaps" | jq -r '.[]' | tr '\n' ', ' | sed 's/,$//')"
200
+ output+=" Overlapping files: ${overlapping_files_str}"$'\n'
201
+ else
202
+ output+=" No exact file overlaps (module-level only)"$'\n'
203
+ fi
204
+ fi
205
+ fi
206
+
207
+ i=$((i + 1))
208
+ done
209
+
210
+ if [[ "$conflicts_found" -eq 1 ]]; then
211
+ printf '%s' "$output"
212
+ return 1
213
+ else
214
+ echo "No conflicts found."
215
+ return 0
216
+ fi
217
+ }
218
+
219
+ # ── Main ─────────────────────────────────────────────────────────────────
220
+
221
+ main() {
222
+ local mode="" # "issue" or "files"
223
+ local issue_id=""
224
+ local files_arg=""
225
+ local detail="false"
226
+
227
+ # Parse arguments
228
+ while [[ $# -gt 0 ]]; do
229
+ case "$1" in
230
+ --issue)
231
+ mode="issue"
232
+ shift
233
+ if [[ $# -eq 0 ]]; then
234
+ echo "Error: --issue requires an argument" >&2
235
+ exit 1
236
+ fi
237
+ issue_id="$1"
238
+ shift
239
+ ;;
240
+ --files)
241
+ mode="files"
242
+ shift
243
+ if [[ $# -eq 0 ]]; then
244
+ echo "Error: --files requires an argument" >&2
245
+ exit 1
246
+ fi
247
+ files_arg="$1"
248
+ shift
249
+ ;;
250
+ --detail)
251
+ detail="true"
252
+ shift
253
+ ;;
254
+ *)
255
+ echo "Error: unknown argument '$1'" >&2
256
+ echo "Usage: conflict-detect.sh --issue <id> [--detail]" >&2
257
+ echo " conflict-detect.sh --files <file1,file2,...> [--detail]" >&2
258
+ exit 1
259
+ ;;
260
+ esac
261
+ done
262
+
263
+ # Require at least one mode
264
+ if [[ -z "$mode" ]]; then
265
+ echo "Usage: conflict-detect.sh --issue <id> [--detail]" >&2
266
+ echo " conflict-detect.sh --files <file1,file2,...> [--detail]" >&2
267
+ exit 1
268
+ fi
269
+
270
+ # Check for stale sync
271
+ _check_stale_sync
272
+
273
+ if [[ "$mode" == "issue" ]]; then
274
+ # Validate issue_id
275
+ _validate_issue_id_arg "$issue_id" || exit 2
276
+
277
+ # Get the issue's entry from the file index
278
+ local entry
279
+ entry="$(file_index_get "$issue_id")"
280
+
281
+ if [[ "$entry" == "null" ]]; then
282
+ echo "Info: issue '$issue_id' not yet in file index — no conflicts possible." >&2
283
+ exit 0
284
+ fi
285
+
286
+ local check_modules check_files
287
+ check_modules="$(printf '%s' "$entry" | jq -c '.modules')"
288
+ check_files="$(printf '%s' "$entry" | jq -c '.files')"
289
+
290
+ _detect_conflicts "$check_modules" "$check_files" "$issue_id" "$detail"
291
+
292
+ elif [[ "$mode" == "files" ]]; then
293
+ # Parse comma-separated file list
294
+ local IFS=','
295
+ local -a file_list
296
+ read -ra file_list <<< "$files_arg"
297
+
298
+ # Validate each file path
299
+ local -a valid_files=()
300
+ local -a derived_modules=()
301
+ for f in "${file_list[@]}"; do
302
+ # Trim whitespace
303
+ f="$(printf '%s' "$f" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
304
+ _validate_file_path "$f" || exit 2
305
+ valid_files+=("$f")
306
+
307
+ # Derive module from file path
308
+ local mod
309
+ mod="$(_derive_module "$f")"
310
+ derived_modules+=("$mod")
311
+ done
312
+
313
+ # Build JSON arrays using jq
314
+ local files_json modules_json
315
+ files_json="$(printf '%s\n' "${valid_files[@]}" | jq -R '.' | jq -s -c '.')"
316
+ # Deduplicate modules
317
+ modules_json="$(printf '%s\n' "${derived_modules[@]}" | jq -R '.' | jq -s -c 'unique')"
318
+
319
+ _detect_conflicts "$modules_json" "$files_json" "" "$detail"
320
+ fi
321
+ }
322
+
323
+ main "$@"
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('node:fs');
4
+
5
+ const { analyzePhase3Dependencies } = require('../lib/dep-guard/analyzer.js');
6
+
7
+ function readJsonFile(filePath) {
8
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
9
+ }
10
+
11
+ function normalizeListPayload(payload) {
12
+ if (Array.isArray(payload)) {
13
+ return payload;
14
+ }
15
+
16
+ if (payload && typeof payload === 'object') {
17
+ return [payload];
18
+ }
19
+
20
+ return [];
21
+ }
22
+
23
+ function normalizeSingleIssuePayload(payload) {
24
+ if (Array.isArray(payload)) {
25
+ return payload[0] ?? {};
26
+ }
27
+
28
+ if (payload && typeof payload === 'object') {
29
+ return payload;
30
+ }
31
+
32
+ return {};
33
+ }
34
+
35
+ function main() {
36
+ const [
37
+ currentIssueFile,
38
+ openIssuesFile,
39
+ inProgressIssuesFile,
40
+ taskFile,
41
+ repositoryRoot = process.cwd(),
42
+ ] = process.argv.slice(2);
43
+
44
+ if (!currentIssueFile || !openIssuesFile || !inProgressIssuesFile || !taskFile) {
45
+ throw new Error(
46
+ 'Usage: dep-guard-analyze.js <current-issue.json> <open-issues.json> <in-progress-issues.json> <task-file> [repository-root]',
47
+ );
48
+ }
49
+
50
+ const currentIssue = normalizeSingleIssuePayload(readJsonFile(currentIssueFile));
51
+ const openIssues = [
52
+ ...normalizeListPayload(readJsonFile(openIssuesFile)),
53
+ ...normalizeListPayload(readJsonFile(inProgressIssuesFile)),
54
+ ].filter((issue) => issue && issue.id && issue.id !== currentIssue.id);
55
+
56
+ const result = analyzePhase3Dependencies({
57
+ currentIssue,
58
+ openIssues,
59
+ taskFile,
60
+ repositoryRoot,
61
+ });
62
+
63
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
64
+ }
65
+
66
+ try {
67
+ main();
68
+ } catch (error) {
69
+ process.stderr.write(`${error.message}\n`);
70
+ process.exit(1);
71
+ }