@felixdotgo/aidlc-kit 0.1.0 → 0.2.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 (150) hide show
  1. package/DEVELOPMENT.md +14 -0
  2. package/README.md +14 -6
  3. package/dist/assets/.agents/aidlc/docker/Dockerfile.claude +7 -0
  4. package/dist/assets/.agents/aidlc/docker/Dockerfile.codex +9 -0
  5. package/dist/assets/.agents/aidlc/docker/Dockerfile.opencode +8 -0
  6. package/dist/assets/.agents/aidlc/orchestrator.md +1 -1
  7. package/dist/assets/.agents/aidlc/phase-clarify.md +1 -1
  8. package/dist/assets/.agents/aidlc/phase-plan.md +2 -2
  9. package/dist/assets/.agents/aidlc/schemas/config.schema.json +2 -1
  10. package/dist/assets/.agents/aidlc/schemas/manifest.schema.json +1 -1
  11. package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +5 -0
  12. package/dist/assets/.agents/aidlc/scripts/lib/loop-authority.mjs +245 -0
  13. package/dist/assets/.agents/aidlc/scripts/lib/loop-commit-message.mjs +45 -0
  14. package/dist/assets/.agents/aidlc/scripts/lib/loop-daemon.mjs +232 -0
  15. package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +21 -4
  16. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +86 -28
  17. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +7 -0
  18. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +22 -6
  19. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +46 -15
  20. package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +22 -9
  21. package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +23 -5
  22. package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +43 -15
  23. package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +82 -6
  24. package/dist/assets/.agents/aidlc/scripts/lib/loop-stream.mjs +60 -0
  25. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +142 -5
  26. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor.mjs +9 -1
  27. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +31 -5
  28. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-image.mjs +64 -0
  29. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-snapshot.mjs +3 -2
  30. package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +59 -4
  31. package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +85 -73
  32. package/dist/assets/.agents/aidlc/scripts/loop-gate.mjs +19 -7
  33. package/dist/assets/.agents/aidlc/scripts/loop.mjs +2 -1
  34. package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +2 -0
  35. package/dist/assets/.agents/aidlc/scripts/state.mjs +4 -0
  36. package/dist/assets/.agents/aidlc/templates/design.md +9 -0
  37. package/dist/assets/.agents/aidlc/templates/intent.md +4 -0
  38. package/dist/assets/.agents/aidlc/templates/workplan.md +1 -1
  39. package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +1 -1
  40. package/dist/assets/ui/bulma.min.css +3 -0
  41. package/dist/assets/ui/dashboard.css +414 -0
  42. package/dist/services/aidlc-coordination/Dockerfile +11 -3
  43. package/dist/services/aidlc-coordination/compose.yaml +12 -0
  44. package/dist/services/aidlc-coordination/package-lock.json +3 -1
  45. package/dist/services/aidlc-coordination/package.json +9 -4
  46. package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +59 -4
  47. package/dist/services/aidlc-coordination/src/repository.mjs +66 -0
  48. package/dist/services/aidlc-coordination/src/server.mjs +9 -3
  49. package/dist/services/aidlc-coordination/test/fleet.test.mjs +21 -0
  50. package/dist/services/aidlc-coordination/test/server-health-auth.test.mjs +67 -0
  51. package/dist/src/adapters.d.ts +0 -3
  52. package/dist/src/adapters.js +11 -6
  53. package/dist/src/cli/argv.d.ts +12 -0
  54. package/dist/src/cli/argv.js +62 -0
  55. package/dist/src/cli/commands.d.ts +8 -0
  56. package/dist/src/cli/commands.js +129 -0
  57. package/dist/src/cli/options.d.ts +25 -0
  58. package/dist/src/cli/options.js +32 -0
  59. package/dist/src/cli/prompt.d.ts +9 -0
  60. package/dist/src/cli/prompt.js +66 -0
  61. package/dist/src/cli.js +67 -217
  62. package/dist/src/context.js +2 -0
  63. package/dist/src/gate.d.ts +1 -0
  64. package/dist/src/gate.js +51 -37
  65. package/dist/src/installer.d.ts +19 -7
  66. package/dist/src/installer.js +111 -178
  67. package/dist/src/legacy.js +1 -0
  68. package/dist/src/managed-content.d.ts +7 -0
  69. package/dist/src/managed-content.js +15 -4
  70. package/dist/src/managed-planning.d.ts +65 -0
  71. package/dist/src/managed-planning.js +219 -0
  72. package/dist/src/mcp-setup.js +3 -1
  73. package/dist/src/model/config-contracts.d.ts +65 -0
  74. package/dist/src/model/config-contracts.js +1 -0
  75. package/dist/src/model/install-contracts.d.ts +29 -0
  76. package/dist/src/model/install-contracts.js +1 -0
  77. package/dist/src/model/lifecycle-enums.d.ts +13 -0
  78. package/dist/src/model/lifecycle-enums.js +1 -0
  79. package/dist/src/model/loop.d.ts +20 -0
  80. package/dist/src/model/loop.js +1 -0
  81. package/dist/src/model/manifest.d.ts +16 -0
  82. package/dist/src/model/manifest.js +1 -0
  83. package/dist/src/model/records.d.ts +138 -0
  84. package/dist/src/model/records.js +1 -0
  85. package/dist/src/model.d.ts +11 -264
  86. package/dist/src/model.js +1 -1
  87. package/dist/src/profiles.d.ts +8 -0
  88. package/dist/src/profiles.js +63 -8
  89. package/dist/src/project-migration.d.ts +1 -0
  90. package/dist/src/project-migration.js +7 -0
  91. package/dist/src/state/archive-record.d.ts +9 -0
  92. package/dist/src/state/archive-record.js +26 -0
  93. package/dist/src/state/commands.d.ts +18 -0
  94. package/dist/src/state/commands.js +27 -0
  95. package/dist/src/state/domain.d.ts +9 -0
  96. package/dist/src/state/domain.js +9 -0
  97. package/dist/src/state/guards.d.ts +7 -0
  98. package/dist/src/state/guards.js +17 -0
  99. package/dist/src/state/io.d.ts +10 -0
  100. package/dist/src/state/io.js +35 -0
  101. package/dist/src/state/items.d.ts +24 -0
  102. package/dist/src/state/items.js +147 -0
  103. package/dist/src/state/lessons.d.ts +7 -0
  104. package/dist/src/state/lessons.js +74 -0
  105. package/dist/src/state/lifecycle.d.ts +5 -0
  106. package/dist/src/state/lifecycle.js +78 -0
  107. package/dist/src/state/lock.d.ts +7 -0
  108. package/dist/src/state/lock.js +125 -0
  109. package/dist/src/state/memory.d.ts +8 -0
  110. package/dist/src/state/memory.js +100 -0
  111. package/dist/src/state/migrate.d.ts +17 -0
  112. package/dist/src/state/migrate.js +60 -0
  113. package/dist/src/state/partition.d.ts +15 -0
  114. package/dist/src/state/partition.js +90 -0
  115. package/dist/src/state/paths.d.ts +11 -0
  116. package/dist/src/state/paths.js +12 -0
  117. package/dist/src/state/render.d.ts +26 -0
  118. package/dist/src/state/render.js +32 -0
  119. package/dist/src/state/store.d.ts +4 -0
  120. package/dist/src/state/store.js +75 -0
  121. package/dist/src/state/transitions.d.ts +21 -0
  122. package/dist/src/state/transitions.js +95 -0
  123. package/dist/src/state/validate.d.ts +4 -0
  124. package/dist/src/state/validate.js +143 -0
  125. package/dist/src/state.d.ts +20 -117
  126. package/dist/src/state.js +20 -815
  127. package/dist/src/task-ui.d.ts +54 -0
  128. package/dist/src/task-ui.js +1570 -0
  129. package/dist/src/upgrade-board.d.ts +5 -0
  130. package/dist/src/upgrade-board.js +89 -0
  131. package/dist/src/upgrade.d.ts +3 -5
  132. package/dist/src/upgrade.js +174 -255
  133. package/dist/src/workflow.js +4 -4
  134. package/docs/README.md +6 -2
  135. package/docs/architecture.md +12 -8
  136. package/docs/command-reference.md +83 -19
  137. package/docs/configuration.md +30 -7
  138. package/docs/coordination-service.md +20 -5
  139. package/docs/design-decisions.md +83 -0
  140. package/docs/development.md +41 -2
  141. package/docs/loop-local-run.md +495 -0
  142. package/docs/loop-runbook.md +235 -0
  143. package/docs/operating-workflow.md +5 -2
  144. package/docs/testing-and-release.md +11 -7
  145. package/docs/troubleshooting.md +125 -0
  146. package/package.json +5 -2
  147. package/dist/src/ledger.d.ts +0 -22
  148. package/dist/src/ledger.js +0 -119
  149. package/dist/src/tracker.d.ts +0 -29
  150. package/dist/src/tracker.js +0 -41
