@magnusekdahl/parallix 1.0.0 → 1.0.2

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.
@@ -33,14 +33,45 @@ const { resolveStageTelemetry } = require('../agents/stage-telemetry');
33
33
  // guarantees at most one of implementer/reviewer is codex); other families
34
34
  // record honest zeros (task-1251).
35
35
  //
36
- // Codex writes a fresh-counter rollout per round, so we sum total_token_usage
37
- // across every rollout since the review loop started (`sinceMs`). This is
38
- // recomputed from the rollout files each round — idempotent across the workflow
39
- // process being resumed mid-mission — and the per-(mission,stage) upsert leaves
40
- // the codex role's full multi-round total. `result.telemetry` is used only as a
41
- // signal that codex ran this round, so the window-sum is attributed to the right
42
- // role and never double-counted onto the non-codex stage.
43
- function recordStageStatsSafe(kind, { slug, rootDir, worktree, implementer, reviewer, result, sinceMs, log, error }) {
36
+ // Codex writes a fresh-counter rollout per launch, so we sum total_token_usage
37
+ // across every rollout since the FIRST launch for this stored mission phase and
38
+ // agent family. For non-Codex families we accumulate launcher-attached telemetry
39
+ // one launch at a time, de-duped via review-state metadata, so Claude/Qwen rows
40
+ // are cumulative too. A family switch creates a separate row instead of mixing
41
+ // agents in one record.
42
+ function stageLaunchFingerprint(agentFamily, result) {
43
+ return [
44
+ String(agentFamily || '').trim().toLowerCase(),
45
+ result && result.sessionId ? String(result.sessionId) : '',
46
+ result && result.startedAt ? String(result.startedAt) : '',
47
+ result && result.endedAt ? String(result.endedAt) : '',
48
+ result && result.status !== undefined && result.status !== null ? String(result.status) : '',
49
+ ].join('|');
50
+ }
51
+
52
+ function markStageLaunchRecorded(state, { stage, agentFamily, result, slug, worktree, writeReviewStateFn = writeReviewState } = {}) {
53
+ if (!state || !agentFamily) return true;
54
+ const key = stageWindowKey(stage, agentFamily);
55
+ const fingerprint = stageLaunchFingerprint(agentFamily, result);
56
+ const recordedStageLaunches = state.metadata && typeof state.metadata === 'object'
57
+ ? (state.metadata.recordedStageLaunches || {})
58
+ : {};
59
+ const recorded = Array.isArray(recordedStageLaunches[key]) ? recordedStageLaunches[key] : [];
60
+ if (recorded.includes(fingerprint)) {
61
+ return false;
62
+ }
63
+ if (!state.metadata || typeof state.metadata !== 'object') {
64
+ state.metadata = {};
65
+ }
66
+ state.metadata.recordedStageLaunches = {
67
+ ...(state.metadata.recordedStageLaunches || {}),
68
+ [key]: [...recorded, fingerprint].slice(-20),
69
+ };
70
+ writeReviewStateFn(slug, state, worktree);
71
+ return true;
72
+ }
73
+
74
+ function recordStageStatsSafe(kind, { stage, slug, rootDir, worktree, implementer, reviewer, result, sinceMs, log, error, state, writeReviewStateFn = writeReviewState }) {
44
75
  let durationMinutes = 0;
45
76
  if (result && result.startedAt && result.endedAt) {
46
77
  durationMinutes = (Date.parse(result.endedAt) - Date.parse(result.startedAt)) / 60000;
@@ -51,8 +82,18 @@ function recordStageStatsSafe(kind, { slug, rootDir, worktree, implementer, revi
51
82
  // (task-1274), bounded to this stage by sinceMs (task-1285 F6).
52
83
  const telemetry = resolveStageTelemetry({ worktree, result, sinceMs });
53
84
  try {
54
- const fn = kind === 'review' ? stats.recordReviewStats : stats.recordActiveStats;
55
- fn({ slug, rootDir, implementer, reviewer, telemetry, durationMinutes });
85
+ const actorFamily = kind === 'review' ? reviewer : implementer;
86
+ if (state && !markStageLaunchRecorded(state, {
87
+ stage,
88
+ agentFamily: actorFamily,
89
+ result,
90
+ slug,
91
+ worktree,
92
+ writeReviewStateFn
93
+ })) {
94
+ return;
95
+ }
96
+ stats.accumulateStageStats({ stage, slug, rootDir, implementer, reviewer, telemetry, durationMinutes });
56
97
  } catch (err) {
57
98
  // Best-effort: never escalate to the fatal `error` channel.
58
99
  log(fmt.status('WARN', `Could not record ${kind} stats for ${slug}: ${err.message}`));
@@ -135,6 +176,25 @@ function persistNormalizedPhaseRepair(slug, state, worktree, { log = fmt.log.pla
135
176
  state.phaseOriginal = null;
136
177
  }
137
178
 
179
+ function stageWindowKey(stage, agentFamily) {
180
+ const normalizedStage = String(stage || 'default').trim().toLowerCase() || 'default';
181
+ const normalizedAgent = String(agentFamily || '').trim().toLowerCase();
182
+ return `${normalizedStage}:${normalizedAgent}`;
183
+ }
184
+
185
+ // Window the Codex telemetry read to the CURRENT launch's start so each round
186
+ // contributes only its own rollouts. Earlier rounds' rollouts have an mtime
187
+ // strictly before this launch's startedAt and are excluded, so the per-round
188
+ // deltas accumulate (via accumulateStageStats) to the family's true total
189
+ // instead of re-summing a cumulative-since-first-launch window every round.
190
+ // Resume-idempotency is provided separately by the launch fingerprint in
191
+ // markStageLaunchRecorded, so the window itself does not need to persist.
192
+ function stageLaunchSinceMs(result) {
193
+ const startedAt = result && result.startedAt ? String(result.startedAt) : '';
194
+ const startedMs = startedAt ? Date.parse(startedAt) : NaN;
195
+ return Number.isFinite(startedMs) ? startedMs : 0;
196
+ }
197
+
138
198
  // ============================================================================
139
199
  // Main Review Loop
140
200
  // ============================================================================
@@ -200,7 +260,8 @@ async function startReviewLoop(slug, opts = {}) {
200
260
  gitFn = git,
201
261
  isReviewProviderEnabledFn = undefined,
202
262
  legacyIsForgejoReviewEnabledFn = null,
203
- isForgejoReviewEnabledFn = null
263
+ isForgejoReviewEnabledFn = null,
264
+ recordStageStatsSafeFn = () => {}
204
265
  } = opts;
205
266
  let prNumber = null;
206
267
  isContinue = Boolean(isContinue || continueFlag);
@@ -635,13 +696,14 @@ async function startReviewLoop(slug, opts = {}) {
635
696
 
636
697
  // Record review-stage telemetry immediately after the reviewer runs,
637
698
  // so the worktree's newest codex rollout is this reviewer's session.
638
- // Codex sessions are resumed across rounds, so total_token_usage is
639
- // cumulative for the reviewer role; the per-round upsert overwrites,
640
- // leaving the final row = the reviewer's full-mission usage.
641
- recordStageStatsSafe('review', {
642
- slug, rootDir: worktree, worktree, reviewer, implementer,
699
+ // The window is bounded to this round's launch so each round adds
700
+ // only its own usage; accumulateStageStats sums the rounds into one
701
+ // cumulative row per reviewer family.
702
+ const reviewSinceMs = stageLaunchSinceMs(reviewerLaunchResult && reviewerLaunchResult.result);
703
+ recordStageStatsSafeFn('review', {
704
+ stage: 'review', slug, rootDir: worktree, worktree, reviewer, implementer,
643
705
  result: reviewerLaunchResult && reviewerLaunchResult.result,
644
- sinceMs: state.startedAt ? Date.parse(state.startedAt) : 0, log, error
706
+ sinceMs: reviewSinceMs, log, error, state, writeReviewStateFn
645
707
  });
646
708
  }
647
709
 
@@ -850,13 +912,15 @@ async function startReviewLoop(slug, opts = {}) {
850
912
  state, slug, worktree, taskResolution, log, writeReviewStateFn, enforceTaskAssigneeFn
851
913
  });
852
914
 
853
- // Record active-stage telemetry immediately after the implementer runs.
854
- // Same cumulative-session semantics as the review stage above; the final
855
- // round's upsert leaves the implementer's full-mission usage.
856
- recordStageStatsSafe('active', {
857
- slug, rootDir: worktree, worktree, implementer, reviewer,
915
+ // Record follow-up telemetry immediately after the implementer runs.
916
+ // This is intentionally a separate stored phase from the initial
917
+ // execute/active launch. The window is bounded to this round's launch
918
+ // so each act-on-review round adds only its own usage.
919
+ const followUpSinceMs = stageLaunchSinceMs(implementerLaunchResult && implementerLaunchResult.result);
920
+ recordStageStatsSafeFn('active', {
921
+ stage: 'follow-up', slug, rootDir: worktree, worktree, implementer, reviewer,
858
922
  result: implementerLaunchResult && implementerLaunchResult.result,
859
- sinceMs: state.startedAt ? Date.parse(state.startedAt) : 0, log, error
923
+ sinceMs: followUpSinceMs, log, error, state, writeReviewStateFn
860
924
  });
861
925
  }
862
926
 
@@ -1000,5 +1064,7 @@ module.exports = {
1000
1064
  rebaseBeforeReviewRound,
1001
1065
  applyAgentFallback,
1002
1066
  persistNormalizedPhaseRepair,
1003
- startReviewLoop
1067
+ stageLaunchSinceMs,
1068
+ startReviewLoop,
1069
+ recordStageStatsSafe
1004
1070
  };
@@ -16,7 +16,7 @@ const { POLL_TIMEOUT, delay, resolvePollIntervalMs, resolvePollTimeoutMs, format
16
16
  const { buildMetadataFooter, reviewArtifactPath, readArtifactFile, deleteArtifactFile, normalizeReviewVerdict, normalizeDisposition, postWorkflowComment, postWorkflowReview, consumeReviewerArtifacts, consumeImplementerArtifacts } = require('./review-artifacts');
17
17
 
18
18
  // Re-export from review-loop module
19
- const { maybeUpdateGraphifyBeforeReview, commitSafeMissionArtifacts, rebaseBeforeReviewRound, applyAgentFallback, persistNormalizedPhaseRepair, startReviewLoop } = require('./review-loop');
19
+ const { maybeUpdateGraphifyBeforeReview, commitSafeMissionArtifacts, rebaseBeforeReviewRound, applyAgentFallback, persistNormalizedPhaseRepair, startReviewLoop, recordStageStatsSafe } = require('./review-loop');
20
20
 
21
21
  // Re-export from review-commands module
22
22
  const {
@@ -3,15 +3,18 @@ const path = require('path');
3
3
  const { git } = require('../core/git');
4
4
  const { WORKFLOW_AGENT_NAMES } = require('../agents/agents');
5
5
  const fmt = require('../core/fmt');
6
+ const { resolveTaskStorage } = require('../core/product-config');
6
7
 
7
8
  function getSupportedAgents() {
8
9
  return WORKFLOW_AGENT_NAMES;
9
10
  }
10
11
 
12
+ function getTaskStorage(rootDir = process.cwd()) {
13
+ return resolveTaskStorage(rootDir);
14
+ }
15
+
11
16
  function findTaskFiles(slug, rootDir = process.cwd()) {
12
- const tasksDir = path.join(rootDir, 'backlog', 'tasks');
13
- const completedDir = path.join(rootDir, 'backlog', 'completed');
14
- const archivedDir = path.join(rootDir, 'backlog', 'archive', 'tasks');
17
+ const { tasksDir, completedDir, archiveTasksDir: archivedDir } = getTaskStorage(rootDir);
15
18
  const sortByBacklogState = (filePath) => {
16
19
  if (filePath.startsWith(tasksDir + path.sep)) return 0;
17
20
  if (filePath.startsWith(completedDir + path.sep)) return 1;
@@ -35,7 +38,7 @@ function findTaskFiles(slug, rootDir = process.cwd()) {
35
38
  function resolveTaskFile(slug, rootDir = process.cwd()) {
36
39
  let matches = findTaskFiles(slug, rootDir);
37
40
  const normalizedId = slug.toUpperCase();
38
- const archivedDir = path.join(rootDir, 'backlog', 'archive', 'tasks');
41
+ const { tasksDir, completedDir, archiveTasksDir: archivedDir } = getTaskStorage(rootDir);
39
42
  const preferSameTaskInHigherPriorityDir = (candidateMatches) => {
40
43
  if (candidateMatches.length < 2) return null;
41
44
  const [preferred, ...rest] = candidateMatches;
@@ -56,8 +59,6 @@ function resolveTaskFile(slug, rootDir = process.cwd()) {
56
59
 
57
60
  if (matches.length === 0) {
58
61
  // Hardening: If no prefix match, search ALL task files for an exact ID match
59
- const tasksDir = path.join(rootDir, 'backlog', 'tasks');
60
- const completedDir = path.join(rootDir, 'backlog', 'completed');
61
62
  const allFiles = [
62
63
  ...(fs.existsSync(tasksDir) ? fs.readdirSync(tasksDir).map(f => path.join(tasksDir, f)) : []),
63
64
  ...(fs.existsSync(completedDir) ? fs.readdirSync(completedDir).map(f => path.join(completedDir, f)) : []),
@@ -121,6 +122,9 @@ function findTaskFile(slug, rootDir = process.cwd()) {
121
122
 
122
123
  function reportTaskResolution(result, slug, log = fmt.log.plain) {
123
124
  if (result.ok) return;
125
+ const { tasksDir, completedDir } = getTaskStorage(process.cwd());
126
+ const taskHint = path.relative(process.cwd(), tasksDir).split(path.sep).join('/');
127
+ const completedHint = path.relative(process.cwd(), completedDir).split(path.sep).join('/');
124
128
 
125
129
  if (result.reason === 'ambiguous') {
126
130
  log(fmt.status('FAIL', `Backlog task resolution is ambiguous for slug: ${fmt.slug(slug)}`));
@@ -128,21 +132,32 @@ function reportTaskResolution(result, slug, log = fmt.log.plain) {
128
132
  result.matches.forEach(m => log(` - ${m}`));
129
133
  log(fmt.status('INFO', 'Repair: Ensure only one task has the filename prefix or matching "id:" frontmatter.'));
130
134
  } else {
131
- log(fmt.status('FAIL', `Backlog task for ${fmt.slug(slug)} not found in backlog/tasks/ or backlog/completed/.`));
135
+ log(fmt.status('FAIL', `Backlog task for ${fmt.slug(slug)} not found in ${fmt.path(taskHint)}/ or ${fmt.path(completedHint)}/.`));
132
136
  log(fmt.status('INFO', 'Create the task file first.'));
133
- log(fmt.status('INFO', 'Repair: px draft requires an existing Backlog.md-style task file.'));
134
- log(fmt.status('INFO', `If Backlog.md is installed, create one with ${fmt.command(`backlog_task_create --title "Mission: ${slug}"`)}.`));
135
- log(fmt.status('INFO', `Without Backlog.md, create ${fmt.path(`backlog/tasks/${slug} - <title>.md`)} with frontmatter id ${fmt.bold(slug.toUpperCase())}.`));
137
+ log(fmt.status('INFO', 'Repair: create the task with your task adapter, or add a markdown task file manually.'));
138
+ log(fmt.status('INFO', `Manual fallback: create ${fmt.path(`${taskHint}/${slug} - <title>.md`)} with frontmatter id ${fmt.bold(slug.toUpperCase())}.`));
136
139
  }
137
140
  }
138
141
 
142
+ function taskIdFromFilename(file) {
143
+ // Extract ID from filename prefix (e.g., task-093 or task-093.01)
144
+ const filenameMatch = file.match(/^(task-\d+(?:\.\d+)?)/i);
145
+ return filenameMatch ? filenameMatch[1].toUpperCase() : null;
146
+ }
147
+
139
148
  function checkBacklogIntegrity(rootDir = process.cwd(), slug = null) {
140
- const tasksDir = path.join(rootDir, 'backlog', 'tasks');
141
- const completedDir = path.join(rootDir, 'backlog', 'completed');
149
+ const { tasksDir, completedDir, archiveTasksDir: archivedDir } = getTaskStorage(rootDir);
142
150
  const issues = [];
143
151
  const normalizedSlug = slug ? slug.toLowerCase() : null;
144
152
 
145
- const scan = (dir) => {
153
+ // Track where each task id lives so we can detect the same id appearing in
154
+ // both backlog/tasks/ and backlog/completed/ (or backlog/archive/) — the
155
+ // reorder-recreates-completed-task defect (TASK-1343). The completed/archive
156
+ // copy is canonical; a backlog/tasks/ copy alongside it is stale.
157
+ const tasksLocations = new Map(); // id -> rel path in tasks/
158
+ const canonicalLocations = new Map(); // id -> rel path in completed|archive
159
+
160
+ const scan = (dir, { canonical = false } = {}) => {
146
161
  if (!fs.existsSync(dir)) return;
147
162
  const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
148
163
  for (const file of files) {
@@ -150,22 +165,26 @@ function checkBacklogIntegrity(rootDir = process.cwd(), slug = null) {
150
165
  continue;
151
166
  }
152
167
  const filePath = path.join(dir, file);
168
+ const filenameId = taskIdFromFilename(file);
169
+ if (!filenameId) continue;
170
+ const relPath = path.relative(rootDir, filePath);
171
+
172
+ if (canonical) {
173
+ if (!canonicalLocations.has(filenameId)) canonicalLocations.set(filenameId, relPath);
174
+ } else if (!tasksLocations.has(filenameId)) {
175
+ tasksLocations.set(filenameId, relPath);
176
+ }
177
+
153
178
  try {
154
179
  const content = fs.readFileSync(filePath, 'utf8');
155
180
 
156
- // Extract ID from filename prefix (e.g., task-093 or task-093.01)
157
- const filenameMatch = file.match(/^(task-\d+(?:\.\d+)?)/i);
158
- if (!filenameMatch) continue;
159
-
160
- const filenameId = filenameMatch[1].toUpperCase();
161
-
162
181
  // Extract ID from frontmatter
163
182
  const idMatch = content.match(/^id:\s*([^\r\n]+)/m);
164
183
  if (idMatch) {
165
184
  const frontmatterId = idMatch[1].trim().toUpperCase();
166
185
  if (frontmatterId !== filenameId) {
167
186
  issues.push({
168
- file: path.relative(rootDir, filePath),
187
+ file: relPath,
169
188
  type: 'id-mismatch',
170
189
  filenameId,
171
190
  frontmatterId
@@ -179,10 +198,51 @@ function checkBacklogIntegrity(rootDir = process.cwd(), slug = null) {
179
198
  };
180
199
 
181
200
  scan(tasksDir);
182
- scan(completedDir);
201
+ scan(completedDir, { canonical: true });
202
+ scan(archivedDir, { canonical: true });
203
+
204
+ for (const [id, taskPath] of tasksLocations) {
205
+ const canonicalPath = canonicalLocations.get(id);
206
+ if (canonicalPath) {
207
+ issues.push({
208
+ file: taskPath,
209
+ type: 'duplicate-completed',
210
+ taskId: id,
211
+ canonicalFile: canonicalPath
212
+ });
213
+ }
214
+ }
215
+
183
216
  return issues;
184
217
  }
185
218
 
219
+ /**
220
+ * Drop backlog/tasks/ copies of task ids whose canonical record already lives
221
+ * in backlog/completed/ or backlog/archive/. Used to make any board mutation
222
+ * (reorder / ordinal write) completed- and archive-aware: the completed copy is
223
+ * treated as canonical and the stale backlog/tasks/ copy is removed. Returns
224
+ * the list of removed { taskId, file, canonicalFile } records. See TASK-1343.
225
+ */
226
+ function pruneStaleBacklogDuplicates(rootDir = process.cwd()) {
227
+ const removed = [];
228
+ const duplicates = checkBacklogIntegrity(rootDir)
229
+ .filter(issue => issue.type === 'duplicate-completed');
230
+
231
+ for (const dup of duplicates) {
232
+ const absPath = path.join(rootDir, dup.file);
233
+ try {
234
+ if (fs.existsSync(absPath)) {
235
+ fs.rmSync(absPath);
236
+ removed.push({ taskId: dup.taskId, file: dup.file, canonicalFile: dup.canonicalFile });
237
+ }
238
+ } catch (_) {
239
+ // ignore removal errors; the integrity gate will still surface the duplicate
240
+ }
241
+ }
242
+
243
+ return removed;
244
+ }
245
+
186
246
  function getTaskStatus(taskFilePath) {
187
247
  if (!taskFilePath || !fs.existsSync(taskFilePath)) return null;
188
248
  const content = fs.readFileSync(taskFilePath, 'utf8');
@@ -233,8 +293,7 @@ function completeTask(slug, rootDir = process.cwd()) {
233
293
 
234
294
  const taskFilePath = resolution.taskFile;
235
295
  const fileName = path.basename(taskFilePath);
236
- const tasksDir = path.join(rootDir, 'backlog', 'tasks');
237
- const completedDir = path.join(rootDir, 'backlog', 'completed');
296
+ const { tasksDir, completedDir } = getTaskStorage(rootDir);
238
297
 
239
298
  if (!taskFilePath.includes(tasksDir)) {
240
299
  // Already in completed or somewhere else
@@ -501,6 +560,7 @@ function getTaskClassification(taskFilePath) {
501
560
  .map(s => s.toLowerCase());
502
561
  if (labels.includes('ai_sdlc')) matches.add('ai_sdlc');
503
562
  if (labels.includes('user_value')) matches.add('user_value');
563
+ if (labels.includes('unknown')) matches.add('unknown');
504
564
  }
505
565
 
506
566
  const inlineMatch = content.match(/^labels:[ \t]*\[(.*?)\]/m);
@@ -510,6 +570,7 @@ function getTaskClassification(taskFilePath) {
510
570
  .map(s => s.toLowerCase());
511
571
  if (labels.includes('ai_sdlc')) matches.add('ai_sdlc');
512
572
  if (labels.includes('user_value')) matches.add('user_value');
573
+ if (labels.includes('unknown')) matches.add('unknown');
513
574
  }
514
575
 
515
576
  return matches.size === 1 ? [...matches][0] : null;
@@ -662,6 +723,8 @@ module.exports = {
662
723
  resolveTaskFile,
663
724
  reportTaskResolution,
664
725
  checkBacklogIntegrity,
726
+ pruneStaleBacklogDuplicates,
727
+ getTaskStorage,
665
728
  getTaskStatus,
666
729
  setTaskStatus,
667
730
  transitionTask,
@@ -3,7 +3,7 @@ const path = require('path');
3
3
  const fmt = require('../core/fmt');
4
4
 
5
5
  const { findMissionDir, findCheckpoints, missionDirForSlug, missionBranchName } = require('../core/mission-utils');
6
- const { resolveTaskFile } = require('./backlog');
6
+ const { resolveTaskFile, getTaskStorage } = require('./backlog');
7
7
  const { readToken, postReview } = require('./forgejo');
8
8
 
9
9
  const DEFAULT_GATEKEEPER_USER = 'forgejo-gatekeeper';
@@ -42,8 +42,11 @@ function checkMandatoryFiles(slug, options = {}) {
42
42
  }
43
43
 
44
44
  const taskResolution = resolveTaskFileFn(slug, rootDir);
45
- if (!taskResolution || !taskResolution.ok) {
46
- missing.push('backlog/tasks/' + slug + ' - *.md');
45
+ const missionArtifactsPresent = Boolean(missionPath && fs.existsSync(missionPath) && missionDir && findCheckpointsFn(missionDir).length > 0);
46
+ if ((!taskResolution || !taskResolution.ok) && !missionArtifactsPresent) {
47
+ const { tasksDir } = getTaskStorage(rootDir);
48
+ const taskDirRel = path.relative(rootDir, tasksDir).split(path.sep).join('/');
49
+ missing.push(`${taskDirRel}/${slug} - *.md`);
47
50
  }
48
51
 
49
52
  return { ok: missing.length === 0, missing };
@@ -90,7 +90,7 @@ function detectPrimaryBranchDefault(rootDir = process.cwd(), options = {}) {
90
90
 
91
91
  function standardLayoutDescription() {
92
92
  return [
93
- 'standard = Backlog.md task storage in `backlog/`',
93
+ 'standard = markdown task storage in `backlog/`',
94
94
  'missions in `docs/missions/`',
95
95
  'mission/* branches with auto-detected `main`/`master` primary',
96
96
  'worktrees in `../<repo>-<slug>`',
@@ -613,7 +613,7 @@ async function collectWizardAnswers(rootDir = process.cwd(), options = {}) {
613
613
  || defaultProductName(rootDir);
614
614
  log(fmt.status('INFO', `Standard workflow layout: ${standardLayoutDescription()}`));
615
615
  const useDefaults = parseYesNo(
616
- await promptFn('Use the standard Backlog.md-style workflow layout and defaults? [Y/n]: '),
616
+ await promptFn('Use the standard markdown task workflow layout and defaults? [Y/n]: '),
617
617
  true
618
618
  );
619
619
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magnusekdahl/parallix",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "AI mission workflow toolkit with a px CLI — local-first, human-in-the-loop multi-agent development",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "private": false,
package/prompts/draft.md CHANGED
@@ -9,7 +9,7 @@ Drafting requirements:
9
9
  - fill every scaffolded section in `{{missionPath}}` with concrete, non-generic content (no placeholders, no "TBD")
10
10
  - include a Goal, Why now, Scope, Out of scope, Success criteria, Risks/assumptions, Checkpoints, Gates, Restricted areas, and Stop rules
11
11
  - success criteria must be specific enough to derive a goal-check table during execution
12
- - set exactly one of `ai_sdlc` or `user_value` in the Backlog task labels — no other value. Use `ai_sdlc` for workflow, prompt, or agent-fix work; use `user_value` for everything else (including code tech debt). Do not add a separate frontmatter field for mission type.
12
+ - {{classificationInstructions}}
13
13
  - preserve `{{taskPath}}`: update content as needed but do not delete, rename, or move the file
14
14
  - do not edit the backlog `assignee` field; the workflow records ownership itself
15
15