buildline 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buildline",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Install an AI software factory into any project. A spec-driven pipeline for Claude Code (and any AGENTS.md-compatible agent), for developers, designers and product managers.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/features.js CHANGED
@@ -214,7 +214,8 @@ export function readActivity(root, limit = 200) {
214
214
  * Claude Code), a feature whose files changed in the last two minutes counts as worked on.
215
215
  */
216
216
  export function liveState(feature, sessions, at = Date.now()) {
217
- const mine = sessions.filter((s) => s.feature === feature.id);
217
+ // A finished feature has nothing live, even if an old session is still tied to it.
218
+ const mine = feature.status === 'done' ? [] : sessions.filter((s) => s.feature === feature.id);
218
219
  const working = mine.filter((s) => s.state === 'working');
219
220
  const recent = at - new Date(feature.updated || 0) < 2 * 60e3;
220
221
  const waiting = mine.find((s) => s.state === 'waiting');
@@ -113,6 +113,17 @@ function promptDetail(prompt) {
113
113
  return done ? oneLine(done[1], 90) : oneLine(prompt, 60);
114
114
  }
115
115
 
116
+ /** A feature.mjs command that changes a feature's state, e.g. "feature.mjs task 3 T1 pass". */
117
+ function actedOn(command) {
118
+ const m = /feature\.mjs\s+(?:move|task|log|block|unblock|approve)\s+(\d{1,4})\b/.exec(String(command || ''));
119
+ return m ? m[1].padStart(3, '0') : null;
120
+ }
121
+
122
+ function isDone(id) {
123
+ const dir = fs.existsSync(featuresDir) ? fs.readdirSync(featuresDir).find((d) => d.startsWith(`${id}-`)) : null;
124
+ return dir ? readJson(path.join(featuresDir, dir, 'feature.json'))?.status === 'done' : false;
125
+ }
126
+
116
127
  function detail(input) {
117
128
  const ti = input.tool_input || {};
118
129
  if (ti.file_path) return path.basename(ti.file_path);
@@ -189,12 +200,15 @@ function heartbeat(input, event) {
189
200
  const prompt = input.prompt || '';
190
201
  const station = /\/buildline-([a-z]+)/.exec(prompt)?.[1];
191
202
  const ti = input.tool_input || {};
192
- // A session stays on the feature it was started for; reading another feature's files doesn't move it.
193
- const feature = featureRef(prompt) || prev.feature || featureRef(ti.file_path) || featureRef(ti.command) || featureRef(ti.prompt) || null;
203
+ // A session stays on the feature it was started for; reading another feature's files doesn't move it,
204
+ // recording progress on one does. It lets go once its feature is done or the conversation is cleared,
205
+ // so a reopened session doesn't keep flagging a finished feature.
206
+ const bound = prev.feature && !isDone(prev.feature) && !(event === 'start' && input.source === 'clear') ? prev.feature : null;
207
+ const feature = featureRef(prompt) || actedOn(ti.command) || bound || featureRef(ti.file_path) || featureRef(ti.command) || featureRef(ti.prompt) || null;
194
208
  const next = {
195
209
  ...prev,
196
210
  feature,
197
- station: station || prev.station || null,
211
+ station: station || (feature === prev.feature ? prev.station : null) || null,
198
212
  model: input.model?.id || input.model || prev.model || null,
199
213
  ...nextState(prev, input, event),
200
214
  lastAt: at,