@@ -0,0 +1,414 @@
1
+ /*
2
+ * AI-DLC dashboard theme layer, loaded after the vendored Bulma 1.0.3.
3
+ *
4
+ * - Keeps Bulma's default palette (primary teal, link blue).
5
+ * - Flat, enterprise look: white canvas, 1px hairline borders, small radii,
6
+ * no elevation except overlays (drawer, modal, toast).
7
+ * - Dark mode follows the system automatically: Bulma switches its scheme
8
+ * variables inside `@media (prefers-color-scheme: dark)`, and every colour
9
+ * below is expressed through `--bulma-*` variables so it follows along.
10
+ * Never override scheme lightness (`--bulma-*-l`) on a bare `:root` here:
11
+ * it has the same specificity as Bulma's media-query block and would pin
12
+ * one scheme.
13
+ * - Custom components use the `ad-` prefix to avoid clashing with Bulma.
14
+ */
15
+
16
+ :root {
17
+ --bulma-family-primary: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
18
+ --bulma-family-secondary: var(--bulma-family-primary);
19
+ --bulma-family-code: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
20
+ --bulma-radius: 4px;
21
+ --bulma-radius-small: 3px;
22
+ --bulma-radius-medium: 4px;
23
+ --bulma-radius-large: 6px;
24
+ --ad-radius: 4px;
25
+ --ad-ring: 0 0 0 1px var(--bulma-border-weak);
26
+ --ad-ring-strong: 0 0 0 1px var(--bulma-border);
27
+ --ad-shadow-sm: none;
28
+ --ad-shadow: none;
29
+ --ad-shadow-lg: 0 8px 24px -4px hsla(221, 30%, 10%, 0.22), 0 0 1px hsla(221, 30%, 10%, 0.3);
30
+ --ad-surface: var(--bulma-scheme-main);
31
+ --ad-canvas: var(--bulma-scheme-main);
32
+ --ad-subtle: var(--bulma-scheme-main-bis);
33
+ --ad-muted: var(--bulma-text-weak);
34
+ --ad-log-bg: hsl(222, 22%, 9%);
35
+ --ad-log-fg: hsl(220, 14%, 86%);
36
+ --ad-transition: 160ms ease;
37
+ }
38
+
39
+ @media (prefers-color-scheme: dark) {
40
+ :root {
41
+ --ad-shadow-lg: 0 12px 32px -6px hsla(0, 0%, 0%, 0.7), 0 0 0 1px hsla(0, 0%, 100%, 0.06);
42
+ --ad-surface: var(--bulma-scheme-main);
43
+ --ad-canvas: var(--bulma-scheme-main);
44
+ --ad-subtle: var(--bulma-scheme-main-bis);
45
+ --ad-log-bg: hsl(222, 25%, 6%);
46
+ }
47
+ }
48
+
49
+ html { background-color: var(--ad-canvas); scroll-padding-top: 5rem; }
50
+ body { min-height: 100vh; font-feature-settings: "cv11", "ss01"; -webkit-font-smoothing: antialiased; }
51
+ code { border-radius: var(--bulma-radius-small); font-size: 0.85em; }
52
+ [hidden] { display: none !important; }
53
+ :focus-visible { outline: 2px solid var(--bulma-link); outline-offset: 2px; }
54
+
55
+ /* ---------- App header ---------- */
56
+ .ad-header {
57
+ position: sticky; top: 0; z-index: 30;
58
+ background: var(--ad-surface);
59
+ border-bottom: 1px solid var(--bulma-border-weak);
60
+ }
61
+ .ad-header-inner { display: flex; align-items: center; gap: 1rem; min-height: 3.75rem; flex-wrap: wrap; padding-block: 0.5rem; }
62
+ .ad-brand { display: flex; align-items: center; gap: 0.5rem; color: var(--bulma-text-strong); font-weight: 700; font-size: 1.1rem; letter-spacing: -0.01em; }
63
+ .ad-brand:hover { color: var(--bulma-text-strong); }
64
+ .ad-logo {
65
+ display: inline-grid; place-items: center; width: 1.75rem; height: 1.75rem; border-radius: var(--ad-radius);
66
+ background: var(--bulma-primary); color: var(--bulma-primary-invert); font-size: 0.9rem; font-weight: 800;
67
+ }
68
+ .ad-header-actions { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
69
+ .ad-main { padding-block: 1.5rem 3rem; padding-inline: 0; }
70
+
71
+ /* ---------- Keyboard hint ---------- */
72
+ kbd, .ad-kbd {
73
+ display: inline-block; min-width: 1.25rem; padding: 0 0.3rem; margin-left: 0.4rem;
74
+ font-family: var(--bulma-family-code); font-size: 0.7rem; line-height: 1.25rem; text-align: center;
75
+ color: inherit; opacity: 0.75; border: 1px solid currentColor; border-radius: 0.3rem;
76
+ }
77
+
78
+ /* ---------- Generic dot / pill ---------- */
79
+ .ad-dot { display: inline-block; width: 0.5rem; height: 0.5rem; border-radius: 999px; background: currentColor; flex: none; }
80
+ .ad-pill {
81
+ display: inline-flex; align-items: center; gap: 0.375rem; white-space: nowrap;
82
+ padding: 0.0625rem 0.45rem; border-radius: var(--bulma-radius-small); font-size: 0.72rem; font-weight: 600; line-height: 1.4;
83
+ background: var(--bulma-scheme-main-ter); color: var(--bulma-text);
84
+ border: 1px solid var(--bulma-border-weak);
85
+ }
86
+ .ad-pill.is-info { background: var(--bulma-info-soft); color: var(--bulma-info-bold); border-color: transparent; }
87
+ .ad-pill.is-success { background: var(--bulma-success-soft); color: var(--bulma-success-bold); border-color: transparent; }
88
+ .ad-pill.is-warning { background: var(--bulma-warning-soft); color: var(--bulma-warning-bold); border-color: transparent; }
89
+ .ad-pill.is-danger { background: var(--bulma-danger-soft); color: var(--bulma-danger-bold); border-color: transparent; }
90
+ .ad-pill.is-primary { background: var(--bulma-primary-soft); color: var(--bulma-primary-bold); border-color: transparent; }
91
+ .ad-pill.is-link { background: var(--bulma-link-soft); color: var(--bulma-link-bold); border-color: transparent; }
92
+ .ad-pill.is-ghost { background: transparent; color: var(--ad-muted); }
93
+ .ad-pill.is-live .ad-dot { animation: ad-pulse 1.8s ease-in-out infinite; }
94
+ @keyframes ad-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
95
+
96
+ /* ---------- Soft buttons (Bulma's is-light stays bright in dark mode) ---------- */
97
+ .button.ad-soft { border-color: transparent; }
98
+ .button.ad-soft.is-warning { background: var(--bulma-warning-soft); color: var(--bulma-warning-bold); }
99
+ .button.ad-soft.is-link { background: var(--bulma-link-soft); color: var(--bulma-link-bold); }
100
+ .button.ad-soft.is-danger { background: var(--bulma-danger-soft); color: var(--bulma-danger-bold); }
101
+ .button.ad-soft:hover { filter: brightness(0.96); }
102
+ @media (prefers-color-scheme: dark) { .button.ad-soft:hover { filter: brightness(1.2); } }
103
+
104
+ /* ---------- Stat cards ---------- */
105
+ .ad-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); gap: 0.875rem; margin-bottom: 1.5rem; }
106
+ .ad-stat {
107
+ display: flex; flex-direction: column; gap: 0.25rem; padding: 0.9rem 1rem; border-radius: var(--ad-radius);
108
+ background: var(--ad-surface); box-shadow: var(--ad-ring), var(--ad-shadow-sm); position: relative; overflow: hidden;
109
+ color: inherit; text-align: left; border: 0; font: inherit; cursor: pointer; transition: box-shadow var(--ad-transition), background var(--ad-transition);
110
+ }
111
+ .ad-stat:hover { box-shadow: var(--ad-ring-strong); background: var(--ad-subtle); }
112
+ .ad-stat[aria-pressed="true"] { box-shadow: 0 0 0 2px var(--ad-accent, var(--bulma-link)); }
113
+ .ad-stat::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 3px; background: var(--ad-accent, var(--bulma-border)); }
114
+ .ad-stat-label { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--ad-muted); display: flex; align-items: center; gap: 0.4rem; }
115
+ .ad-stat-label .ad-dot { color: var(--ad-accent, var(--ad-muted)); }
116
+ .ad-stat-value { font-size: 1.6rem; font-weight: 700; line-height: 1.1; color: var(--bulma-text-strong); font-variant-numeric: tabular-nums; }
117
+ .ad-stat.is-info { --ad-accent: var(--bulma-info); }
118
+ .ad-stat.is-warning { --ad-accent: var(--bulma-warning); }
119
+ .ad-stat.is-success { --ad-accent: var(--bulma-success); }
120
+ .ad-stat.is-danger { --ad-accent: var(--bulma-danger); }
121
+ .ad-stat.is-primary { --ad-accent: var(--bulma-primary); }
122
+
123
+ /* ---------- Needs your review ---------- */
124
+ .ad-attention {
125
+ margin-bottom: 1.5rem; border-radius: var(--ad-radius); overflow: hidden;
126
+ background: var(--ad-surface); box-shadow: 0 0 0 1px var(--bulma-warning), var(--ad-shadow-sm);
127
+ }
128
+ .ad-attention-head {
129
+ display: flex; align-items: center; gap: 0.6rem; padding: 0.7rem 1rem;
130
+ background: var(--bulma-warning-soft); color: var(--bulma-warning-bold); font-weight: 700; font-size: 0.9rem;
131
+ }
132
+ .ad-attention-head .ad-count { margin-left: auto; }
133
+ .ad-attention-list { list-style: none; margin: 0; padding: 0; }
134
+ .ad-attention-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.65rem 1rem; border-top: 1px solid var(--bulma-border-weak); flex-wrap: wrap; }
135
+ .ad-attention-item .ad-title { font-weight: 600; color: var(--bulma-text-strong); flex: 1 1 14rem; min-width: 0; }
136
+ .ad-attention-item .ad-actions { margin-left: auto; }
137
+
138
+ /* ---------- Toolbar ---------- */
139
+ .ad-toolbar { display: flex; align-items: center; gap: 0.625rem; margin-bottom: 1rem; flex-wrap: wrap; }
140
+ .ad-search { position: relative; flex: 1 1 16rem; max-width: 26rem; }
141
+ .ad-search .input { padding-left: 2.1rem; padding-right: 2.2rem; }
142
+ .ad-search-icon { position: absolute; left: 0.7rem; top: 50%; transform: translateY(-50%); color: var(--ad-muted); pointer-events: none; z-index: 5; }
143
+ .ad-search kbd { position: absolute; right: 0.6rem; top: 50%; transform: translateY(-50%); margin: 0; color: var(--ad-muted); pointer-events: none; }
144
+ .ad-toolbar .select select { min-width: 9rem; }
145
+ .ad-toolbar-end { margin-left: auto; display: flex; align-items: center; gap: 0.625rem; }
146
+ .ad-segmented { display: inline-flex; padding: 0.2rem; gap: 0.2rem; border-radius: var(--bulma-radius); background: var(--ad-subtle); box-shadow: inset 0 0 0 1px var(--bulma-border-weak); }
147
+ .ad-segmented button {
148
+ border: 0; background: transparent; color: var(--ad-muted); font: inherit; font-size: 0.82rem; font-weight: 600;
149
+ padding: 0.3rem 0.8rem; border-radius: calc(var(--bulma-radius) - 0.15rem); cursor: pointer; transition: background var(--ad-transition), color var(--ad-transition);
150
+ }
151
+ .ad-segmented button:hover { color: var(--bulma-text-strong); }
152
+ .ad-segmented button.is-active { background: var(--ad-surface); color: var(--bulma-text-strong); box-shadow: var(--ad-ring); }
153
+ .ad-result-count { font-size: 0.8rem; color: var(--ad-muted); }
154
+
155
+ /* ---------- Kanban ---------- */
156
+ .ad-board { display: grid; grid-template-columns: repeat(5, minmax(15rem, 1fr)); gap: 0.75rem; overflow-x: auto; padding-bottom: 0.5rem; align-items: start; }
157
+ .ad-column {
158
+ display: flex; flex-direction: column; min-height: 12rem; max-height: calc(100vh - 14rem); border-radius: var(--ad-radius);
159
+ background: var(--ad-subtle);
160
+ }
161
+ .ad-column-head { display: flex; align-items: center; gap: 0.5rem; padding: 0.75rem 0.875rem 0.5rem; }
162
+ .ad-column-title { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--ad-muted); }
163
+ .ad-column-head .ad-step-index { color: var(--ad-muted); font-size: 0.72rem; font-variant-numeric: tabular-nums; }
164
+ .ad-count {
165
+ margin-left: auto; min-width: 1.5rem; padding: 0 0.45rem; border-radius: 999px; text-align: center;
166
+ font-size: 0.72rem; font-weight: 700; line-height: 1.35rem; background: var(--ad-surface); color: var(--ad-muted); box-shadow: var(--ad-ring);
167
+ }
168
+ .ad-column-body { display: flex; flex-direction: column; gap: 0.5rem; padding: 0.25rem 0.625rem 0.75rem; overflow-y: auto; }
169
+ .ad-column-empty { margin: 0.5rem 0.25rem; padding: 1.25rem 0.5rem; border: 1px dashed var(--bulma-border); border-radius: var(--bulma-radius); text-align: center; font-size: 0.8rem; color: var(--ad-muted); }
170
+
171
+ /* ---------- Task card ---------- */
172
+ .ad-card {
173
+ position: relative; display: flex; flex-direction: column; gap: 0.5rem; padding: 0.8rem 0.875rem 0.8rem 1rem;
174
+ border-radius: var(--ad-radius); background: var(--ad-surface); box-shadow: var(--ad-ring);
175
+ transition: box-shadow var(--ad-transition), background var(--ad-transition);
176
+ }
177
+ .ad-card::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; border-radius: var(--ad-radius) 0 0 var(--ad-radius); background: var(--ad-status-color, var(--bulma-border)); }
178
+ .ad-card:hover { box-shadow: var(--ad-ring-strong); background: var(--ad-subtle); }
179
+ .ad-card.is-attention { box-shadow: 0 0 0 1px var(--bulma-warning); }
180
+ .ad-card-top { display: flex; align-items: center; gap: 0.5rem; }
181
+ .ad-card-id { font-family: var(--bulma-family-code); font-size: 0.72rem; color: var(--ad-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
182
+ .ad-card-id:hover { color: var(--bulma-link); }
183
+ .ad-card-top .ad-pill { margin-left: auto; }
184
+ .ad-card-title { font-weight: 600; font-size: 0.92rem; line-height: 1.35; color: var(--bulma-text-strong); word-break: break-word; }
185
+ .ad-card-title a { color: inherit; }
186
+ .ad-card-title a:hover { color: var(--bulma-link); }
187
+ .ad-card-meta { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }
188
+ .ad-card-foot { display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; color: var(--ad-muted); }
189
+ .ad-card-foot .ad-areas { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
190
+ .ad-card-foot .button { margin-left: auto; }
191
+
192
+ [data-status="active"] { --ad-status-color: var(--bulma-info); }
193
+ [data-status="blocked_on_user"] { --ad-status-color: var(--bulma-warning); }
194
+ [data-status="paused"] { --ad-status-color: var(--bulma-border); }
195
+ [data-status="done"] { --ad-status-color: var(--bulma-success); }
196
+ [data-status="closed"], [data-status="superseded"], [data-handoff="true"] { --ad-status-color: var(--bulma-danger); }
197
+
198
+ /* ---------- Phase stepper (mini, horizontal) ---------- */
199
+ .ad-steps { display: flex; gap: 3px; width: 100%; }
200
+ .ad-steps > span { flex: 1; height: 4px; border-radius: 999px; background: var(--bulma-border-weak); }
201
+ .ad-steps > span.is-complete { background: var(--bulma-primary); }
202
+ .ad-steps > span.is-current { background: var(--bulma-primary); box-shadow: 0 0 0 2px var(--bulma-primary-soft); }
203
+ .ad-steps.is-done > span { background: var(--bulma-success); }
204
+
205
+ /* ---------- Phase stepper (horizontal, detail page and side panel) ---------- */
206
+ .ad-stepper {
207
+ list-style: none; display: flex; margin: 0 0 1.25rem; padding: 0.85rem 1rem; overflow-x: auto;
208
+ border-radius: var(--ad-radius); background: var(--ad-surface); box-shadow: var(--ad-ring);
209
+ }
210
+ .ad-stepper li { flex: 1 0 auto; display: flex; align-items: center; gap: 0.6rem; }
211
+ .ad-stepper li::after { content: ""; flex: 1 1 auto; min-width: 1.5rem; height: 2px; margin: 0 0.75rem 0 0.25rem; background: var(--bulma-border-weak); }
212
+ .ad-stepper li:last-child { flex: 0 0 auto; min-width: 0; }
213
+ .ad-stepper li:last-child::after { display: none; }
214
+ .ad-stepper li.is-complete::after { background: var(--bulma-primary); }
215
+ .ad-stepper-marker {
216
+ flex: none; display: grid; place-items: center; width: 1.5rem; height: 1.5rem; border-radius: 999px;
217
+ font-size: 0.72rem; font-weight: 700; background: var(--ad-surface); color: var(--ad-muted); box-shadow: inset 0 0 0 2px var(--bulma-border);
218
+ }
219
+ .ad-stepper li.is-complete .ad-stepper-marker { background: var(--bulma-primary); color: var(--bulma-primary-invert); box-shadow: none; }
220
+ .ad-stepper li.is-current .ad-stepper-marker { color: var(--bulma-primary-bold); box-shadow: inset 0 0 0 2px var(--bulma-primary), 0 0 0 3px var(--bulma-primary-soft); }
221
+ .ad-stepper-text { display: flex; flex-direction: column; line-height: 1.25; white-space: nowrap; }
222
+ .ad-stepper-text strong { font-size: 0.85rem; text-transform: capitalize; color: var(--bulma-text-strong); }
223
+ .ad-stepper li:not(.is-current):not(.is-complete) .ad-stepper-text strong { color: var(--ad-muted); font-weight: 500; }
224
+ .ad-stepper-text small { font-size: 0.72rem; color: var(--ad-muted); }
225
+ .ad-stepper li.is-current .ad-stepper-text small { color: var(--bulma-primary-bold); font-weight: 600; }
226
+
227
+ /* ---------- List view ---------- */
228
+ .ad-table-wrap { border-radius: var(--ad-radius); background: var(--ad-surface); box-shadow: var(--ad-ring); overflow-x: auto; }
229
+ .ad-table { width: 100%; margin: 0 !important; background: transparent; font-size: 0.875rem; }
230
+ .ad-table th { font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--ad-muted) !important; background: var(--ad-subtle); white-space: nowrap; border-color: var(--bulma-border-weak) !important; }
231
+ .ad-table td { vertical-align: middle; border-color: var(--bulma-border-weak) !important; }
232
+ .ad-table tbody tr { transition: background var(--ad-transition); }
233
+ .ad-table tbody tr:hover { background: var(--ad-subtle); }
234
+ .ad-table tbody tr:last-child td { border-bottom: 0; }
235
+ .ad-table .ad-steps { min-width: 6rem; }
236
+ .ad-table td.is-empty { text-align: center; color: var(--ad-muted); padding: 2rem; }
237
+
238
+ /* ---------- Empty states ---------- */
239
+ .ad-empty { text-align: center; padding: 3.5rem 1rem; border-radius: var(--ad-radius); background: var(--ad-surface); box-shadow: var(--ad-ring); }
240
+ .ad-empty-icon { font-size: 2.25rem; line-height: 1; margin-bottom: 0.75rem; }
241
+ .ad-empty h2 { font-size: 1.1rem; font-weight: 700; color: var(--bulma-text-strong); margin-bottom: 0.35rem; }
242
+ .ad-empty p { color: var(--ad-muted); margin-bottom: 1.25rem; }
243
+ .ad-no-match { display: none; text-align: center; padding: 1.5rem; color: var(--ad-muted); }
244
+ .ad-no-match.is-visible { display: block; }
245
+
246
+ /* ---------- Panels (detail page) ---------- */
247
+ .ad-panel { border-radius: var(--ad-radius); background: var(--ad-surface); box-shadow: var(--ad-ring); margin-bottom: 1rem; overflow: hidden; }
248
+ .ad-panel-head { display: flex; align-items: center; gap: 0.6rem; padding: 0.65rem 1.1rem; border-bottom: 1px solid var(--bulma-border-weak); }
249
+ .ad-panel-head h2, .ad-panel-head h3 { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--ad-muted); margin: 0; }
250
+ .ad-panel-head .ad-count, .ad-panel-head .ad-panel-tools { margin-left: auto; }
251
+ .ad-panel-tools { display: flex; gap: 0.4rem; }
252
+ .ad-panel-body { padding: 1rem 1.1rem; }
253
+ .ad-panel-body.is-flush { padding: 0; }
254
+ .ad-panel .ad-table th:first-child, .ad-panel .ad-table td:first-child { padding-left: 1.1rem; }
255
+ .ad-panel .ad-table th:last-child, .ad-panel .ad-table td:last-child { padding-right: 1.1rem; }
256
+ .ad-panel .ad-empty-line { padding: 1rem 1.1rem; color: var(--ad-muted); font-size: 0.875rem; }
257
+ .ad-panel.is-warning { box-shadow: 0 0 0 1px var(--bulma-warning); }
258
+ .ad-panel.is-warning > .ad-panel-head { background: var(--bulma-warning-soft); color: var(--bulma-warning-bold); border-bottom-color: transparent; }
259
+ .ad-panel.is-warning > .ad-panel-head h2, .ad-panel.is-warning > .ad-panel-head h3 { color: inherit; }
260
+ .ad-panel.is-danger { box-shadow: 0 0 0 1px var(--bulma-danger); }
261
+ .ad-panel.is-danger > .ad-panel-head { background: var(--bulma-danger-soft); color: var(--bulma-danger-bold); border-bottom-color: transparent; }
262
+ .ad-panel.is-danger > .ad-panel-head h2 { color: inherit; }
263
+ .ad-panel.is-success { box-shadow: 0 0 0 1px var(--bulma-success); }
264
+
265
+ /* ---------- Detail page header ---------- */
266
+ .ad-breadcrumb { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.85rem; color: var(--ad-muted); margin-bottom: 0.75rem; }
267
+ .ad-breadcrumb:hover { color: var(--bulma-link); }
268
+ .ad-page-title { font-size: 1.5rem; font-weight: 700; letter-spacing: -0.02em; line-height: 1.25; color: var(--bulma-text-strong); margin-bottom: 0.5rem; word-break: break-word; }
269
+ .ad-page-sub { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-bottom: 1rem; }
270
+ .ad-page-sub code { color: var(--ad-muted); background: var(--bulma-scheme-main-ter); }
271
+
272
+ /* ---------- Meta list (sidebar) ---------- */
273
+ .ad-meta { display: grid; grid-template-columns: auto 1fr; gap: 0.55rem 1rem; margin: 0; font-size: 0.85rem; }
274
+ .ad-meta dt { color: var(--ad-muted); font-weight: 500; }
275
+ .ad-meta dd { margin: 0; color: var(--bulma-text-strong); text-align: right; word-break: break-word; min-width: 0; }
276
+ .ad-sticky { position: sticky; top: 5rem; }
277
+
278
+ /* ---------- Gate view ---------- */
279
+ .ad-gate-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: 0.75rem; margin-bottom: 1rem; }
280
+ .ad-gate-cell { padding: 0.65rem 0.8rem; border-radius: var(--bulma-radius); background: var(--ad-subtle); box-shadow: var(--ad-ring); }
281
+ .ad-gate-cell small { display: block; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--ad-muted); margin-bottom: 0.15rem; }
282
+ .ad-gate-cell span { font-size: 0.875rem; color: var(--bulma-text-strong); }
283
+ .ad-subhead { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--ad-muted); margin: 1rem 0 0.5rem; }
284
+ .ad-plain-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; font-size: 0.875rem; }
285
+ .ad-plain-list li { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
286
+ .ad-callout { display: flex; gap: 0.6rem; align-items: flex-start; margin-top: 1rem; padding: 0.75rem 0.9rem; border-radius: var(--bulma-radius); background: var(--bulma-warning-soft); color: var(--bulma-warning-bold); font-weight: 600; font-size: 0.9rem; }
287
+
288
+ /* ---------- Artifacts accordion ---------- */
289
+ .ad-artifact { border-top: 1px solid var(--bulma-border-weak); }
290
+ .ad-artifact:first-child { border-top: 0; }
291
+ .ad-artifact > summary {
292
+ display: flex; align-items: center; gap: 0.6rem; padding: 0.8rem 1.1rem; cursor: pointer; list-style: none;
293
+ font-weight: 600; color: var(--bulma-text-strong); transition: background var(--ad-transition);
294
+ }
295
+ .ad-artifact > summary::-webkit-details-marker { display: none; }
296
+ .ad-artifact > summary::before { content: "›"; display: inline-block; width: 0.75rem; font-size: 1.1rem; line-height: 1; color: var(--ad-muted); transition: transform var(--ad-transition); }
297
+ .ad-artifact[open] > summary::before { transform: rotate(90deg); }
298
+ .ad-artifact > summary:hover { background: var(--ad-subtle); }
299
+ .ad-artifact > summary code { margin-left: auto; font-weight: 400; color: var(--ad-muted); background: transparent; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 60%; }
300
+ .ad-artifact .artifact { padding: 0.25rem 1.25rem 1.25rem 2.5rem; max-height: 40rem; overflow: auto; }
301
+ .content.artifact { font-size: 0.92rem; }
302
+ .content.artifact h1, .content.artifact h2, .content.artifact h3 { margin-top: 1.25em; margin-bottom: 0.5em; }
303
+ .content.artifact h1 { font-size: 1.35rem; }
304
+ .content.artifact h2 { font-size: 1.1rem; padding-bottom: 0.3rem; border-bottom: 1px solid var(--bulma-border-weak); }
305
+ .content.artifact h3 { font-size: 1rem; }
306
+ .content.artifact table { font-size: 0.82rem; }
307
+ .content.artifact pre { background: var(--bulma-scheme-main-ter); padding: 0.75rem 1rem; border-radius: var(--bulma-radius); overflow: auto; }
308
+ .content.artifact blockquote { border-left-color: var(--bulma-primary); background: var(--bulma-scheme-main-ter); }
309
+
310
+ /* ---------- Terminal log ---------- */
311
+ .ad-terminal { border-radius: var(--ad-radius); overflow: hidden; background: var(--ad-log-bg); margin-bottom: 1rem; }
312
+ .ad-terminal-head { display: flex; align-items: center; gap: 0.5rem; padding: 0.55rem 0.9rem; background: hsla(0, 0%, 100%, 0.04); border-bottom: 1px solid hsla(0, 0%, 100%, 0.06); color: hsl(220, 10%, 70%); font-size: 0.8rem; }
313
+ .ad-terminal-head .ad-lights { display: none; }
314
+ .ad-terminal-head .ad-lights span { width: 0.65rem; height: 0.65rem; border-radius: 999px; background: hsl(0, 0%, 35%); }
315
+ .ad-terminal-head .ad-lights span:nth-child(1) { background: hsl(4, 85%, 62%); }
316
+ .ad-terminal-head .ad-lights span:nth-child(2) { background: hsl(40, 90%, 58%); }
317
+ .ad-terminal-head .ad-lights span:nth-child(3) { background: hsl(140, 55%, 50%); }
318
+ .ad-terminal-head strong { color: hsl(220, 14%, 90%); font-weight: 600; }
319
+ .ad-terminal-tools { margin-left: auto; display: flex; gap: 0.35rem; }
320
+ .ad-terminal-tools button {
321
+ font: inherit; font-size: 0.72rem; padding: 0.15rem 0.55rem; border-radius: 0.35rem; cursor: pointer;
322
+ color: hsl(220, 10%, 78%); background: hsla(0, 0%, 100%, 0.06); border: 1px solid hsla(0, 0%, 100%, 0.1);
323
+ }
324
+ .ad-terminal-tools button:hover { background: hsla(0, 0%, 100%, 0.12); color: #fff; }
325
+ .ad-terminal-tools button[aria-pressed="true"] { color: hsl(171, 80%, 60%); border-color: hsla(171, 80%, 60%, 0.4); }
326
+ .log-panel {
327
+ height: 24rem; overflow: auto; padding: 0.75rem 0.9rem; margin: 0;
328
+ font-family: var(--bulma-family-code); font-size: 0.78rem; line-height: 1.55; color: var(--ad-log-fg); white-space: pre-wrap; word-break: break-word;
329
+ }
330
+ .ad-log-line { display: grid; grid-template-columns: auto auto 1fr; gap: 0.75rem; }
331
+ .ad-log-time { color: hsl(220, 10%, 50%); }
332
+ .ad-log-type { color: hsl(200, 80%, 66%); min-width: 8ch; }
333
+ .ad-log-line.is-error .ad-log-type, .ad-log-line.is-error .ad-log-text { color: hsl(0, 85%, 70%); }
334
+ .ad-log-line.is-warn .ad-log-type { color: hsl(40, 90%, 62%); }
335
+ .ad-log-line.is-ok .ad-log-type { color: hsl(140, 55%, 60%); }
336
+ .ad-log-line.is-agent .ad-log-type { color: hsl(171, 80%, 55%); }
337
+ .ad-log-muted { color: hsl(220, 10%, 50%); font-style: italic; }
338
+
339
+ /* ---------- Toasts ---------- */
340
+ .ad-toasts { position: fixed; right: 1rem; bottom: 1rem; z-index: 100; display: flex; flex-direction: column; gap: 0.5rem; max-width: min(26rem, calc(100vw - 2rem)); pointer-events: none; }
341
+ .ad-toast {
342
+ pointer-events: auto; display: flex; align-items: flex-start; gap: 0.65rem; padding: 0.75rem 0.9rem; border-radius: var(--bulma-radius-medium);
343
+ background: var(--ad-surface); color: var(--bulma-text-strong); box-shadow: var(--ad-ring), var(--ad-shadow-lg);
344
+ border-left: 4px solid var(--ad-accent, var(--bulma-info)); font-size: 0.875rem; animation: ad-toast-in 200ms ease-out;
345
+ }
346
+ .ad-toast.is-success { --ad-accent: var(--bulma-success); }
347
+ .ad-toast.is-danger { --ad-accent: var(--bulma-danger); }
348
+ .ad-toast.is-warning { --ad-accent: var(--bulma-warning); }
349
+ .ad-toast-icon { flex: none; font-weight: 800; color: var(--ad-accent, var(--bulma-info)); }
350
+ .ad-toast-body { flex: 1; min-width: 0; word-break: break-word; }
351
+ .ad-toast-body small { display: block; color: var(--ad-muted); margin-top: 0.15rem; }
352
+ .ad-toast-close { flex: none; border: 0; background: transparent; color: var(--ad-muted); cursor: pointer; font-size: 1rem; line-height: 1; padding: 0; }
353
+ .ad-toast.is-leaving { opacity: 0; transform: translateY(0.5rem); transition: opacity 200ms, transform 200ms; }
354
+ @keyframes ad-toast-in { from { opacity: 0; transform: translateY(0.5rem); } to { opacity: 1; transform: none; } }
355
+
356
+ /* ---------- Modals ---------- */
357
+ .modal-background { background-color: hsla(221, 39%, 8%, 0.45); }
358
+ .modal-card { width: min(44rem, calc(100vw - 2rem)); border-radius: var(--ad-radius); box-shadow: var(--ad-shadow-lg); }
359
+ .modal-card-head, .modal-card-foot { background: var(--ad-surface); border-color: var(--bulma-border-weak); padding: 0.9rem 1.25rem; }
360
+ .modal-card-head { box-shadow: none; }
361
+ .modal-card-title { font-size: 1.05rem; font-weight: 700; }
362
+ .modal-card-body { background: var(--ad-surface); }
363
+ .modal-card-foot { justify-content: flex-end; gap: 0.5rem; }
364
+ .modal-card-foot .ad-foot-start { margin-right: auto; font-size: 0.8rem; color: var(--ad-muted); }
365
+ .modal.is-active .modal-card { animation: ad-modal-in 140ms ease-out; }
366
+ @keyframes ad-modal-in { from { opacity: 0; transform: translateY(0.5rem); } to { opacity: 1; transform: none; } }
367
+ .ad-field-hint { display: flex; justify-content: space-between; gap: 1rem; margin-top: 0.4rem; font-size: 0.75rem; color: var(--ad-muted); }
368
+ .ad-field-hint .is-over { color: var(--bulma-danger); font-weight: 600; }
369
+
370
+ /* ---------- Task side panel (drawer, JIRA-style) ---------- */
371
+ .ad-drawer { position: fixed; inset: 0; z-index: 40; }
372
+ .ad-drawer-backdrop { position: absolute; inset: 0; background: hsla(221, 39%, 8%, 0.35); opacity: 0; transition: opacity 200ms ease; }
373
+ .ad-drawer-panel {
374
+ position: absolute; top: 0; right: 0; height: 100%; width: clamp(40rem, 55vw, 72rem); max-width: 100vw;
375
+ display: flex; flex-direction: column; background: var(--ad-surface); border-left: 1px solid var(--bulma-border-weak);
376
+ box-shadow: var(--ad-shadow-lg); transform: translateX(100%); transition: transform 200ms ease; outline: none;
377
+ }
378
+ .ad-drawer.is-open .ad-drawer-backdrop { opacity: 1; }
379
+ .ad-drawer.is-open .ad-drawer-panel { transform: none; }
380
+ .ad-drawer-head {
381
+ flex: none; display: flex; align-items: center; gap: 0.75rem; padding: 0.7rem 1.25rem;
382
+ border-bottom: 1px solid var(--bulma-border-weak); background: var(--ad-surface);
383
+ }
384
+ .ad-drawer-id { font-size: 0.8rem; color: var(--ad-muted); background: transparent; padding: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
385
+ .ad-drawer-head .button { margin-left: auto; }
386
+ .ad-drawer-body { flex: 1 1 auto; overflow-y: auto; padding: 1.25rem 1.5rem 2rem; overscroll-behavior: contain; }
387
+ /* The detail layout stacks in one column inside the panel; Details goes last. */
388
+ .ad-drawer-body .columns { display: block; margin: 0; }
389
+ .ad-drawer-body .column { width: auto; padding: 0; }
390
+ .ad-drawer-body .ad-sticky { position: static; }
391
+ .ad-drawer-body .ad-page-title { font-size: 1.3rem; }
392
+
393
+ /* ---------- Footer help ---------- */
394
+ .ad-help { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--bulma-border-weak); font-size: 0.8rem; color: var(--ad-muted); line-height: 1.6; }
395
+ .ad-help code { font-size: 0.78rem; }
396
+
397
+ /* ---------- Responsive ---------- */
398
+ @media screen and (max-width: 1023px) {
399
+ .ad-board { grid-template-columns: repeat(5, minmax(16rem, 80vw)); scroll-snap-type: x mandatory; }
400
+ .ad-column { scroll-snap-align: start; max-height: none; }
401
+ .ad-sticky { position: static; }
402
+ }
403
+ @media screen and (max-width: 768px) {
404
+ .ad-toolbar-end { margin-left: 0; width: 100%; justify-content: space-between; }
405
+ .ad-search { max-width: none; }
406
+ .ad-header-actions .ad-hide-mobile { display: none; }
407
+ .ad-page-title { font-size: 1.3rem; }
408
+ .ad-drawer-panel { width: 100vw; }
409
+ .ad-drawer-body { padding: 1rem; }
410
+ }
411
+
412
+ @media (prefers-reduced-motion: reduce) {
413
+ *, *::before, *::after { animation: none !important; transition: none !important; }
414
+ }
@@ -1,10 +1,18 @@
1
- FROM node:24-bookworm-slim
1
+ FROM node:24-bookworm-slim AS build
2
2
  WORKDIR /app
3
3
  RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ && rm -rf /var/lib/apt/lists/*
4
4
  COPY package.json package-lock.json ./
5
5
  RUN npm ci --omit=dev
6
- COPY src ./src
7
- RUN mkdir -p /data
6
+
7
+ FROM node:24-bookworm-slim
8
+ WORKDIR /app
9
+ ENV NODE_ENV=production
8
10
  ENV MCP_STATE_HOST=0.0.0.0 MCP_STATE_PORT=8787 MCP_STATE_SQLITE_PATH=/data/mcp-state.db
11
+ COPY --from=build /app/node_modules ./node_modules
12
+ COPY package.json package-lock.json ./
13
+ COPY src ./src
14
+ RUN mkdir -p /data && chown -R node:node /data
15
+ USER node
9
16
  EXPOSE 8787
17
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD node -e "fetch('http://127.0.0.1:8787/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
10
18
  CMD ["node", "src/server.mjs"]
@@ -9,6 +9,12 @@ services:
9
9
  MCP_STATE_DATABASE: sqlite
10
10
  MCP_STATE_SQLITE_PATH: /data/mcp-state.db
11
11
  MCP_STATE_AUTH_TOKEN: ${MCP_STATE_AUTH_TOKEN:?set MCP_STATE_AUTH_TOKEN in .env before starting the service}
12
+ healthcheck:
13
+ test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8787/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
14
+ interval: 30s
15
+ timeout: 3s
16
+ start_period: 5s
17
+ retries: 3
12
18
 
13
19
  postgres:
14
20
  image: postgres:16-alpine
@@ -40,6 +46,12 @@ services:
40
46
  MCP_FLEET_LEAD_TOKEN: ${MCP_FLEET_LEAD_TOKEN:?set MCP_FLEET_LEAD_TOKEN in .env}
41
47
  MCP_FLEET_REVIEWER_TOKEN: ${MCP_FLEET_REVIEWER_TOKEN:?set MCP_FLEET_REVIEWER_TOKEN in .env}
42
48
  MCP_FLEET_WORKER_TOKENS: ${MCP_FLEET_WORKER_TOKENS:?set MCP_FLEET_WORKER_TOKENS JSON in .env}
49
+ healthcheck:
50
+ test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8787/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
51
+ interval: 30s
52
+ timeout: 3s
53
+ start_period: 5s
54
+ retries: 3
43
55
 
44
56
  volumes:
45
57
  aidlc-coordination-data:
@@ -4,7 +4,9 @@
4
4
  "requires": true,
5
5
  "packages": {
6
6
  "": {
7
- "bin": { "aidlc-fleet": "src/fleet-run.mjs" },
7
+ "bin": {
8
+ "aidlc-fleet": "src/fleet-run.mjs"
9
+ },
8
10
  "dependencies": {
9
11
  "@modelcontextprotocol/express": "^2.0.0",
10
12
  "@modelcontextprotocol/node": "^2.0.0",
@@ -2,12 +2,16 @@
2
2
  "name": "@felixdotgo/aidlc-coordination",
3
3
  "private": true,
4
4
  "type": "module",
5
- "bin": { "aidlc-fleet": "src/fleet-run.mjs" },
6
- "engines": { "node": ">=20" },
5
+ "bin": {
6
+ "aidlc-fleet": "src/fleet-run.mjs"
7
+ },
8
+ "engines": {
9
+ "node": ">=20"
10
+ },
7
11
  "scripts": {
8
12
  "start": "node src/server.mjs",
9
13
  "test": "node --test test/*.test.mjs",
10
- "lint": "node --check src/repository.mjs && node --check src/coordination.mjs && node --check src/fleet.mjs && node --check src/fleet-client.mjs && node --check src/fleet-run.mjs && node --check src/server.mjs"
14
+ "lint": "node -e \"const { readdirSync } = require('node:fs'); const { execFileSync } = require('node:child_process'); for (const file of readdirSync('src').filter((name) => name.endsWith('.mjs'))) execFileSync(process.execPath, ['--check', 'src/' + file], { stdio: 'inherit' });\""
11
15
  },
12
16
  "dependencies": {
13
17
  "@modelcontextprotocol/express": "^2.0.0",
@@ -17,5 +21,6 @@
17
21
  "express": "^5.2.1",
18
22
  "pg": "^8.23.0",
19
23
  "zod": "^4.4.3"
20
- }
24
+ },
25
+ "version": "0.2.0"
21
26
  }
@@ -29,7 +29,7 @@ const lockRecord = (path) => {
29
29
  try {
30
30
  const value = JSON.parse(readFileSync(path, "utf8"));
31
31
  return typeof value.token === "string" && Number.isInteger(value.pid) && value.pid > 0 && Number.isFinite(value.createdAt) ? value : undefined;
32
- } catch { return undefined; }
32
+ } catch { return undefined; } /* intentional: unreadable record is treated as absent */
33
33
  };
34
34
  const processAlive = (pid) => {
35
35
  try { process.kill(pid, 0); return true; }
@@ -307,6 +307,60 @@ export const readyTaskItems = (task) => {
307
307
  return { ready, blocked };
308
308
  };
309
309
 
310
+ /* Parallel work partition: owned paths overlap by path segment (`a` overlaps `a/b`, not `ab`).
311
+ * Two open items can run concurrently unless one is a direct or transitive dependency of the other. */
312
+ const ownedPathKey = (path) => String(path).replace(/\/+$/, "");
313
+ export const pathOverlaps = (left, right) => { const a = ownedPathKey(left), b = ownedPathKey(right); return a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`); };
314
+ export const pathsOverlap = (left = [], right = []) => left.some((a) => right.some((b) => pathOverlaps(a, b)));
315
+ const itemAncestors = (task) => {
316
+ const byId = new Map(task.tasks.map((item) => [item.id, item])); const memo = new Map();
317
+ const walk = (id, trail = new Set()) => { if (memo.has(id)) return memo.get(id); const found = new Set(); if (trail.has(id)) return found; trail.add(id); for (const dep of byId.get(id)?.dependsOn ?? []) { if (!byId.has(dep)) continue; found.add(dep); for (const deeper of walk(dep, trail)) found.add(deeper); } trail.delete(id); memo.set(id, found); return found; };
318
+ return (id) => walk(id);
319
+ };
320
+ export const concurrentItemPairs = (task) => {
321
+ const open = task.tasks.filter((item) => !["done", "deferred"].includes(item.status)); const ancestors = itemAncestors(task); const pairs = [];
322
+ for (let i = 0; i < open.length; i += 1) for (let j = i + 1; j < open.length; j += 1) { const a = open[i], b = open[j]; if (!ancestors(a.id).has(b.id) && !ancestors(b.id).has(a.id)) pairs.push([a, b]); }
323
+ return pairs;
324
+ };
325
+ /** Approve-plan partition check. `strict` (enabled loop) makes findings ERRORs; otherwise WARNs. */
326
+ export const partitionDiagnostics = (task, { strict = true } = {}) => {
327
+ const level = strict ? "ERROR" : "WARN"; const diagnostics = [];
328
+ const unscoped = task.tasks.filter((item) => !["done", "deferred"].includes(item.status) && !(Array.isArray(item.ownedPaths) && item.ownedPaths.length));
329
+ if (unscoped.length) diagnostics.push({ level, code: "PARTITION_SCOPE", message: `Build items need owned paths (--owns) for parallel workers: ${unscoped.map((item) => item.id).join(", ")}` });
330
+ for (const [a, b] of concurrentItemPairs(task)) {
331
+ const shared = (a.ownedPaths ?? []).filter((path) => (b.ownedPaths ?? []).some((other) => pathOverlaps(path, other)));
332
+ if (shared.length) diagnostics.push({ level, code: "PARTITION_OVERLAP", message: `Items ${a.id} and ${b.id} can run concurrently but both own ${shared.join(", ")}; split the paths or add --depends-on` });
333
+ }
334
+ return diagnostics;
335
+ };
336
+ /** The partition check is strict when the loop is enabled: `loop.mode` local/fleet in the project config, or
337
+ * running inside a fleet-enabled coordination service. A missing config means the default `off`; an
338
+ * unreadable or corrupt config fails closed (strict). */
339
+ export const loopPartitionStrict = (root) => {
340
+ if (process.env.MCP_FLEET_ENABLED === "1") return true;
341
+ if (root === undefined) return false;
342
+ const path = join(resolve(root), ".agents", "config.json");
343
+ if (!existsSync(path)) return false;
344
+ try { const mode = JSON.parse(readFileSync(path, "utf8"))?.loop?.mode; return mode === "local" || mode === "fleet"; }
345
+ catch { return true; }
346
+ };
347
+ /** Pick the next dispatchable batch: dependency-ready items in plan order, at most `capacity`,
348
+ * skipping items that overlap each other, `busyPaths`, or are listed in `exclude`. Never throws for overlap. */
349
+ export const selectBuildBatch = (task, { capacity = Infinity, busyPaths = [], exclude = [] } = {}) => {
350
+ const byId = new Map(task.tasks.map((item) => [item.id, item])); const selected = []; const deferred = []; const taken = [];
351
+ for (const id of readyTaskItems(task).ready) {
352
+ const owned = byId.get(id)?.ownedPaths ?? [];
353
+ if (exclude.includes(id)) { deferred.push({ id, reason: "unresolved assignment" }); continue; }
354
+ if (!owned.length) { deferred.push({ id, reason: "no owned paths" }); continue; }
355
+ if (pathsOverlap(owned, busyPaths)) { deferred.push({ id, reason: "busy path held by an active assignment" }); continue; }
356
+ const clash = taken.find((entry) => pathsOverlap(owned, entry.owned));
357
+ if (clash) { deferred.push({ id, reason: `overlaps ${clash.id}` }); continue; }
358
+ if (selected.length >= capacity) { deferred.push({ id, reason: "capacity" }); continue; }
359
+ selected.push(id); taken.push({ id, owned });
360
+ }
361
+ return { selected, deferred };
362
+ };
363
+
310
364
  export const nextAction = (task, root) => {
311
365
  const rootSuffix = root === undefined ? "" : ` --root ${JSON.stringify(resolve(root))}`;
312
366
  const script = (name) => root === undefined ? `node .agents/aidlc/scripts/${name}` : `node ${JSON.stringify(join(resolve(root), ".agents/aidlc/scripts", name))}`;
@@ -357,13 +411,14 @@ export const searchLessons = (root, state, query, areas = [], limit = 5) => { co
357
411
 
358
412
  export const listTaskSummaries = (state, options = {}) => { const active = Object.values(state.tasks).map((task) => ({ id: task.id, title: task.title, phase: task.phase, gate: task.gate, status: task.status, risk: task.risk, areas: task.areas, updatedAt: task.updatedAt, source: "active" })); const archived = options.includeArchive ? Object.values(state.archive ?? {}).map((task) => ({ id: task.id, title: task.title, phase: task.phase, gate: task.gate, status: task.status, risk: task.risk, areas: task.areas, updatedAt: task.updatedAt, source: "archive" })) : []; const queryTokens = tokens(options.query ?? ""); const statusSet = new Set(options.statuses ?? []); const filtered = [...active, ...archived].filter((item) => (!statusSet.size || statusSet.has(item.status)) && (!queryTokens.length || queryTokens.every((token) => tokens(`${item.id} ${item.title} ${item.areas.join(" ")}`).includes(token)))).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || a.id.localeCompare(b.id)); const cursor = Math.max(0, options.cursor ?? 0); const limit = Math.max(1, Math.min(options.limit ?? 20, 100)); const items = filtered.slice(cursor, cursor + limit); return { items, nextCursor: cursor + items.length < filtered.length ? String(cursor + items.length) : null, total: filtered.length }; };
359
413
 
360
- export const renderWorkplan = (task) => { const decisions = task.decisions.length ? task.decisions.map((item) => `- [${item.status === "approved" ? "x" : " "}] ${item.id} — ${item.label}${item.resolution ? ` — ${item.resolution}` : ""}`).join("\n") : "- None"; const tasks = task.tasks.length ? task.tasks.map((item) => `- [${item.status === "done" ? "x" : item.status === "in_progress" ? "~" : " "}] ${item.id} — ${item.label}`).join("\n") : "- None"; const lifecycle = [`- Status: \`${task.status}\``, task.handoff ? `- Handoff: \`${task.handoff.kind}\` — ${task.handoff.reason}` : "", task.closure ? `- Closure: ${task.closure.reason}` : "", task.predecessorTaskId ? `- Predecessor: \`${task.predecessorTaskId}\`` : "", task.successorTaskId ? `- Successor: \`${task.successorTaskId}\`` : "", task.lessonDisposition ? `- Lessons: \`${task.lessonDisposition.status}\`` : ""].filter(Boolean).join("\n"); return `# Workplan — ${task.title} (\`${task.id}\`)\n\n> Generated from canonical JSON state.\n\n## Lifecycle\n${lifecycle}\n\n## 🧩 Decisions (Approve plan — before implementation)\n${decisions}\n\n## 🧩 Tasks (Review implementation — work items)\n${tasks}\n`; };
414
+ const workplanScope = (item) => { const parts = [item.ownedPaths?.length ? `owns: ${item.ownedPaths.join(", ")}` : "", item.dependsOn?.length ? `after: ${item.dependsOn.join(", ")}` : ""].filter(Boolean); return parts.length ? ` — ${parts.join(" · ")}` : ""; };
415
+ export const renderWorkplan = (task) => { const decisions = task.decisions.length ? task.decisions.map((item) => `- [${item.status === "approved" ? "x" : " "}] ${item.id} — ${item.label}${item.resolution ? ` — ${item.resolution}` : ""}`).join("\n") : "- None"; const tasks = task.tasks.length ? task.tasks.map((item) => `- [${item.status === "done" ? "x" : item.status === "in_progress" ? "~" : " "}] ${item.id} — ${item.label}${workplanScope(item)}`).join("\n") : "- None"; const lifecycle = [`- Status: \`${task.status}\``, task.handoff ? `- Handoff: \`${task.handoff.kind}\` — ${task.handoff.reason}` : "", task.closure ? `- Closure: ${task.closure.reason}` : "", task.predecessorTaskId ? `- Predecessor: \`${task.predecessorTaskId}\`` : "", task.successorTaskId ? `- Successor: \`${task.successorTaskId}\`` : "", task.lessonDisposition ? `- Lessons: \`${task.lessonDisposition.status}\`` : ""].filter(Boolean).join("\n"); return `# Workplan — ${task.title} (\`${task.id}\`)\n\n> Generated from canonical JSON state.\n\n## Lifecycle\n${lifecycle}\n\n## 🧩 Decisions (Approve plan — before implementation)\n${decisions}\n\n## 🧩 Tasks (Review implementation — work items)\n${tasks}\n`; };
361
416
  export const renderViews = (root, state = loadState(root), selected = Object.values(state.tasks)) => { for (const task of selected) { if (!task.artifacts.workplan) continue; assertNoSymlinkPath(root, task.artifacts.workplan); writeAtomic(join(resolve(root), task.artifacts.workplan), renderWorkplan(task)); } };
362
417
 
363
418
  const safeArtifactTarget = (root, path) => { assertNoSymlinkPath(root, path); return join(resolve(root), path); };
364
- const artifactContent = (root, path) => { if (!path) return undefined; try { const target = safeArtifactTarget(root, path); return existsSync(target) ? readFileSync(target, "utf8") : undefined; } catch { return undefined; } };
419
+ const artifactContent = (root, path) => { if (!path) return undefined; try { const target = safeArtifactTarget(root, path); return existsSync(target) ? readFileSync(target, "utf8") : undefined; } catch { return undefined; } /* intentional: unreadable/unsafe artifact is treated as absent */ };
365
420
  const artifactDiagnostics = (root, task, names) => names.flatMap((name) => { const path = task.artifacts[name]; if (!path) return [{ level: "ERROR", code: "ARTIFACT_REFERENCE", message: `${name} artifact is not referenced` }]; try { return existsSync(safeArtifactTarget(root, path)) ? [] : [{ level: "ERROR", code: "ARTIFACT_MISSING", message: `${path} does not exist` }]; } catch (error) { return [{ level: "ERROR", code: "ARTIFACT_UNSAFE", message: error instanceof Error ? error.message : String(error) }]; } });
366
- export const checkGate = (root, state, taskId, gate) => { const task = state.tasks[taskId]; if (!task) return [{ level: "ERROR", code: "TASK_UNKNOWN", message: `Unknown active task: ${taskId}` }]; const diagnostics = []; if (task.gate !== gate) diagnostics.push({ level: "ERROR", code: "GATE_STATE", message: `Task is at ${task.gate}, not ${gate}` }); if (["closed", "superseded"].includes(task.status)) diagnostics.push({ level: "ERROR", code: "TASK_TERMINAL", message: `Terminal task cannot approve ${gate}` }); if (task.handoff) diagnostics.push({ level: "ERROR", code: "TASK_HANDOFF", message: `Task has unresolved handoff: ${task.handoff.reason}` }); if (task.id !== taskId) diagnostics.push({ level: "ERROR", code: "TASK_ID", message: "Task key and id differ" }); if (!task.title.trim() || !task.areas.length) diagnostics.push({ level: "ERROR", code: "TASK_FIELDS", message: "Task title and affected areas are required" }); if (gate === "G0_confirm") { diagnostics.push(...artifactDiagnostics(root, task, ["intent"])); const content = artifactContent(root, task.artifacts.intent); if (content !== undefined) for (const heading of ["## 📋 Problem", "## 🗺️ Affected areas", "## 💭 Assumptions", "## ❓ Open questions", "## 🎯 Scope"]) if (!content.includes(heading)) diagnostics.push({ level: "ERROR", code: "INTENT_HEADING", message: `Intent is missing ${heading}` }); } if (gate === "G1_review") { diagnostics.push(...artifactDiagnostics(root, task, ["intent", "design", "workplan"])); diagnostics.push(...transitionDiagnostics(task, "build").filter((item) => item.code === "UNRESOLVED_DECISION")); const content = artifactContent(root, task.artifacts.design); if (content !== undefined) for (const heading of ["## 🧩 Solution per affected area", "## 📌 Spec traceability", "## 🔗 Cross-service contracts", "## ⚠️ Risks / edge cases"]) if (!content.includes(heading)) diagnostics.push({ level: "ERROR", code: "DESIGN_HEADING", message: `Design is missing ${heading}` }); } if (gate === "G2_codereview") { diagnostics.push(...artifactDiagnostics(root, task, ["intent", "design", "workplan"])); if (task.tasks.some((item) => !["done", "deferred"].includes(item.status))) diagnostics.push({ level: "ERROR", code: "TASKS_OPEN", message: "Build tasks remain open" }); for (const area of task.areas) if (!hasAreaVerification(task, area)) diagnostics.push({ level: "ERROR", code: "VERIFY_EVIDENCE", message: `Latest post-G1 verification evidence must pass for affected area: ${area}` }); if (!hasReview(task)) diagnostics.push({ level: "ERROR", code: "REVIEW_EVIDENCE", message: "Latest post-G1 code-review evidence must pass" }); const bounds = repairBounds(task); if (!isLegacyG2Wait(task) && (bounds.verifyExhausted || bounds.reviewExhausted)) diagnostics.push({ level: "ERROR", code: "REPAIR_BOUND", message: "Repair bound exhausted; record a durable handoff instead of presenting G2" }); } if (!diagnostics.length) diagnostics.push({ level: "INFO", code: "GATE_OK", message: `${gate} checks passed for ${taskId}` }); return diagnostics; };
421
+ export const checkGate = (root, state, taskId, gate) => { const task = state.tasks[taskId]; if (!task) return [{ level: "ERROR", code: "TASK_UNKNOWN", message: `Unknown active task: ${taskId}` }]; const diagnostics = []; if (task.gate !== gate) diagnostics.push({ level: "ERROR", code: "GATE_STATE", message: `Task is at ${task.gate}, not ${gate}` }); if (["closed", "superseded"].includes(task.status)) diagnostics.push({ level: "ERROR", code: "TASK_TERMINAL", message: `Terminal task cannot approve ${gate}` }); if (task.handoff) diagnostics.push({ level: "ERROR", code: "TASK_HANDOFF", message: `Task has unresolved handoff: ${task.handoff.reason}` }); if (task.id !== taskId) diagnostics.push({ level: "ERROR", code: "TASK_ID", message: "Task key and id differ" }); if (!task.title.trim() || !task.areas.length) diagnostics.push({ level: "ERROR", code: "TASK_FIELDS", message: "Task title and affected areas are required" }); if (gate === "G0_confirm") { diagnostics.push(...artifactDiagnostics(root, task, ["intent"])); const content = artifactContent(root, task.artifacts.intent); if (content !== undefined) for (const heading of ["## 📋 Problem", "## 🗺️ Affected areas", "## 💭 Assumptions", "## ❓ Open questions", "## 🎯 Scope"]) if (!content.includes(heading)) diagnostics.push({ level: "ERROR", code: "INTENT_HEADING", message: `Intent is missing ${heading}` }); } if (gate === "G1_review") { diagnostics.push(...artifactDiagnostics(root, task, ["intent", "design", "workplan"])); diagnostics.push(...transitionDiagnostics(task, "build").filter((item) => item.code === "UNRESOLVED_DECISION")); const content = artifactContent(root, task.artifacts.design); if (content !== undefined) for (const heading of ["## 🧩 Solution per affected area", "## 📌 Spec traceability", "## 🔗 Cross-service contracts", "## ⚠️ Risks / edge cases"]) if (!content.includes(heading)) diagnostics.push({ level: "ERROR", code: "DESIGN_HEADING", message: `Design is missing ${heading}` }); diagnostics.push(...partitionDiagnostics(task, { strict: loopPartitionStrict(root) })); } if (gate === "G2_codereview") { diagnostics.push(...artifactDiagnostics(root, task, ["intent", "design", "workplan"])); if (task.tasks.some((item) => !["done", "deferred"].includes(item.status))) diagnostics.push({ level: "ERROR", code: "TASKS_OPEN", message: "Build tasks remain open" }); for (const area of task.areas) if (!hasAreaVerification(task, area)) diagnostics.push({ level: "ERROR", code: "VERIFY_EVIDENCE", message: `Latest post-G1 verification evidence must pass for affected area: ${area}` }); if (!hasReview(task)) diagnostics.push({ level: "ERROR", code: "REVIEW_EVIDENCE", message: "Latest post-G1 code-review evidence must pass" }); const bounds = repairBounds(task); if (!isLegacyG2Wait(task) && (bounds.verifyExhausted || bounds.reviewExhausted)) diagnostics.push({ level: "ERROR", code: "REPAIR_BOUND", message: "Repair bound exhausted; record a durable handoff instead of presenting G2" }); } if (!diagnostics.some((item) => item.level === "ERROR")) diagnostics.push({ level: "INFO", code: "GATE_OK", message: `${gate} checks passed for ${taskId}` }); return diagnostics; };
367
422
  const gateTarget = { G0_confirm: "plan", G1_review: "build", G2_codereview: "wrap" };
368
423
  export const approveAndAdvance = (root, state, taskId, gate, source, recordedAt = new Date().toISOString()) => { const task = state.tasks[taskId]; if (!task) throw new Error(`Unknown active task: ${taskId}`); if (["closed", "superseded"].includes(task.status) || task.handoff) throw new Error(`Terminal or handed-off task cannot approve ${gate}`); const target = gateTarget[gate]; if (!target) throw new Error(`Gate ${gate} cannot be approved`); if (task.phase === target) return { task, nextAction: nextAction(task, root), idempotent: true }; if (task.gate !== gate || task.status !== "blocked_on_user") throw new Error(`Task must be blocked_on_user at ${gate}`); const errors = checkGate(root, state, taskId, gate).filter((item) => item.level === "ERROR"); if (errors.length) throw new Error(errors.map((item) => item.message).join("; ")); task.evidence.push({ kind: "approval", gate, result: "pass", source, recordedAt }); transitionTask(state, taskId, target); return { task, nextAction: nextAction(task, root), idempotent: false }; };
369
424
  export const formatDiagnostics = (diagnostics) => diagnostics.map((item) => `${item.level} ${item.code}: ${item.message}`).join("\n");