@magnusekdahl/parallix 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/index.js +190 -185
  2. package/lib/agents/agents.js +34 -4
  3. package/lib/agents/agents.ts +40 -5
  4. package/lib/commands/active.js +502 -579
  5. package/lib/commands/active.ts +665 -0
  6. package/lib/commands/checkpoint.js +98 -74
  7. package/lib/commands/checkpoint.ts +79 -0
  8. package/lib/commands/config.js +61 -31
  9. package/lib/commands/config.ts +47 -0
  10. package/lib/commands/coverage-gate.js +271 -246
  11. package/lib/commands/coverage-gate.ts +362 -0
  12. package/lib/commands/diff.js +140 -119
  13. package/lib/commands/diff.ts +125 -0
  14. package/lib/commands/draft.js +770 -923
  15. package/lib/commands/draft.ts +1021 -0
  16. package/lib/commands/handoff.js +588 -608
  17. package/lib/commands/handoff.ts +694 -0
  18. package/lib/commands/integrate.js +1331 -1468
  19. package/lib/commands/integrate.ts +1688 -0
  20. package/lib/commands/mission-start.js +279 -237
  21. package/lib/commands/mission-start.ts +263 -0
  22. package/lib/commands/rebase.js +606 -582
  23. package/lib/commands/rebase.ts +635 -0
  24. package/lib/commands/repair-handoff.js +181 -179
  25. package/lib/commands/repair-handoff.ts +228 -0
  26. package/lib/commands/resolve-conflict.js +121 -102
  27. package/lib/commands/resolve-conflict.ts +112 -0
  28. package/lib/commands/review.js +3 -4
  29. package/lib/commands/review.ts +14 -0
  30. package/lib/commands/setup-review.js +4 -5
  31. package/lib/commands/setup-review.ts +14 -0
  32. package/lib/commands/setup.js +3 -3
  33. package/lib/commands/setup.ts +2 -0
  34. package/lib/commands/stats-backfill.js +394 -359
  35. package/lib/commands/stats-backfill.ts +418 -0
  36. package/lib/commands/stats.js +1424 -1665
  37. package/lib/commands/stats.ts +2186 -0
  38. package/lib/commands/status.js +219 -200
  39. package/lib/commands/status.ts +219 -0
  40. package/lib/commands/verify.js +3 -1
  41. package/lib/commands/verify.ts +2 -0
  42. package/lib/core/nels.js +112 -127
  43. package/lib/core/nels.ts +189 -0
  44. package/lib/core/subagent-limit.js +15 -16
  45. package/lib/core/subagent-limit.ts +25 -0
  46. package/lib/index.js +142 -59
  47. package/lib/index.ts +134 -0
  48. package/lib/review/rebase.js +142 -145
  49. package/lib/review/rebase.ts +188 -0
  50. package/lib/review/review-adapter.js +99 -87
  51. package/lib/review/review-adapter.ts +198 -0
  52. package/lib/review/review-artifacts.js +486 -629
  53. package/lib/review/review-artifacts.ts +622 -0
  54. package/lib/review/review-commands.js +1220 -1373
  55. package/lib/review/review-commands.ts +1579 -0
  56. package/lib/review/review-events.js +544 -886
  57. package/lib/review/review-events.ts +954 -0
  58. package/lib/review/review-loop.js +945 -1020
  59. package/lib/review/review-loop.ts +1174 -0
  60. package/lib/review/review-polling.js +86 -115
  61. package/lib/review/review-polling.ts +194 -0
  62. package/lib/review/review-prompts.js +154 -189
  63. package/lib/review/review-prompts.ts +182 -0
  64. package/lib/review/review-state.js +246 -225
  65. package/lib/review/review-state.ts +353 -0
  66. package/lib/review/review.js +63 -84
  67. package/lib/review/review.ts +138 -0
  68. package/lib/tools/backlog.js +694 -680
  69. package/lib/tools/backlog.ts +835 -0
  70. package/lib/tools/forgejo.js +1270 -1405
  71. package/lib/tools/forgejo.ts +1793 -0
  72. package/lib/tools/gatekeeper.js +106 -96
  73. package/lib/tools/gatekeeper.ts +124 -0
  74. package/lib/tools/redgreen.js +200 -181
  75. package/lib/tools/redgreen.ts +221 -0
  76. package/lib/tools/sessions.js +58 -52
  77. package/lib/tools/sessions.ts +81 -0
  78. package/lib/tools/setup-review.js +902 -975
  79. package/lib/tools/setup-review.ts +1152 -0
  80. package/package.json +4 -4
  81. package/px.js +226 -208
@@ -1,446 +1,409 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const crypto = require('crypto');
4
- const fmt = require('../core/fmt');
5
- const { git, getWorktreeStatus } = require('../core/git');
6
- const { startDraftAgent, selectAgent, readAgentConfigOrExit } = require('../agents/agents');
7
- const { resolveTaskFile, enforceTaskAssignee, reportTaskResolution, checkBacklogIntegrity, transitionTask, getTaskStorage } = require('../tools/backlog');
8
- const { findMissionArea, findMissionDir, inferSlug, getMissionYear, resolveMainRepo, conventionalWorktreePath, squashTrailingBacklogNoiseIntoPreviousMission, resolveWorktree, getPrimaryBranch, missionBranchName, missionDirForSlug, detectLaunchBaseBranch } = require('../core/mission-utils');
9
- const { transitionVirtual } = require('../core/state-map');
10
- const stats = require('./stats');
11
- const { formatVerificationCommand } = require('../core/verification');
12
- const { ensureStandaloneMissionBaseline, resolveAgentModel } = require('../core/product-config');
13
- const { ensureWorkflowGitignore } = require('../core/gitignore');
14
- const { unquoteGitStatusPath } = require('./active');
15
-
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ // @ts-nocheck
36
+ const fs = __importStar(require("node:fs"));
37
+ const path = __importStar(require("node:path"));
38
+ const crypto = __importStar(require("node:crypto"));
39
+ const fmt = __importStar(require("../core/fmt.js"));
40
+ const git_js_1 = require("../core/git.js");
41
+ const agents_js_1 = require("../agents/agents.js");
42
+ const backlog_js_1 = require("../tools/backlog.js");
43
+ const mission_utils_js_1 = require("../core/mission-utils.js");
44
+ const state_map_js_1 = require("../core/state-map.js");
45
+ const stats = __importStar(require("./stats.js"));
46
+ const verification_js_1 = require("../core/verification.js");
47
+ const product_config_js_1 = require("../core/product-config.js");
48
+ const gitignore_js_1 = require("../core/gitignore.js");
49
+ const active_js_1 = require("./active.js");
16
50
  const DRAFT_PROMPT_PATH = path.join(__dirname, '..', '..', 'prompts', 'draft.md');
17
51
  const MISSION_SCAFFOLD_PATH = path.join(__dirname, '..', '..', 'templates', 'mission-scaffold.md');
18
52
  const SYNTHETIC_SLUG_PREFIX = 'adhoc-';
19
-
20
53
  function slugifyDraftIntent(/** @type {string} */ value) {
21
- return String(value || '')
22
- .trim()
23
- .toLowerCase()
24
- .replace(/^[./\\]+/, '')
25
- .replace(/[^a-z0-9]+/g, '-')
26
- .replace(/^-+|-+$/g, '')
27
- .replace(/-{2,}/g, '-')
28
- .slice(0, 64);
54
+ return String(value || '')
55
+ .trim()
56
+ .toLowerCase()
57
+ .replace(/^[./\\]+/, '')
58
+ .replace(/[^a-z0-9]+/g, '-')
59
+ .replace(/^-+|-+$/g, '')
60
+ .replace(/-{2,}/g, '-')
61
+ .slice(0, 64);
29
62
  }
30
-
31
63
  function syntheticTaskId(/** @type {string} */ slug, /** @type {string} */ seed) {
32
- const hash = crypto.createHash('sha1').update(String(seed || slug)).digest('hex').slice(0, 8).toUpperCase();
33
- const prefix = slug.startsWith(SYNTHETIC_SLUG_PREFIX) ? 'ADHOC' : 'TASK';
34
- const base = slug
35
- .replace(/^(task|adhoc)-/i, '')
36
- .replace(/[^a-z0-9]+/gi, '-')
37
- .toUpperCase();
38
- return `${prefix}-${base}-${hash}`;
64
+ const hash = crypto.createHash('sha1').update(String(seed || slug)).digest('hex').slice(0, 8).toUpperCase();
65
+ const prefix = slug.startsWith(SYNTHETIC_SLUG_PREFIX) ? 'ADHOC' : 'TASK';
66
+ const base = slug
67
+ .replace(/^(task|adhoc)-/i, '')
68
+ .replace(/[^a-z0-9]+/gi, '-')
69
+ .toUpperCase();
70
+ return `${prefix}-${base}-${hash}`;
39
71
  }
40
-
41
72
  function resolveDraftTarget(/** @type {string} */ rawInput, cwd = process.cwd()) {
42
- const explicit = String(rawInput || '').trim();
43
- if (!explicit) {return null;}
44
-
45
- if (explicit.toLowerCase().startsWith('task-')) {
46
- return {
47
- slug: explicit.toLowerCase(),
48
- syntheticTask: null,
49
- };
50
- }
51
-
52
- if (explicit.toLowerCase().startsWith(SYNTHETIC_SLUG_PREFIX)) {
53
- return {
54
- slug: explicit.toLowerCase(),
55
- syntheticTask: {
56
- title: explicit,
57
- intent: explicit,
58
- id: syntheticTaskId(explicit.toLowerCase(), explicit),
59
- source: 'synthetic-explicit-slug',
60
- },
61
- };
62
- }
63
-
64
- const absoluteCandidate = path.resolve(cwd, explicit);
65
- if (fs.existsSync(absoluteCandidate) && fs.statSync(absoluteCandidate).isDirectory()) {
66
- const baseName = path.basename(absoluteCandidate);
67
- const normalizedBase = slugifyDraftIntent(baseName) || 'project';
68
- const slug = `${SYNTHETIC_SLUG_PREFIX}${normalizedBase}`;
73
+ const explicit = String(rawInput || '').trim();
74
+ if (!explicit) {
75
+ return null;
76
+ }
77
+ if (explicit.toLowerCase().startsWith('task-')) {
78
+ return {
79
+ slug: explicit.toLowerCase(),
80
+ syntheticTask: null,
81
+ };
82
+ }
83
+ if (explicit.toLowerCase().startsWith(SYNTHETIC_SLUG_PREFIX)) {
84
+ return {
85
+ slug: explicit.toLowerCase(),
86
+ syntheticTask: {
87
+ title: explicit,
88
+ intent: explicit,
89
+ id: syntheticTaskId(explicit.toLowerCase(), explicit),
90
+ source: 'synthetic-explicit-slug',
91
+ },
92
+ };
93
+ }
94
+ const absoluteCandidate = path.resolve(cwd, explicit);
95
+ if (fs.existsSync(absoluteCandidate) && fs.statSync(absoluteCandidate).isDirectory()) {
96
+ const baseName = path.basename(absoluteCandidate);
97
+ const normalizedBase = slugifyDraftIntent(baseName) || 'project';
98
+ const slug = `${SYNTHETIC_SLUG_PREFIX}${normalizedBase}`;
99
+ return {
100
+ slug,
101
+ syntheticTask: {
102
+ title: `Draft mission for ${baseName}`,
103
+ intent: `Directory input: ${explicit}`,
104
+ id: syntheticTaskId(slug, absoluteCandidate),
105
+ source: 'synthetic-directory',
106
+ },
107
+ };
108
+ }
109
+ const normalized = slugifyDraftIntent(explicit) || 'mission';
110
+ const slug = normalized.startsWith(SYNTHETIC_SLUG_PREFIX) ? normalized : `${SYNTHETIC_SLUG_PREFIX}${normalized}`;
69
111
  return {
70
- slug,
71
- syntheticTask: {
72
- title: `Draft mission for ${baseName}`,
73
- intent: `Directory input: ${explicit}`,
74
- id: syntheticTaskId(slug, absoluteCandidate),
75
- source: 'synthetic-directory',
76
- },
112
+ slug,
113
+ syntheticTask: {
114
+ title: explicit,
115
+ intent: explicit,
116
+ id: syntheticTaskId(slug, explicit),
117
+ source: 'synthetic-free-text',
118
+ },
77
119
  };
78
- }
79
-
80
- const normalized = slugifyDraftIntent(explicit) || 'mission';
81
- const slug = normalized.startsWith(SYNTHETIC_SLUG_PREFIX) ? normalized : `${SYNTHETIC_SLUG_PREFIX}${normalized}`;
82
- return {
83
- slug,
84
- syntheticTask: {
85
- title: explicit,
86
- intent: explicit,
87
- id: syntheticTaskId(slug, explicit),
88
- source: 'synthetic-free-text',
89
- },
90
- };
91
120
  }
92
-
93
- async function runDraftCommand(/** @type {string[]} */ args, {
94
- inferSlugFn = inferSlug,
95
- resolveMainRepoFn = resolveMainRepo,
96
- conventionalWorktreePathFn = conventionalWorktreePath,
97
- ensureMissionBranchFn = ensureMissionBranch,
98
- ensureWorktreeFn = ensureWorktree,
99
- ensureGraphifyWorkspaceFn = ensureGraphifyWorkspace,
100
- ensureGraphifyIgnoreFn = ensureGraphifyIgnore,
101
- ensureMissionFileFn = ensureMissionFile,
102
- detectLaunchBaseBranchFn = detectLaunchBaseBranch,
103
- ensureMissionBaseBranchRecordedFn = ensureMissionBaseBranchRecorded,
104
- bootstrapBacklogTaskFn = bootstrapBacklogTask,
105
- ensureStandaloneMissionBaselineFn = ensureStandaloneMissionBaseline,
106
- ensureDraftRepoConfigCommittedFn = ensureDraftRepoConfigCommitted,
107
- readAgentConfigOrExitFn = readAgentConfigOrExit,
108
- selectAgentFn = selectAgent,
109
- startDraftAgentFn = startDraftAgent,
110
- resolveTaskFileFn = resolveTaskFile,
111
- reportTaskResolutionFn = reportTaskResolution,
112
- checkBacklogIntegrityFn = checkBacklogIntegrity,
113
- ensureRepoExistsFn = ensureRepoExists,
114
- transitionTaskFn = transitionTask,
115
- transitionVirtualFn = transitionVirtual,
116
- recordDraftImplementerFn = recordDraftImplementer,
117
- recordDraftStatsFn = recordDraftStats,
118
- enforceDraftCommitSafetyFn = enforceDraftCommitSafety,
119
- validateDraftClassificationFn = validateDraftClassification,
120
- normalizeDraftClassificationFn = normalizeDraftClassification,
121
- restartDraftAgentFn = restartDraftAgent,
122
- exitFn = process.exit,
123
- logFn = fmt.log.plain,
124
- errorFn = fmt.log.plainError
125
- } = {}) {
126
- const explicitInput = args[0];
127
- const draftTarget = resolveDraftTarget(explicitInput) || { slug: inferSlugFn(explicitInput), syntheticTask: null };
128
- const slug = draftTarget.slug;
129
- if (!slug) {
130
- errorFn(fmt.status('FAIL', 'Usage: px draft <slug> [--agent <family>]'));
131
- exitFn(1);
132
- return;
133
- }
134
-
135
- const normalizedSlug = slug.toLowerCase();
136
- const syntheticTask = draftTarget.syntheticTask;
137
-
138
- // Allow operators to pin the agent family via CLI flag instead of WORKFLOW_AGENT env var.
139
- function flagValue(/** @type {string[]} */ arr, /** @type {string} */ flag, /** @type {string} */ name) {
140
- const i = arr.indexOf(flag);
141
- if (i === -1) {return null;}
142
- const v = arr[i + 1];
143
- if (!v || v.startsWith('--')) {
144
- errorFn(fmt.status('FAIL', `Missing value for --${name}. Usage: px draft <slug> --${name} <family>`));
145
- exitFn(1);
146
- return null;
147
- }
148
- return v;
149
- }
150
- const preselectedAgent = flagValue(args, '--agent', 'agent');
151
- logFn(fmt.bold(`Starting mission draft automation for: ${fmt.slug(normalizedSlug)}`));
152
-
153
- const mainRepo = resolveMainRepoFn();
154
- if (ensureRepoExistsFn(mainRepo, exitFn, errorFn) === false) {
155
- return;
156
- }
157
-
158
- const baselineResult = ensureStandaloneMissionBaselineFn(mainRepo);
159
- if (baselineResult && baselineResult.failed) {
160
- errorFn(fmt.status('FAIL', `Standalone mission baseline: ${baselineResult.message}`));
161
- exitFn(1);
162
- return;
163
- }
164
- if (baselineResult && baselineResult.committed) {
165
- logFn(fmt.status('PASS', 'Standalone mission baseline committed in the primary checkout.'));
166
- }
167
-
168
- if (!ensureDraftRepoConfigCommittedFn(mainRepo, { errorFn })) {
169
- exitFn(1);
170
- return;
171
- }
172
-
173
- // Detect the branch HEAD is on at draft time (where the human stands). This is
174
- // the single source of truth for the mission's base. A non-primary feature
175
- // branch is recorded as the mission base; the primary branch (or a detached
176
- // HEAD) leaves no record and keeps the byte-identical legacy behaviour.
177
- let launchBase = null;
178
- try {
179
- launchBase = detectLaunchBaseBranchFn(process.cwd());
180
- } catch (error) {
181
- errorFn(fmt.status('FAIL', /** @type {any} */ (error).message));
182
- exitFn(1);
183
- return;
184
- }
185
- let recordedBase = null;
186
- if (launchBase && launchBase !== getPrimaryBranch(mainRepo)) {
187
- recordedBase = launchBase;
188
- logFn(fmt.status('INFO', `Feature-branch mission: base branch detected as ${fmt.branch(recordedBase)}.`));
189
- }
190
-
191
- // A feature-branch mission may reference a task that was only committed on that
192
- // feature branch, so it is absent from the primary checkout (mainRepo). In that
193
- // case the task lives in the current worktree (where HEAD is on the feature
194
- // branch), which the mission branch is cut from. Resolve preflight checks there.
195
- const taskLookupRoot = recordedBase ? process.cwd() : mainRepo;
196
-
197
- // Preflight: Ensure Backlog task exists and is unambiguous before side effects
198
- const mainResolution = resolveTaskFileFn(normalizedSlug, taskLookupRoot);
199
- if (!mainResolution.ok && !syntheticTask) {
200
- reportTaskResolutionFn(mainResolution, normalizedSlug, errorFn);
201
- exitFn(1);
202
- return;
203
- }
204
-
205
- // Preflight: Backlog integrity check (filename vs frontmatter ID)
206
- const relevantIssues = syntheticTask ? [] : checkBacklogIntegrityFn(taskLookupRoot, normalizedSlug);
207
- if (relevantIssues.length > 0) {
208
- errorFn(fmt.status('FAIL', `Backlog integrity issues detected for ${normalizedSlug}:`));
209
- relevantIssues.forEach(issue => {
210
- if (issue.type === 'duplicate-completed') {
211
- logFn(` - ${fmt.path(/** @type {any} */ (issue).file)}: task ${fmt.bold(/** @type {any} */ (issue).taskId)} already has a canonical copy in ${fmt.path(/** @type {any} */ (issue).canonicalFile)}; this backlog/tasks copy is stale.`);
212
- } else {
213
- logFn(` - ${fmt.path(/** @type {any} */ (issue).file)}: filename ID (${fmt.bold(/** @type {any} */ (issue).filenameId)}) does not match frontmatter ID (${fmt.bold(/** @type {any} */ (issue).frontmatterId)})`);
214
- }
121
+ async function runDraftCommand(/** @type {string[]} */ args, { inferSlugFn = mission_utils_js_1.inferSlug, resolveMainRepoFn = mission_utils_js_1.resolveMainRepo, conventionalWorktreePathFn = mission_utils_js_1.conventionalWorktreePath, ensureMissionBranchFn = ensureMissionBranch, ensureWorktreeFn = ensureWorktree, ensureGraphifyWorkspaceFn = ensureGraphifyWorkspace, ensureGraphifyIgnoreFn = ensureGraphifyIgnore, ensureMissionFileFn = ensureMissionFile, detectLaunchBaseBranchFn = mission_utils_js_1.detectLaunchBaseBranch, ensureMissionBaseBranchRecordedFn = ensureMissionBaseBranchRecorded, bootstrapBacklogTaskFn = bootstrapBacklogTask, ensureStandaloneMissionBaselineFn = product_config_js_1.ensureStandaloneMissionBaseline, ensureDraftRepoConfigCommittedFn = ensureDraftRepoConfigCommitted, readAgentConfigOrExitFn = agents_js_1.readAgentConfigOrExit, selectAgentFn = agents_js_1.selectAgent, startDraftAgentFn = agents_js_1.startDraftAgent, resolveTaskFileFn = backlog_js_1.resolveTaskFile, reportTaskResolutionFn = backlog_js_1.reportTaskResolution, checkBacklogIntegrityFn = backlog_js_1.checkBacklogIntegrity, ensureRepoExistsFn = ensureRepoExists, transitionTaskFn = backlog_js_1.transitionTask, transitionVirtualFn = state_map_js_1.transitionVirtual, recordDraftImplementerFn = recordDraftImplementer, recordDraftStatsFn = recordDraftStats, enforceDraftCommitSafetyFn = enforceDraftCommitSafety, validateDraftClassificationFn = validateDraftClassification, normalizeDraftClassificationFn = normalizeDraftClassification, restartDraftAgentFn = restartDraftAgent, exitFn = process.exit, logFn = fmt.log.plain, errorFn = fmt.log.plainError } = {}) {
122
+ const explicitInput = args[0];
123
+ const draftTarget = resolveDraftTarget(explicitInput) || { slug: inferSlugFn(explicitInput), syntheticTask: null };
124
+ const slug = draftTarget.slug;
125
+ if (!slug) {
126
+ errorFn(fmt.status('FAIL', 'Usage: px draft <slug> [--agent <family>]'));
127
+ exitFn(1);
128
+ return;
129
+ }
130
+ const normalizedSlug = slug.toLowerCase();
131
+ const syntheticTask = draftTarget.syntheticTask;
132
+ // Allow operators to pin the agent family via CLI flag instead of WORKFLOW_AGENT env var.
133
+ function flagValue(/** @type {string[]} */ arr, /** @type {string} */ flag, /** @type {string} */ name) {
134
+ const i = arr.indexOf(flag);
135
+ if (i === -1) {
136
+ return null;
137
+ }
138
+ const v = arr[i + 1];
139
+ if (!v || v.startsWith('--')) {
140
+ errorFn(fmt.status('FAIL', `Missing value for --${name}. Usage: px draft <slug> --${name} <family>`));
141
+ exitFn(1);
142
+ return null;
143
+ }
144
+ return v;
145
+ }
146
+ const preselectedAgent = flagValue(args, '--agent', 'agent');
147
+ logFn(fmt.bold(`Starting mission draft automation for: ${fmt.slug(normalizedSlug)}`));
148
+ const mainRepo = resolveMainRepoFn();
149
+ if (ensureRepoExistsFn(mainRepo, exitFn, errorFn) === false) {
150
+ return;
151
+ }
152
+ const baselineResult = ensureStandaloneMissionBaselineFn(mainRepo);
153
+ if (baselineResult && baselineResult.failed) {
154
+ errorFn(fmt.status('FAIL', `Standalone mission baseline: ${baselineResult.message}`));
155
+ exitFn(1);
156
+ return;
157
+ }
158
+ if (baselineResult && baselineResult.committed) {
159
+ logFn(fmt.status('PASS', 'Standalone mission baseline committed in the primary checkout.'));
160
+ }
161
+ if (!ensureDraftRepoConfigCommittedFn(mainRepo, { errorFn })) {
162
+ exitFn(1);
163
+ return;
164
+ }
165
+ // Detect the branch HEAD is on at draft time (where the human stands). This is
166
+ // the single source of truth for the mission's base. A non-primary feature
167
+ // branch is recorded as the mission base; the primary branch (or a detached
168
+ // HEAD) leaves no record and keeps the byte-identical legacy behaviour.
169
+ let launchBase = null;
170
+ try {
171
+ launchBase = detectLaunchBaseBranchFn(process.cwd());
172
+ }
173
+ catch (error) {
174
+ errorFn(fmt.status('FAIL', /** @type {any} */ (error).message));
175
+ exitFn(1);
176
+ return;
177
+ }
178
+ let recordedBase = null;
179
+ if (launchBase && launchBase !== (0, mission_utils_js_1.getPrimaryBranch)(mainRepo)) {
180
+ recordedBase = launchBase;
181
+ logFn(fmt.status('INFO', `Feature-branch mission: base branch detected as ${fmt.branch(recordedBase)}.`));
182
+ }
183
+ // A feature-branch mission may reference a task that was only committed on that
184
+ // feature branch, so it is absent from the primary checkout (mainRepo). In that
185
+ // case the task lives in the current worktree (where HEAD is on the feature
186
+ // branch), which the mission branch is cut from. Resolve preflight checks there.
187
+ const taskLookupRoot = recordedBase ? process.cwd() : mainRepo;
188
+ // Preflight: Ensure Backlog task exists and is unambiguous before side effects
189
+ const mainResolution = resolveTaskFileFn(normalizedSlug, taskLookupRoot);
190
+ if (!mainResolution.ok && !syntheticTask) {
191
+ reportTaskResolutionFn(mainResolution, normalizedSlug, errorFn);
192
+ exitFn(1);
193
+ return;
194
+ }
195
+ // Preflight: Backlog integrity check (filename vs frontmatter ID)
196
+ const relevantIssues = syntheticTask ? [] : checkBacklogIntegrityFn(taskLookupRoot, normalizedSlug);
197
+ if (relevantIssues.length > 0) {
198
+ errorFn(fmt.status('FAIL', `Backlog integrity issues detected for ${normalizedSlug}:`));
199
+ relevantIssues.forEach(issue => {
200
+ if (issue.type === 'duplicate-completed') {
201
+ logFn(` - ${fmt.path(/** @type {any} */ (issue).file)}: task ${fmt.bold(/** @type {any} */ (issue).taskId)} already has a canonical copy in ${fmt.path(/** @type {any} */ (issue).canonicalFile)}; this backlog/tasks copy is stale.`);
202
+ }
203
+ else {
204
+ logFn(` - ${fmt.path(/** @type {any} */ (issue).file)}: filename ID (${fmt.bold(/** @type {any} */ (issue).filenameId)}) does not match frontmatter ID (${fmt.bold(/** @type {any} */ (issue).frontmatterId)})`);
205
+ }
206
+ });
207
+ logFn('Repair: Fix filename/id mismatch, or remove the stale backlog/tasks copy of a completed/archived task, before drafting.');
208
+ exitFn(1);
209
+ return;
210
+ }
211
+ const branchName = (0, mission_utils_js_1.missionBranchName)(normalizedSlug, mainRepo);
212
+ logFn(fmt.bold(`Step 1: Setting up branch ${fmt.branch(branchName)}...`));
213
+ ensureMissionBranchFn(mainRepo, branchName, { logFn, baseBranch: recordedBase });
214
+ const targetWorktree = conventionalWorktreePathFn(normalizedSlug, mainRepo);
215
+ logFn(fmt.bold(`Step 2: Ensuring dedicated worktree at ${fmt.path(targetWorktree)}...`));
216
+ ensureWorktreeFn(mainRepo, targetWorktree, branchName, { logFn, errorFn });
217
+ ensureGraphifyWorkspaceFn(targetWorktree, { logFn });
218
+ ensureGraphifyIgnoreFn(targetWorktree, { logFn });
219
+ const gitignoreResult = (0, gitignore_js_1.ensureWorkflowGitignore)(targetWorktree, { logFn });
220
+ if (gitignoreResult.created) {
221
+ logFn(fmt.status('PASS', `Created .gitignore with ${gitignoreResult.appended} workflow entries in ${fmt.path(targetWorktree)}`));
222
+ }
223
+ else if (gitignoreResult.appended > 0) {
224
+ logFn(fmt.status('PASS', `Appended ${gitignoreResult.appended} workflow entries to .gitignore in ${fmt.path(targetWorktree)}`));
225
+ }
226
+ else if (gitignoreResult.skipped) {
227
+ logFn(fmt.status('INFO', `.gitignore in ${fmt.path(targetWorktree)}: ${gitignoreResult.reason === 'symlink' ? 'symbolic link (skipped)' : 'not a git repo (skipped)'}`));
228
+ }
229
+ else {
230
+ logFn(fmt.status('PASS', `.gitignore in ${fmt.path(targetWorktree)} already contains all workflow entries`));
231
+ }
232
+ logFn(fmt.bold('Step 3: Scaffolding MISSION.md...'));
233
+ const missionFile = ensureMissionFileFn(targetWorktree, normalizedSlug, { logFn });
234
+ ensureMissionBaseBranchRecordedFn(missionFile, recordedBase, { logFn });
235
+ logFn(fmt.bold('Step 4: Ensuring Backlog task exists in worktree...'));
236
+ if (!bootstrapBacklogTaskFn(targetWorktree, mainRepo, normalizedSlug, { logFn, errorFn, syntheticTask: /** @type {null | undefined} */ (syntheticTask) })) {
237
+ const { tasksDir } = (0, backlog_js_1.getTaskStorage)(targetWorktree);
238
+ const taskDirHint = path.relative(targetWorktree, tasksDir).split(path.sep).join('/');
239
+ errorFn(fmt.status('FAIL', `Backlog task for ${normalizedSlug} could not be prepared in the mission worktree.`));
240
+ logFn(`Repair: create the task with your task adapter, or add ${fmt.path(`${taskDirHint}/${normalizedSlug} - <title>.md`)} manually.`);
241
+ exitFn(1);
242
+ return;
243
+ }
244
+ const classificationCheck = validateDraftClassificationFn(normalizedSlug, targetWorktree, {
245
+ errorFn
215
246
  });
216
- logFn('Repair: Fix filename/id mismatch, or remove the stale backlog/tasks copy of a completed/archived task, before drafting.');
217
- exitFn(1);
218
- return;
219
- }
220
-
221
- const branchName = missionBranchName(normalizedSlug, mainRepo);
222
- logFn(fmt.bold(`Step 1: Setting up branch ${fmt.branch(branchName)}...`));
223
- ensureMissionBranchFn(mainRepo, branchName, { logFn, baseBranch: recordedBase });
224
-
225
- const targetWorktree = conventionalWorktreePathFn(normalizedSlug, mainRepo);
226
- logFn(fmt.bold(`Step 2: Ensuring dedicated worktree at ${fmt.path(targetWorktree)}...`));
227
- ensureWorktreeFn(mainRepo, targetWorktree, branchName, { logFn, errorFn });
228
- ensureGraphifyWorkspaceFn(targetWorktree, { logFn });
229
- ensureGraphifyIgnoreFn(targetWorktree, { logFn });
230
-
231
- const gitignoreResult = ensureWorkflowGitignore(targetWorktree, { logFn });
232
- if (gitignoreResult.created) {
233
- logFn(fmt.status('PASS', `Created .gitignore with ${gitignoreResult.appended} workflow entries in ${fmt.path(targetWorktree)}`));
234
- } else if (gitignoreResult.appended > 0) {
235
- logFn(fmt.status('PASS', `Appended ${gitignoreResult.appended} workflow entries to .gitignore in ${fmt.path(targetWorktree)}`));
236
- } else if (gitignoreResult.skipped) {
237
- logFn(fmt.status('INFO', `.gitignore in ${fmt.path(targetWorktree)}: ${gitignoreResult.reason === 'symlink' ? 'symbolic link (skipped)' : 'not a git repo (skipped)'}`));
238
- } else {
239
- logFn(fmt.status('PASS', `.gitignore in ${fmt.path(targetWorktree)} already contains all workflow entries`));
240
- }
241
-
242
- logFn(fmt.bold('Step 3: Scaffolding MISSION.md...'));
243
- const missionFile = ensureMissionFileFn(targetWorktree, normalizedSlug, { logFn });
244
- ensureMissionBaseBranchRecordedFn(missionFile, recordedBase, { logFn });
245
-
246
- logFn(fmt.bold('Step 4: Ensuring Backlog task exists in worktree...'));
247
- if (!bootstrapBacklogTaskFn(targetWorktree, mainRepo, normalizedSlug, { logFn, errorFn, syntheticTask: /** @type {null | undefined} */ (syntheticTask) })) {
248
- const { tasksDir } = getTaskStorage(targetWorktree);
249
- const taskDirHint = path.relative(targetWorktree, tasksDir).split(path.sep).join('/');
250
- errorFn(fmt.status('FAIL', `Backlog task for ${normalizedSlug} could not be prepared in the mission worktree.`));
251
- logFn(`Repair: create the task with your task adapter, or add ${fmt.path(`${taskDirHint}/${normalizedSlug} - <title>.md`)} manually.`);
252
- exitFn(1);
253
- return;
254
- }
255
-
256
- const classificationCheck = validateDraftClassificationFn(normalizedSlug, targetWorktree, {
257
- errorFn
258
- });
259
- if (!classificationCheck.ok) {
260
- exitFn(1);
261
- return;
262
- }
263
-
264
- logFn('\n' + fmt.status('PASS', 'Draft setup complete.'));
265
- logFn(`Worktree: ${fmt.path(targetWorktree)}`);
266
- logFn(`Mission doc: ${fmt.path(missionFile)}`);
267
-
268
- if (!transitionTaskFn(normalizedSlug, 'backlog', { rootDir: targetWorktree, log: logFn })) {
269
- errorFn(fmt.status('FAIL', `Could not transition task ${normalizedSlug} to backlog status.`));
270
- exitFn(1);
271
- return;
272
- }
273
-
274
- // Pre-select the initial family; bookkeeping commit is deferred until after the
275
- // launch settles so a failed launch cannot leave a stale assignee commit behind.
276
- const agentConfig = readAgentConfigOrExitFn();
277
- const agent = preselectedAgent || selectAgentFn('draft', { config: agentConfig });
278
-
279
- // @ts-expect-error buildDraftPrompt type mismatch
280
- const prompt = buildDraftPrompt(normalizedSlug, { rootDir: mainRepo, worktree: targetWorktree || '' });
281
- logFn('Launching draft agent...');
282
- const { agent: actualAgent, result } = await startDraftAgentFn({
283
- prompt,
284
- // @ts-expect-error worktree not in type
285
- worktree: targetWorktree,
286
- agent
287
- });
288
- logFn(`Draft agent family: ${fmt.agent(/** @type {any} */ (actualAgent))}`);
289
-
290
- if (result.error) {
291
- errorFn(fmt.status('FAIL', `Could not start draft agent (${fmt.agent(/** @type {any} */ (actualAgent))}): ${/** @type {any} */ (result.error).message}`));
292
- exitFn(1);
293
- return;
294
- }
295
-
296
- if (typeof /** @type {any} */ (result).status === 'number' && /** @type {any} */ (result).status !== 0) {
297
- errorFn(fmt.status('FAIL', `Draft agent (${fmt.agent(/** @type {any} */ (actualAgent))}) exited with status ${/** @type {any} */ (result).status}.`));
298
- exitFn(result.status || 1);
299
- return;
300
- }
301
-
302
- const taskResolutionAfter = resolveTaskFileFn(normalizedSlug, targetWorktree);
303
- recordDraftImplementerFn({
304
- selected: agent,
305
- actual: actualAgent,
306
- taskResolution: taskResolutionAfter,
307
- slug: normalizedSlug,
308
- worktree: targetWorktree
309
- });
310
-
311
- // Record stats from the mission worktree, where the backlog task always lives
312
- // (a feature-branch task is absent from mainRepo/the primary checkout). This
313
- // matches how the review and active stages record stats (rootDir: worktree).
314
- recordDraftStatsFn({
315
- slug: normalizedSlug,
316
- rootDir: targetWorktree,
317
- agentFamily: actualAgent,
318
- result,
319
- log: logFn,
320
- // @ts-expect-error reportTaskResolutionFn accepts extra properties
321
- error: errorFn
322
- });
323
-
324
- // Post-draft mission type repair: if the agent did not produce valid labels,
325
- // relaunch it once with a targeted fix prompt and validate again.
326
- const normalizationResult = normalizeDraftClassificationFn(normalizedSlug, targetWorktree, {
327
- errorFn
328
- });
329
- if (!normalizationResult.ok) {
330
- logFn(fmt.status('WARN', `Post-draft mission type labels are not valid (${normalizationResult.reason}). Relaunching draft agent to repair them.`));
331
- const restartOk = await restartDraftAgentFn(normalizedSlug, targetWorktree, {
332
- logFn,
333
- errorFn,
334
- // @ts-expect-error restartDraftAgentFn accepts extra properties
335
- exitFn
247
+ if (!classificationCheck.ok) {
248
+ exitFn(1);
249
+ return;
250
+ }
251
+ logFn('\n' + fmt.status('PASS', 'Draft setup complete.'));
252
+ logFn(`Worktree: ${fmt.path(targetWorktree)}`);
253
+ logFn(`Mission doc: ${fmt.path(missionFile)}`);
254
+ if (!transitionTaskFn(normalizedSlug, 'backlog', { rootDir: targetWorktree, log: logFn })) {
255
+ errorFn(fmt.status('FAIL', `Could not transition task ${normalizedSlug} to backlog status.`));
256
+ exitFn(1);
257
+ return;
258
+ }
259
+ // Pre-select the initial family; bookkeeping commit is deferred until after the
260
+ // launch settles so a failed launch cannot leave a stale assignee commit behind.
261
+ const agentConfig = readAgentConfigOrExitFn();
262
+ const agent = preselectedAgent || selectAgentFn('draft', { config: agentConfig });
263
+ // @ts-expect-error buildDraftPrompt type mismatch
264
+ const prompt = buildDraftPrompt(normalizedSlug, { rootDir: mainRepo, worktree: targetWorktree || '' });
265
+ logFn('Launching draft agent...');
266
+ const { agent: actualAgent, result } = await startDraftAgentFn({
267
+ prompt,
268
+ // @ts-expect-error worktree not in type
269
+ worktree: targetWorktree,
270
+ agent
336
271
  });
337
- if (!restartOk) {
338
- exitFn(1);
339
- return;
272
+ logFn(`Draft agent family: ${fmt.agent(/** @type {any} */ (actualAgent))}`);
273
+ if (result.error) {
274
+ errorFn(fmt.status('FAIL', `Could not start draft agent (${fmt.agent(/** @type {any} */ (actualAgent))}): ${ /** @type {any} */(result.error).message}`));
275
+ exitFn(1);
276
+ return;
340
277
  }
341
- const postRestartNorm = normalizeDraftClassificationFn(normalizedSlug, targetWorktree, {
342
- errorFn
278
+ if (typeof /** @type {any} */ (result).status === 'number' && /** @type {any} */ (result).status !== 0) {
279
+ errorFn(fmt.status('FAIL', `Draft agent (${fmt.agent(/** @type {any} */ (actualAgent))}) exited with status ${ /** @type {any} */(result).status}.`));
280
+ exitFn(result.status || 1);
281
+ return;
282
+ }
283
+ const taskResolutionAfter = resolveTaskFileFn(normalizedSlug, targetWorktree);
284
+ recordDraftImplementerFn({
285
+ selected: agent,
286
+ actual: actualAgent,
287
+ taskResolution: taskResolutionAfter,
288
+ slug: normalizedSlug,
289
+ worktree: targetWorktree
343
290
  });
344
- if (!postRestartNorm.ok) {
345
- errorFn(fmt.status('FAIL', `Post-draft mission type labels are still invalid after restart (${postRestartNorm.reason}).`));
346
- exitFn(1);
347
- return;
348
- } else {
349
- logFn(fmt.status('PASS', `Post-draft mission type labels validated after restart: ${postRestartNorm.classification}`));
350
- }
351
- } else {
352
- logFn(fmt.status('PASS', `Post-draft mission type labels validated: ${normalizationResult.classification}`));
353
- }
354
-
355
- // Re-assert the Base-Branch record after the agent runs so a full MISSION.md
356
- // rewrite cannot drop it; the safety-harness commit below captures the change.
357
- ensureMissionBaseBranchRecordedFn(missionFile, recordedBase, { logFn });
358
-
359
- try {
360
- enforceDraftCommitSafetyFn({ slug: normalizedSlug, worktree: targetWorktree, logFn, errorFn });
361
- } catch (error) {
362
- errorFn(fmt.status('FAIL', /** @type {any} */ (error).message));
363
- exitFn(1);
364
- return;
365
- }
366
-
367
- if (!transitionVirtualFn(transitionTaskFn, normalizedSlug, 'ready', /** @type {{ rootDir: string, log: Function }} */ ({ rootDir: targetWorktree, log: logFn }))) {
368
- errorFn(fmt.status('FAIL', `Could not transition task ${normalizedSlug} to ready status.`));
369
- exitFn(1);
370
- return;
371
- }
372
-
373
- logFn('\n' + fmt.status('INFO', `Next: ${fmt.command(`cd ${targetWorktree}`)}`));
291
+ // Record stats from the mission worktree, where the backlog task always lives
292
+ // (a feature-branch task is absent from mainRepo/the primary checkout). This
293
+ // matches how the review and active stages record stats (rootDir: worktree).
294
+ recordDraftStatsFn({
295
+ slug: normalizedSlug,
296
+ rootDir: targetWorktree,
297
+ agentFamily: actualAgent,
298
+ result,
299
+ log: logFn,
300
+ // @ts-expect-error reportTaskResolutionFn accepts extra properties
301
+ error: errorFn
302
+ });
303
+ // Post-draft mission type repair: if the agent did not produce valid labels,
304
+ // relaunch it once with a targeted fix prompt and validate again.
305
+ const normalizationResult = normalizeDraftClassificationFn(normalizedSlug, targetWorktree, {
306
+ errorFn
307
+ });
308
+ if (!normalizationResult.ok) {
309
+ logFn(fmt.status('WARN', `Post-draft mission type labels are not valid (${normalizationResult.reason}). Relaunching draft agent to repair them.`));
310
+ const restartOk = await restartDraftAgentFn(normalizedSlug, targetWorktree, {
311
+ logFn,
312
+ errorFn,
313
+ // @ts-expect-error restartDraftAgentFn accepts extra properties
314
+ exitFn
315
+ });
316
+ if (!restartOk) {
317
+ exitFn(1);
318
+ return;
319
+ }
320
+ const postRestartNorm = normalizeDraftClassificationFn(normalizedSlug, targetWorktree, {
321
+ errorFn
322
+ });
323
+ if (!postRestartNorm.ok) {
324
+ errorFn(fmt.status('FAIL', `Post-draft mission type labels are still invalid after restart (${postRestartNorm.reason}).`));
325
+ exitFn(1);
326
+ return;
327
+ }
328
+ else {
329
+ logFn(fmt.status('PASS', `Post-draft mission type labels validated after restart: ${postRestartNorm.classification}`));
330
+ }
331
+ }
332
+ else {
333
+ logFn(fmt.status('PASS', `Post-draft mission type labels validated: ${normalizationResult.classification}`));
334
+ }
335
+ // Re-assert the Base-Branch record after the agent runs so a full MISSION.md
336
+ // rewrite cannot drop it; the safety-harness commit below captures the change.
337
+ ensureMissionBaseBranchRecordedFn(missionFile, recordedBase, { logFn });
338
+ try {
339
+ enforceDraftCommitSafetyFn({ slug: normalizedSlug, worktree: targetWorktree, logFn, errorFn });
340
+ }
341
+ catch (error) {
342
+ errorFn(fmt.status('FAIL', /** @type {any} */ (error).message));
343
+ exitFn(1);
344
+ return;
345
+ }
346
+ if (!transitionVirtualFn(transitionTaskFn, normalizedSlug, 'ready', /** @type {{ rootDir: string, log: Function }} */ ({ rootDir: targetWorktree, log: logFn }))) {
347
+ errorFn(fmt.status('FAIL', `Could not transition task ${normalizedSlug} to ready status.`));
348
+ exitFn(1);
349
+ return;
350
+ }
351
+ logFn('\n' + fmt.status('INFO', `Next: ${fmt.command(`cd ${targetWorktree}`)}`));
374
352
  }
375
-
376
353
  // @ts-expect-error implicit any on args/deps
377
354
  async function draft(args, deps) {
378
- return runDraftCommand(args, deps);
379
- }
380
-
381
- function recordDraftImplementer({
382
- // @ts-expect-error implicit any binding elements
383
- selected,
384
- // @ts-expect-error implicit any binding elements
385
- actual,
386
- // @ts-expect-error implicit any binding elements
387
- taskResolution,
388
- log = fmt.log.plain,
389
- enforceTaskAssigneeFn = enforceTaskAssignee,
390
- gitFn = git,
391
- // @ts-expect-error implicit any binding elements
392
- slug,
393
- // @ts-expect-error implicit any binding elements
394
- worktree
395
- }) {
396
- if (!taskResolution || !taskResolution.ok || !actual) {
397
- return actual || selected;
398
- }
399
-
400
- if (selected && actual !== selected) {
401
- log(fmt.status('INFO', `Draft agent fell back from ${fmt.agent(selected)} to ${fmt.agent(actual)}; enforcing backlog assignee.`));
402
- } else {
403
- log(fmt.status('INFO', `Enforcing draft agent ${fmt.agent(actual)} as assignee...`));
404
- }
405
-
406
- if (enforceTaskAssigneeFn(taskResolution.taskFile, actual)) {
407
- // Ensure we commit the fallback/recording to the mission branch so it is shared.
408
- const effectiveWorktree = worktree || resolveWorktree(slug) || process.cwd();
409
- const relativeTaskPath = path.relative(effectiveWorktree, taskResolution.taskFile);
410
- gitFn(['-C', effectiveWorktree, 'add', relativeTaskPath]);
411
- const commitResult = gitFn(['-C', effectiveWorktree, 'commit', '-m', `backlog(${slug}): enforce implementer=${actual}`]);
412
- if (commitResult.status !== 0) {
413
- log(fmt.status('WARN', `Failed to commit implementer recording: ${commitResult.stderr}`));
355
+ return runDraftCommand(args, deps);
356
+ }
357
+ function recordDraftImplementer({
358
+ // @ts-expect-error implicit any binding elements
359
+ selected,
360
+ // @ts-expect-error implicit any binding elements
361
+ actual,
362
+ // @ts-expect-error implicit any binding elements
363
+ taskResolution, log = fmt.log.plain, enforceTaskAssigneeFn = backlog_js_1.enforceTaskAssignee, gitFn = git_js_1.git,
364
+ // @ts-expect-error implicit any binding elements
365
+ slug,
366
+ // @ts-expect-error implicit any binding elements
367
+ worktree }) {
368
+ if (!taskResolution || !taskResolution.ok || !actual) {
369
+ return actual || selected;
414
370
  }
415
- } else {
416
- log(fmt.status('WARN', `Could not enforce draft agent ${fmt.agent(actual)} in backlog task.`));
417
- }
418
- return actual;
371
+ if (selected && actual !== selected) {
372
+ log(fmt.status('INFO', `Draft agent fell back from ${fmt.agent(selected)} to ${fmt.agent(actual)}; enforcing backlog assignee.`));
373
+ }
374
+ else {
375
+ log(fmt.status('INFO', `Enforcing draft agent ${fmt.agent(actual)} as assignee...`));
376
+ }
377
+ if (enforceTaskAssigneeFn(taskResolution.taskFile, actual)) {
378
+ // Ensure we commit the fallback/recording to the mission branch so it is shared.
379
+ const effectiveWorktree = worktree || (0, mission_utils_js_1.resolveWorktree)(slug) || process.cwd();
380
+ const relativeTaskPath = path.relative(effectiveWorktree, taskResolution.taskFile);
381
+ gitFn(['-C', effectiveWorktree, 'add', relativeTaskPath]);
382
+ const commitResult = gitFn(['-C', effectiveWorktree, 'commit', '-m', `backlog(${slug}): enforce implementer=${actual}`]);
383
+ if (commitResult.status !== 0) {
384
+ log(fmt.status('WARN', `Failed to commit implementer recording: ${commitResult.stderr}`));
385
+ }
386
+ }
387
+ else {
388
+ log(fmt.status('WARN', `Could not enforce draft agent ${fmt.agent(actual)} in backlog task.`));
389
+ }
390
+ return actual;
419
391
  }
420
-
421
392
  // @ts-expect-error implicit any on mainRepo/branchName
422
- function ensureMissionBranch(mainRepo, branchName, {
423
- gitFn = git,
424
- logFn = fmt.log.plain,
425
- squashTrailingBacklogNoiseIntoPreviousMissionFn = squashTrailingBacklogNoiseIntoPreviousMission,
426
- baseBranch = null
427
- } = {}) {
428
- const branches = gitFn(['-C', mainRepo, 'branch', '--list', branchName]).stdout.trim();
429
- if (branches) {
430
- logFn(fmt.status('PASS', `Branch ${fmt.branch(branchName)} already exists.`));
431
- return;
432
- }
433
-
434
- squashTrailingBacklogNoiseIntoPreviousMissionFn(mainRepo, gitFn);
435
-
436
- // The base is whatever HEAD pointed at when draft ran (a feature branch); when
437
- // none was recorded it falls back to the primary branch — byte-identical to
438
- // the legacy single-branch behaviour.
439
- const startPoint = baseBranch || getPrimaryBranch();
440
- gitFn(['-C', mainRepo, 'branch', branchName, startPoint]);
441
- logFn(fmt.status('PASS', `Created branch ${fmt.branch(branchName)} from ${fmt.branch(startPoint)}.`));
393
+ function ensureMissionBranch(mainRepo, branchName, { gitFn = git_js_1.git, logFn = fmt.log.plain, squashTrailingBacklogNoiseIntoPreviousMissionFn = mission_utils_js_1.squashTrailingBacklogNoiseIntoPreviousMission, baseBranch = null } = {}) {
394
+ const branches = gitFn(['-C', mainRepo, 'branch', '--list', branchName]).stdout.trim();
395
+ if (branches) {
396
+ logFn(fmt.status('PASS', `Branch ${fmt.branch(branchName)} already exists.`));
397
+ return;
398
+ }
399
+ squashTrailingBacklogNoiseIntoPreviousMissionFn(mainRepo, gitFn);
400
+ // The base is whatever HEAD pointed at when draft ran (a feature branch); when
401
+ // none was recorded it falls back to the primary branch — byte-identical to
402
+ // the legacy single-branch behaviour.
403
+ const startPoint = baseBranch || (0, mission_utils_js_1.getPrimaryBranch)();
404
+ gitFn(['-C', mainRepo, 'branch', branchName, startPoint]);
405
+ logFn(fmt.status('PASS', `Created branch ${fmt.branch(branchName)} from ${fmt.branch(startPoint)}.`));
442
406
  }
443
-
444
407
  /**
445
408
  * Persist the resolved mission base as a single machine-readable `Base-Branch:`
446
409
  * line in MISSION.md. Idempotent: a no-op when `baseBranch` is falsy (primary or
@@ -449,346 +412,293 @@ function ensureMissionBranch(mainRepo, branchName, {
449
412
  */
450
413
  // @ts-expect-error implicit any on missionFile/baseBranch
451
414
  function ensureMissionBaseBranchRecorded(missionFile, baseBranch, { logFn = fmt.log.plain } = {}) {
452
- if (!baseBranch || !missionFile || !fs.existsSync(missionFile)) {
453
- return false;
454
- }
455
-
456
- const content = fs.readFileSync(missionFile, 'utf8');
457
- const line = `Base-Branch: ${baseBranch}`;
458
- const existing = content.match(/^Base-Branch:\s*(\S+)\s*$/m);
459
- if (existing && existing[1] === baseBranch) {
460
- return false;
461
- }
462
-
463
- let updated;
464
- if (existing) {
465
- updated = content.replace(/^Base-Branch:\s*\S+\s*$/m, line);
466
- } else {
467
- const lines = content.split('\n');
468
- const insertAt = lines.length > 0 ? 1 : 0;
469
- lines.splice(insertAt, 0, '', line);
470
- updated = lines.join('\n');
471
- }
472
-
473
- fs.writeFileSync(missionFile, updated);
474
- logFn(fmt.status('PASS', `Recorded ${line} in ${fmt.path(missionFile)}`));
475
- return true;
415
+ if (!baseBranch || !missionFile || !fs.existsSync(missionFile)) {
416
+ return false;
417
+ }
418
+ const content = fs.readFileSync(missionFile, 'utf8');
419
+ const line = `Base-Branch: ${baseBranch}`;
420
+ const existing = content.match(/^Base-Branch:\s*(\S+)\s*$/m);
421
+ if (existing && existing[1] === baseBranch) {
422
+ return false;
423
+ }
424
+ let updated;
425
+ if (existing) {
426
+ updated = content.replace(/^Base-Branch:\s*\S+\s*$/m, line);
427
+ }
428
+ else {
429
+ const lines = content.split('\n');
430
+ const insertAt = lines.length > 0 ? 1 : 0;
431
+ lines.splice(insertAt, 0, '', line);
432
+ updated = lines.join('\n');
433
+ }
434
+ fs.writeFileSync(missionFile, updated);
435
+ logFn(fmt.status('PASS', `Recorded ${line} in ${fmt.path(missionFile)}`));
436
+ return true;
476
437
  }
477
-
478
438
  // @ts-expect-error implicit any on mainRepo/targetWorktree/branchName
479
- function ensureWorktree(mainRepo, targetWorktree, branchName, {
480
- existsFn = fs.existsSync,
481
- gitFn = git,
482
- logFn = fmt.log.plain,
483
- errorFn = fmt.log.plainError,
484
- exitFn = process.exit
485
- } = {}) {
486
- if (existsFn(targetWorktree)) {
487
- logFn(fmt.status('PASS', `Worktree directory ${fmt.path(targetWorktree)} already exists.`));
439
+ function ensureWorktree(mainRepo, targetWorktree, branchName, { existsFn = fs.existsSync, gitFn = git_js_1.git, logFn = fmt.log.plain, errorFn = fmt.log.plainError, exitFn = process.exit } = {}) {
440
+ if (existsFn(targetWorktree)) {
441
+ logFn(fmt.status('PASS', `Worktree directory ${fmt.path(targetWorktree)} already exists.`));
442
+ try {
443
+ gitFn(['-C', mainRepo, 'worktree', 'add', targetWorktree, branchName]);
444
+ }
445
+ catch (error) {
446
+ // Ignore "already exists" style failures; the directory is already usable.
447
+ }
448
+ return;
449
+ }
488
450
  try {
489
- gitFn(['-C', mainRepo, 'worktree', 'add', targetWorktree, branchName]);
490
- } catch (error) {
491
- // Ignore "already exists" style failures; the directory is already usable.
451
+ gitFn(['-C', mainRepo, 'worktree', 'add', targetWorktree, branchName]);
452
+ logFn(fmt.status('PASS', `Created worktree at ${fmt.path(targetWorktree)}.`));
453
+ }
454
+ catch (error) {
455
+ errorFn(fmt.status('FAIL', `Could not create worktree: ${ /** @type {any} */(error).message}`));
456
+ exitFn(1);
492
457
  }
493
- return;
494
- }
495
-
496
- try {
497
- gitFn(['-C', mainRepo, 'worktree', 'add', targetWorktree, branchName]);
498
- logFn(fmt.status('PASS', `Created worktree at ${fmt.path(targetWorktree)}.`));
499
- } catch (error) {
500
- errorFn(fmt.status('FAIL', `Could not create worktree: ${/** @type {any} */ (error).message}`));
501
- exitFn(1);
502
- }
503
458
  }
504
-
505
459
  // @ts-expect-error implicit any on targetWorktree
506
460
  function ensureGraphifyWorkspace(targetWorktree, { logFn = fmt.log.plain } = {}) {
507
- const targetPath = path.join(targetWorktree, 'graphify-out');
508
-
509
- if (fs.existsSync(targetPath)) {
510
- try {
511
- if (fs.lstatSync(targetPath).isDirectory()) {
512
- logFn(fmt.status('PASS', `graphify-out directory already exists in the mission worktree at ${fmt.path(targetPath)}.`));
513
- return true;
514
- }
515
- } catch (_) {
516
- // Fall through to the generic warning below.
461
+ const targetPath = path.join(targetWorktree, 'graphify-out');
462
+ if (fs.existsSync(targetPath)) {
463
+ try {
464
+ if (fs.lstatSync(targetPath).isDirectory()) {
465
+ logFn(fmt.status('PASS', `graphify-out directory already exists in the mission worktree at ${fmt.path(targetPath)}.`));
466
+ return true;
467
+ }
468
+ }
469
+ catch (_) {
470
+ // Fall through to the generic warning below.
471
+ }
472
+ logFn(fmt.status('WARN', `${fmt.path(targetPath)} already exists and is not a directory. Leaving it unchanged.`));
473
+ return false;
517
474
  }
518
-
519
- logFn(fmt.status('WARN', `${fmt.path(targetPath)} already exists and is not a directory. Leaving it unchanged.`));
520
- return false;
521
- }
522
-
523
- fs.mkdirSync(targetPath, { recursive: true });
524
- logFn(fmt.status('PASS', `Created independent graphify-out directory in the mission worktree at ${fmt.path(targetPath)}.`));
525
- return true;
475
+ fs.mkdirSync(targetPath, { recursive: true });
476
+ logFn(fmt.status('PASS', `Created independent graphify-out directory in the mission worktree at ${fmt.path(targetPath)}.`));
477
+ return true;
526
478
  }
527
-
528
479
  // @ts-expect-error implicit any on targetWorktree
529
- function ensureGraphifyIgnore(targetWorktree, { gitFn = git, logFn = fmt.log.plain } = {}) {
530
- const targetPath = path.join(targetWorktree, '.graphifyignore');
531
-
532
- if (fs.existsSync(targetPath) || fs.existsSync(path.join(targetWorktree, '.gitignore'))) {
533
- logFn(fmt.status('PASS', '.graphifyignore or .gitignore already exists. Leaving unchanged.'));
534
- return true;
535
- }
536
-
537
- if (!fs.existsSync(targetWorktree)) {
538
- logFn(fmt.status('PASS', `Worktree ${fmt.path(targetWorktree)} does not exist yet. .graphifyignore will be created by the draft agent.`));
480
+ function ensureGraphifyIgnore(targetWorktree, { gitFn = git_js_1.git, logFn = fmt.log.plain } = {}) {
481
+ const targetPath = path.join(targetWorktree, '.graphifyignore');
482
+ if (fs.existsSync(targetPath) || fs.existsSync(path.join(targetWorktree, '.gitignore'))) {
483
+ logFn(fmt.status('PASS', '.graphifyignore or .gitignore already exists. Leaving unchanged.'));
484
+ return true;
485
+ }
486
+ if (!fs.existsSync(targetWorktree)) {
487
+ logFn(fmt.status('PASS', `Worktree ${fmt.path(targetWorktree)} does not exist yet. .graphifyignore will be created by the draft agent.`));
488
+ return true;
489
+ }
490
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
491
+ fs.writeFileSync(targetPath, '# Files owned by the workflow toolkit — not part of the project codebase.\n' +
492
+ '# Prevents graphify from extracting session logs, agent state, and tool caches.\n' +
493
+ '.workflow/\n', { encoding: 'utf-8' });
494
+ try {
495
+ const gitRoot = targetWorktree;
496
+ gitFn(['-C', gitRoot, 'add', '.graphifyignore']);
497
+ gitFn(['-C', gitRoot, 'commit', '-m', 'workflow: add .graphifyignore to exclude .workflow/ from graphify']);
498
+ logFn(fmt.status('PASS', `Created and committed .graphifyignore in ${fmt.path(gitRoot)}.`));
499
+ }
500
+ catch (error) {
501
+ logFn(fmt.status('WARN', `Created .graphifyignore but could not commit: ${ /** @type {any} */(error).message}. It will be picked up by the draft safety harness.`));
502
+ }
539
503
  return true;
540
- }
541
-
542
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
543
- fs.writeFileSync(targetPath,
544
- '# Files owned by the workflow toolkit — not part of the project codebase.\n' +
545
- '# Prevents graphify from extracting session logs, agent state, and tool caches.\n' +
546
- '.workflow/\n',
547
- { encoding: 'utf-8' }
548
- );
549
-
550
- try {
551
- const gitRoot = targetWorktree;
552
- gitFn(['-C', gitRoot, 'add', '.graphifyignore']);
553
- gitFn(['-C', gitRoot, 'commit', '-m', 'workflow: add .graphifyignore to exclude .workflow/ from graphify']);
554
- logFn(fmt.status('PASS', `Created and committed .graphifyignore in ${fmt.path(gitRoot)}.`));
555
- } catch (error) {
556
- logFn(fmt.status('WARN', `Created .graphifyignore but could not commit: ${/** @type {any} */ (error).message}. It will be picked up by the draft safety harness.`));
557
- }
558
-
559
- return true;
560
504
  }
561
-
562
505
  // @ts-expect-error implicit any on targetWorktree/slug
563
506
  function ensureMissionFile(targetWorktree, slug, { logFn = fmt.log.plain } = {}) {
564
- const missionDir = missionDirForSlug(targetWorktree, slug);
565
- if (!fs.existsSync(missionDir)) {
566
- fs.mkdirSync(missionDir, { recursive: true });
567
- }
568
-
569
- const missionFile = path.join(missionDir, 'MISSION.md');
570
- if (fs.existsSync(missionFile)) {
571
- logFn(fmt.status('PASS', `MISSION.md already exists at ${fmt.path(missionFile)}`));
507
+ const missionDir = (0, mission_utils_js_1.missionDirForSlug)(targetWorktree, slug);
508
+ if (!fs.existsSync(missionDir)) {
509
+ fs.mkdirSync(missionDir, { recursive: true });
510
+ }
511
+ const missionFile = path.join(missionDir, 'MISSION.md');
512
+ if (fs.existsSync(missionFile)) {
513
+ logFn(fmt.status('PASS', `MISSION.md already exists at ${fmt.path(missionFile)}`));
514
+ return missionFile;
515
+ }
516
+ const template = fs.readFileSync(MISSION_SCAFFOLD_PATH, 'utf8').replaceAll('{{slug}}', slug);
517
+ fs.writeFileSync(missionFile, template);
518
+ logFn(fmt.status('PASS', `Scaffolded MISSION.md at ${fmt.path(missionFile)}`));
519
+ logFn(fmt.status('INFO', 'Note: Draft mode involves AI refinement. Use the draft prompt to complete the contract.'));
572
520
  return missionFile;
573
- }
574
-
575
- const template = fs.readFileSync(MISSION_SCAFFOLD_PATH, 'utf8').replaceAll('{{slug}}', slug);
576
- fs.writeFileSync(missionFile, template);
577
- logFn(fmt.status('PASS', `Scaffolded MISSION.md at ${fmt.path(missionFile)}`));
578
- logFn(fmt.status('INFO', 'Note: Draft mode involves AI refinement. Use the draft prompt to complete the contract.'));
579
- return missionFile;
580
521
  }
581
-
582
522
  // @ts-expect-error implicit any on mainRepo
583
- function ensureDraftRepoConfigCommitted(mainRepo, {
584
- getWorktreeStatusFn = getWorktreeStatus,
585
- errorFn = fmt.log.plainError
586
- } = {}) {
587
- const dirtyEntries = getWorktreeStatusFn(mainRepo);
588
- if (!dirtyEntries || dirtyEntries.length === 0) {
589
- return true;
590
- }
591
-
592
- const configPaths = new Set([
593
- 'workflow.config.json',
594
- 'backlog/config.yml',
595
- 'config/state-map.json'
596
- ]);
597
- const dirtyConfigEntries = dirtyEntries
598
- .map(parseDirtyEntry)
599
- .filter(entry => configPaths.has(entry.filePath));
600
-
601
- if (dirtyConfigEntries.length === 0) {
602
- return true;
603
- }
604
-
605
- errorFn(fmt.status('FAIL', `Draft preflight: repo-state config that affects mission layout is uncommitted in ${fmt.path(mainRepo)}.`));
606
- for (const entry of dirtyConfigEntries) {
607
- errorFn(` ${entry.status} ${entry.filePath}`);
608
- }
609
- errorFn('Commit these repo-state config changes before running draft. Mission worktrees are created from HEAD, so uncommitted config would produce a stale layout.');
610
- return false;
523
+ function ensureDraftRepoConfigCommitted(mainRepo, { getWorktreeStatusFn = git_js_1.getWorktreeStatus, errorFn = fmt.log.plainError } = {}) {
524
+ const dirtyEntries = getWorktreeStatusFn(mainRepo);
525
+ if (!dirtyEntries || dirtyEntries.length === 0) {
526
+ return true;
527
+ }
528
+ const configPaths = new Set([
529
+ 'workflow.config.json',
530
+ 'backlog/config.yml',
531
+ 'config/state-map.json'
532
+ ]);
533
+ const dirtyConfigEntries = dirtyEntries
534
+ .map(parseDirtyEntry)
535
+ .filter(entry => configPaths.has(entry.filePath));
536
+ if (dirtyConfigEntries.length === 0) {
537
+ return true;
538
+ }
539
+ errorFn(fmt.status('FAIL', `Draft preflight: repo-state config that affects mission layout is uncommitted in ${fmt.path(mainRepo)}.`));
540
+ for (const entry of dirtyConfigEntries) {
541
+ errorFn(` ${entry.status} ${entry.filePath}`);
542
+ }
543
+ errorFn('Commit these repo-state config changes before running draft. Mission worktrees are created from HEAD, so uncommitted config would produce a stale layout.');
544
+ return false;
611
545
  }
612
-
613
546
  // @ts-expect-error implicit any on targetWorktree/mainRepo/slug
614
- function bootstrapBacklogTask(targetWorktree, mainRepo, slug, {
615
- resolveTaskFileFn = resolveTaskFile,
616
- reportTaskResolutionFn = reportTaskResolution,
617
- gitFn = git,
618
- logFn = fmt.log.plain,
619
- errorFn = fmt.log.plainError,
620
- syntheticTask = null
621
- } = {}) {
622
- const taskResolution = resolveTaskFileFn(slug, targetWorktree);
623
- if (taskResolution.ok) {
624
- logFn(fmt.status('PASS', `Backlog task for ${fmt.slug(slug)} already exists in worktree.`));
625
- return true;
626
- }
627
-
628
- if (taskResolution.reason === 'ambiguous') {
629
- reportTaskResolutionFn(taskResolution, slug, errorFn);
630
- return false;
631
- }
632
-
633
- if (syntheticTask) {
634
- const { tasksDir } = getTaskStorage(targetWorktree);
635
- const taskPath = path.join(tasksDir, `${slug} - ${slugifyDraftIntent(/** @type {any} */ (syntheticTask).title || slug) || 'mission'}.md`);
636
- const body = [
637
- '---',
638
- `id: ${/** @type {any} */ (syntheticTask).id || syntheticTaskId(slug, /** @type {any} */ (syntheticTask).intent || slug)}`,
639
- `title: ${/** @type {any} */ (syntheticTask).title || slug}`,
640
- 'status: backlog',
641
- 'assignee: []',
642
- "created_date: '" + new Date().toISOString().slice(0, 16).replace('T', ' ') + "'",
643
- 'labels: [unknown]',
644
- 'dependencies: []',
645
- 'source: synthetic',
646
- '---',
647
- '',
648
- '## Description',
649
- '',
650
- /** @type {any} */ (syntheticTask).intent || `Synthetic task created for ${slug}.`,
651
- ''
652
- ].join('\n');
653
-
654
- fs.mkdirSync(path.dirname(taskPath), { recursive: true });
655
- fs.writeFileSync(taskPath, body, 'utf8');
656
- logFn(fmt.status('PASS', `Created synthetic backlog task at ${fmt.path(path.relative(targetWorktree, taskPath))}.`));
657
-
547
+ function bootstrapBacklogTask(targetWorktree, mainRepo, slug, { resolveTaskFileFn = backlog_js_1.resolveTaskFile, reportTaskResolutionFn = backlog_js_1.reportTaskResolution, gitFn = git_js_1.git, logFn = fmt.log.plain, errorFn = fmt.log.plainError, syntheticTask = null } = {}) {
548
+ const taskResolution = resolveTaskFileFn(slug, targetWorktree);
549
+ if (taskResolution.ok) {
550
+ logFn(fmt.status('PASS', `Backlog task for ${fmt.slug(slug)} already exists in worktree.`));
551
+ return true;
552
+ }
553
+ if (taskResolution.reason === 'ambiguous') {
554
+ reportTaskResolutionFn(taskResolution, slug, errorFn);
555
+ return false;
556
+ }
557
+ if (syntheticTask) {
558
+ const { tasksDir } = (0, backlog_js_1.getTaskStorage)(targetWorktree);
559
+ const taskPath = path.join(tasksDir, `${slug} - ${slugifyDraftIntent(/** @type {any} */ (syntheticTask).title || slug) || 'mission'}.md`);
560
+ const body = [
561
+ '---',
562
+ `id: ${ /** @type {any} */(syntheticTask).id || syntheticTaskId(slug, /** @type {any} */ (syntheticTask).intent || slug)}`,
563
+ `title: ${ /** @type {any} */(syntheticTask).title || slug}`,
564
+ 'status: backlog',
565
+ 'assignee: []',
566
+ "created_date: '" + new Date().toISOString().slice(0, 16).replace('T', ' ') + "'",
567
+ 'labels: [unknown]',
568
+ 'dependencies: []',
569
+ 'source: synthetic',
570
+ '---',
571
+ '',
572
+ '## Description',
573
+ '',
574
+ /** @type {any} */ (syntheticTask).intent || `Synthetic task created for ${slug}.`,
575
+ ''
576
+ ].join('\n');
577
+ fs.mkdirSync(path.dirname(taskPath), { recursive: true });
578
+ fs.writeFileSync(taskPath, body, 'utf8');
579
+ logFn(fmt.status('PASS', `Created synthetic backlog task at ${fmt.path(path.relative(targetWorktree, taskPath))}.`));
580
+ try {
581
+ const relativePath = path.relative(targetWorktree, taskPath);
582
+ gitFn(['-C', targetWorktree, 'add', relativePath]);
583
+ gitFn(['-C', targetWorktree, 'commit', '-m', `backlog(${slug}): create synthetic task`]);
584
+ logFn(fmt.status('PASS', 'Committed synthetic task in worktree.'));
585
+ }
586
+ catch (error) {
587
+ errorFn(fmt.status('FAIL', `Could not commit synthetic task: ${ /** @type {any} */(error).message}`));
588
+ return false;
589
+ }
590
+ return true;
591
+ }
592
+ logFn(fmt.status('INFO', `Backlog task for ${fmt.slug(slug)} not found in worktree. Attempting to bootstrap from ${fmt.path(mainRepo)}...`));
593
+ const mainResolution = resolveTaskFileFn(slug, mainRepo);
594
+ if (!mainResolution.ok) {
595
+ reportTaskResolutionFn(mainResolution, slug, errorFn);
596
+ return false;
597
+ }
598
+ // @ts-expect-error mainResolution.taskFile may be undefined
599
+ const relativePath = path.relative(mainRepo, mainResolution.taskFile);
600
+ const targetPath = path.join(targetWorktree || '', relativePath);
601
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
602
+ // @ts-expect-error mainResolution.taskFile may be undefined
603
+ fs.copyFileSync(mainResolution.taskFile, targetPath);
604
+ logFn(fmt.status('PASS', `Bootstrapped ${fmt.path(relativePath)} from main repo.`));
658
605
  try {
659
- const relativePath = path.relative(targetWorktree, taskPath);
660
- gitFn(['-C', targetWorktree, 'add', relativePath]);
661
- gitFn(['-C', targetWorktree, 'commit', '-m', `backlog(${slug}): create synthetic task`]);
662
- logFn(fmt.status('PASS', 'Committed synthetic task in worktree.'));
663
- } catch (error) {
664
- errorFn(fmt.status('FAIL', `Could not commit synthetic task: ${/** @type {any} */ (error).message}`));
665
- return false;
606
+ gitFn(['-C', targetWorktree, 'add', relativePath]);
607
+ gitFn(['-C', targetWorktree, 'commit', '-m', `backlog(${slug}): bootstrap task from ${(0, mission_utils_js_1.getPrimaryBranch)()}`]);
608
+ logFn(fmt.status('PASS', 'Committed bootstrapped task in worktree.'));
609
+ }
610
+ catch (error) {
611
+ errorFn(fmt.status('FAIL', `Could not commit bootstrapped task: ${ /** @type {any} */(error).message}`));
612
+ return false;
666
613
  }
667
614
  return true;
668
- }
669
-
670
- logFn(fmt.status('INFO', `Backlog task for ${fmt.slug(slug)} not found in worktree. Attempting to bootstrap from ${fmt.path(mainRepo)}...`));
671
- const mainResolution = resolveTaskFileFn(slug, mainRepo);
672
- if (!mainResolution.ok) {
673
- reportTaskResolutionFn(mainResolution, slug, errorFn);
674
- return false;
675
- }
676
-
677
- // @ts-expect-error mainResolution.taskFile may be undefined
678
- const relativePath = path.relative(mainRepo, mainResolution.taskFile);
679
- const targetPath = path.join(targetWorktree || '', relativePath);
680
-
681
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
682
- // @ts-expect-error mainResolution.taskFile may be undefined
683
- fs.copyFileSync(mainResolution.taskFile, targetPath);
684
-
685
- logFn(fmt.status('PASS', `Bootstrapped ${fmt.path(relativePath)} from main repo.`));
686
-
687
- try {
688
- gitFn(['-C', targetWorktree, 'add', relativePath]);
689
- gitFn(['-C', targetWorktree, 'commit', '-m', `backlog(${slug}): bootstrap task from ${getPrimaryBranch()}`]);
690
- logFn(fmt.status('PASS', 'Committed bootstrapped task in worktree.'));
691
- } catch (error) {
692
- errorFn(fmt.status('FAIL', `Could not commit bootstrapped task: ${/** @type {any} */ (error).message}`));
693
- return false;
694
- }
695
-
696
- return true;
697
615
  }
698
-
699
616
  // @ts-expect-error implicit any on rootDir
700
617
  function resolveVerifyCmd(rootDir) {
701
- return formatVerificationCommand(undefined, rootDir);
618
+ return (0, verification_js_1.formatVerificationCommand)(undefined, rootDir);
702
619
  }
703
-
704
620
  // @ts-expect-error implicit any on slug/promptRoot
705
621
  function resolveTaskPath(slug, promptRoot) {
706
- const resolution = resolveTaskFile(slug, promptRoot);
707
- if (resolution && resolution.ok && resolution.taskFile) {
708
- return resolution.taskFile;
709
- }
710
- return path.join(promptRoot, 'backlog', 'tasks', `<${slug}>.md`);
622
+ const resolution = (0, backlog_js_1.resolveTaskFile)(slug, promptRoot);
623
+ if (resolution && resolution.ok && resolution.taskFile) {
624
+ return resolution.taskFile;
625
+ }
626
+ return path.join(promptRoot, 'backlog', 'tasks', `<${slug}>.md`);
711
627
  }
712
-
713
628
  // @ts-expect-error implicit any on taskPath
714
629
  function resolveClassificationInstructions(taskPath) {
715
- if (taskPath && fs.existsSync(taskPath)) {
716
- const content = fs.readFileSync(taskPath, 'utf8');
717
- if (/^source:\s*synthetic\s*$/mi.test(content)) {
718
- return 'because this task was synthesized by the harness, preserve the `unknown` label unless you have concrete repo-specific evidence to replace it. Do not add a separate frontmatter field for mission type.';
630
+ if (taskPath && fs.existsSync(taskPath)) {
631
+ const content = fs.readFileSync(taskPath, 'utf8');
632
+ if (/^source:\s*synthetic\s*$/mi.test(content)) {
633
+ return 'because this task was synthesized by the harness, preserve the `unknown` label unless you have concrete repo-specific evidence to replace it. Do not add a separate frontmatter field for mission type.';
634
+ }
719
635
  }
720
- }
721
- return 'set exactly one of `ai_sdlc` or `user_value` in the Backlog task labels — plus optionally `bug` for bug-fix missions. 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.';
636
+ return 'set exactly one of `ai_sdlc` or `user_value` in the Backlog task labels — plus optionally `bug` for bug-fix missions. 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.';
722
637
  }
723
-
724
638
  // @ts-expect-error implicit any on slug/rootDir/worktree
725
639
  function buildDraftPrompt(slug, { rootDir = process.cwd(), worktree = null } = {}) {
726
- const template = fs.readFileSync(DRAFT_PROMPT_PATH, 'utf8');
727
- const promptRoot = worktree || rootDir;
728
- const year = getMissionYear(slug, promptRoot) || String(new Date().getFullYear());
729
- const missionPath = path.join(missionDirForSlug(promptRoot, slug), 'MISSION.md');
730
- const missionDir = path.dirname(missionPath);
731
- const taskPath = resolveTaskPath(slug, promptRoot);
732
- return template
733
- .replaceAll('{{slug}}', slug)
734
- .replaceAll('{{year}}', year)
735
- .replaceAll('{{missionPath}}', missionPath)
736
- .replaceAll('{{missionDir}}', missionDir)
737
- .replaceAll('{{taskPath}}', taskPath)
738
- .replaceAll('{{classificationInstructions}}', resolveClassificationInstructions(taskPath))
739
- .replaceAll('{{verifyCmd}}', resolveVerifyCmd(promptRoot));
640
+ const template = fs.readFileSync(DRAFT_PROMPT_PATH, 'utf8');
641
+ const promptRoot = worktree || rootDir;
642
+ const year = (0, mission_utils_js_1.getMissionYear)(slug, promptRoot) || String(new Date().getFullYear());
643
+ const missionPath = path.join((0, mission_utils_js_1.missionDirForSlug)(promptRoot, slug), 'MISSION.md');
644
+ const missionDir = path.dirname(missionPath);
645
+ const taskPath = resolveTaskPath(slug, promptRoot);
646
+ return template
647
+ .replaceAll('{{slug}}', slug)
648
+ .replaceAll('{{year}}', year)
649
+ .replaceAll('{{missionPath}}', missionPath)
650
+ .replaceAll('{{missionDir}}', missionDir)
651
+ .replaceAll('{{taskPath}}', taskPath)
652
+ .replaceAll('{{classificationInstructions}}', resolveClassificationInstructions(taskPath))
653
+ .replaceAll('{{verifyCmd}}', resolveVerifyCmd(promptRoot));
740
654
  }
741
-
742
655
  // @ts-expect-error implicit any on slug
743
656
  function fallbackDraftCommitMessage(slug) {
744
- return `draft(${slug}): capture agent output`;
657
+ return `draft(${slug}): capture agent output`;
745
658
  }
746
-
747
659
  // @ts-expect-error implicit any on slug/worktree
748
- function validateDraftClassification(slug, worktree, {
749
- resolveMissionClassificationFn = stats.resolveMissionClassification,
750
- errorFn = fmt.log.plainError
751
- } = {}) {
752
- try {
753
- const { classification, error: classificationError } = resolveMissionClassificationFn(slug, worktree);
754
- if (!classification) {
755
- if (classificationError) {errorFn(fmt.status('FAIL', classificationError));}
756
- return { ok: true, classification: null };
757
- }
758
- return { ok: true, classification };
759
- } catch (error) {
760
- if (/** @type {any} */ (error).message.includes('Missing or invalid classification')) {
761
- return { ok: true, classification: null };
762
- }
763
- errorFn(fmt.status('FAIL', /** @type {any} */ (error).message));
764
- return { ok: false, reason: 'invalid-classification' };
765
- }
660
+ function validateDraftClassification(slug, worktree, { resolveMissionClassificationFn = stats.resolveMissionClassification, errorFn = fmt.log.plainError } = {}) {
661
+ try {
662
+ const { classification, error: classificationError } = resolveMissionClassificationFn(slug, worktree);
663
+ if (!classification) {
664
+ if (classificationError) {
665
+ errorFn(fmt.status('FAIL', classificationError));
666
+ }
667
+ return { ok: true, classification: null };
668
+ }
669
+ return { ok: true, classification };
670
+ }
671
+ catch (error) {
672
+ if ( /** @type {any} */(error).message.includes('Missing or invalid classification')) {
673
+ return { ok: true, classification: null };
674
+ }
675
+ errorFn(fmt.status('FAIL', /** @type {any} */ (error).message));
676
+ return { ok: false, reason: 'invalid-classification' };
677
+ }
766
678
  }
767
-
768
679
  // @ts-expect-error implicit any on slug/worktree
769
- function normalizeDraftClassification(slug, worktree, {
770
- resolveMissionClassificationFn = stats.resolveMissionClassification,
771
- errorFn = fmt.log.plainError
772
- } = {}) {
773
- try {
774
- const { classification, error: classificationError } = resolveMissionClassificationFn(slug, worktree);
775
- if (!classification) {
776
- if (classificationError) {errorFn(fmt.status('FAIL', classificationError));}
777
- return { ok: false, reason: 'missing-classification' };
778
- }
779
- return { ok: true, classification };
780
- } catch (error) {
781
- if (/** @type {any} */ (error).message.includes('Missing or invalid classification')) {
782
- return { ok: false, reason: 'missing-classification' };
783
- }
784
- errorFn(fmt.status('FAIL', /** @type {any} */ (error).message));
785
- return { ok: false, reason: 'invalid-classification' };
786
- }
680
+ function normalizeDraftClassification(slug, worktree, { resolveMissionClassificationFn = stats.resolveMissionClassification, errorFn = fmt.log.plainError } = {}) {
681
+ try {
682
+ const { classification, error: classificationError } = resolveMissionClassificationFn(slug, worktree);
683
+ if (!classification) {
684
+ if (classificationError) {
685
+ errorFn(fmt.status('FAIL', classificationError));
686
+ }
687
+ return { ok: false, reason: 'missing-classification' };
688
+ }
689
+ return { ok: true, classification };
690
+ }
691
+ catch (error) {
692
+ if ( /** @type {any} */(error).message.includes('Missing or invalid classification')) {
693
+ return { ok: false, reason: 'missing-classification' };
694
+ }
695
+ errorFn(fmt.status('FAIL', /** @type {any} */ (error).message));
696
+ return { ok: false, reason: 'invalid-classification' };
697
+ }
787
698
  }
788
-
789
699
  // @ts-expect-error implicit any on slug/rootDir/worktree
790
700
  function buildRestartPrompt(slug, { rootDir = process.cwd(), worktree = null } = {}) {
791
- return `${buildDraftPrompt(slug, { rootDir, worktree })}
701
+ return `${buildDraftPrompt(slug, { rootDir, worktree })}
792
702
 
793
703
  Focused repair:
794
704
  - update the backlog task so labels contain exactly one of \`ai_sdlc\` or \`user_value\` (plus optionally \`bug\` if this is a bug fix)
@@ -797,194 +707,152 @@ Focused repair:
797
707
  - if both classification labels are present, keep only the correct one
798
708
  `;
799
709
  }
800
-
801
710
  // @ts-expect-error implicit any on slug/worktree
802
- async function restartDraftAgent(slug, worktree, {
803
- selectAgentFn = selectAgent,
804
- startDraftAgentFn = startDraftAgent,
805
- readAgentConfigOrExitFn = readAgentConfigOrExit,
806
- logFn = fmt.log.plain,
807
- errorFn = fmt.log.plainError
808
- } = {}) {
809
- const agentConfig = readAgentConfigOrExitFn();
810
- const agent = selectAgentFn('draft', { config: agentConfig });
811
-
812
- const prompt = buildRestartPrompt(slug, { rootDir: worktree, worktree });
813
- logFn('Relaunching draft agent to repair mission type labels...');
814
- // @ts-expect-error startDraftAgentFn accepts extra properties
815
- const { agent: actualAgent, result } = await startDraftAgentFn({ prompt, worktree, agent });
816
- logFn(`Restart draft agent family: ${fmt.agent(/** @type {any} */ (actualAgent))}`);
817
-
818
- if (result.error) {
819
- errorFn(fmt.status('FAIL', `Could not restart draft agent (${fmt.agent(/** @type {any} */ (actualAgent))}): ${/** @type {any} */ (result.error).message}`));
820
- return false;
821
- }
822
-
823
- if (typeof /** @type {any} */ (result).status === 'number' && /** @type {any} */ (result).status !== 0) {
824
- errorFn(fmt.status('FAIL', `Restart draft agent (${fmt.agent(/** @type {any} */ (actualAgent))}) exited with status ${/** @type {any} */ (result).status}.`));
825
- return false;
826
- }
827
-
828
- return true;
711
+ async function restartDraftAgent(slug, worktree, { selectAgentFn = agents_js_1.selectAgent, startDraftAgentFn = agents_js_1.startDraftAgent, readAgentConfigOrExitFn = agents_js_1.readAgentConfigOrExit, logFn = fmt.log.plain, errorFn = fmt.log.plainError } = {}) {
712
+ const agentConfig = readAgentConfigOrExitFn();
713
+ const agent = selectAgentFn('draft', { config: agentConfig });
714
+ const prompt = buildRestartPrompt(slug, { rootDir: worktree, worktree });
715
+ logFn('Relaunching draft agent to repair mission type labels...');
716
+ // @ts-expect-error startDraftAgentFn accepts extra properties
717
+ const { agent: actualAgent, result } = await startDraftAgentFn({ prompt, worktree, agent });
718
+ logFn(`Restart draft agent family: ${fmt.agent(/** @type {any} */ (actualAgent))}`);
719
+ if (result.error) {
720
+ errorFn(fmt.status('FAIL', `Could not restart draft agent (${fmt.agent(/** @type {any} */ (actualAgent))}): ${ /** @type {any} */(result.error).message}`));
721
+ return false;
722
+ }
723
+ if (typeof /** @type {any} */ (result).status === 'number' && /** @type {any} */ (result).status !== 0) {
724
+ errorFn(fmt.status('FAIL', `Restart draft agent (${fmt.agent(/** @type {any} */ (actualAgent))}) exited with status ${ /** @type {any} */(result).status}.`));
725
+ return false;
726
+ }
727
+ return true;
829
728
  }
830
-
831
729
  // @ts-expect-error implicit any on entry
832
730
  function parseDirtyEntry(entry) {
833
- const match = entry.match(/^(.{1,2})\s+(.*)$/);
834
- const status = (match ? match[1] : entry.slice(0, 2)).padEnd(2, ' ');
835
- const rawPath = (match ? match[2] : entry.slice(2)).trim();
836
- // For renames git reports `<old> -> <new>`; each side is independently quoted
837
- // and the ` -> ` separator is always literal, so split before unquoting. Both
838
- // sides must be decoded because git C-escapes any path with a space or unusual
839
- // byte under core.quotePath — otherwise `git add --` is handed a quote-wrapped
840
- // pathspec that matches no file and the fallback commit aborts.
841
- const renameParts = rawPath.includes(' -> ') ? rawPath.split(' -> ') : null;
842
- const sourcePath = renameParts ? unquoteGitStatusPath(renameParts[0].trim()) : null;
843
- const filePath = unquoteGitStatusPath(
844
- renameParts ? renameParts[renameParts.length - 1].trim() : rawPath
845
- );
846
- return { status, filePath, sourcePath };
731
+ const match = entry.match(/^(.{1,2})\s+(.*)$/);
732
+ const status = (match ? match[1] : entry.slice(0, 2)).padEnd(2, ' ');
733
+ const rawPath = (match ? match[2] : entry.slice(2)).trim();
734
+ // For renames git reports `<old> -> <new>`; each side is independently quoted
735
+ // and the ` -> ` separator is always literal, so split before unquoting. Both
736
+ // sides must be decoded because git C-escapes any path with a space or unusual
737
+ // byte under core.quotePath — otherwise `git add --` is handed a quote-wrapped
738
+ // pathspec that matches no file and the fallback commit aborts.
739
+ const renameParts = rawPath.includes(' -> ') ? rawPath.split(' -> ') : null;
740
+ const sourcePath = renameParts ? (0, active_js_1.unquoteGitStatusPath)(renameParts[0].trim()) : null;
741
+ const filePath = (0, active_js_1.unquoteGitStatusPath)(renameParts ? renameParts[renameParts.length - 1].trim() : rawPath);
742
+ return { status, filePath, sourcePath };
847
743
  }
848
-
849
744
  // @ts-expect-error implicit any on status
850
745
  function isUnmergedStatus(status) {
851
- return ['DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU'].includes(status);
746
+ return ['DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU'].includes(status);
852
747
  }
853
-
854
748
  // @ts-expect-error implicit any on status
855
749
  function isDeletedStatus(status) {
856
- return status.includes('D') && !isUnmergedStatus(status);
750
+ return status.includes('D') && !isUnmergedStatus(status);
857
751
  }
858
-
859
752
  // @ts-expect-error implicit any on filePath/slug
860
753
  function isMissionTaskPath(filePath, slug) {
861
- if (!filePath) {return false;}
862
- const taskPattern = new RegExp(`^backlog/(?:tasks|completed)/[^/]*${slug}(?:\\b|[^/]*$)`);
863
- return taskPattern.test(filePath);
754
+ if (!filePath) {
755
+ return false;
756
+ }
757
+ const taskPattern = new RegExp(`^backlog/(?:tasks|completed)/[^/]*${slug}(?:\\b|[^/]*$)`);
758
+ return taskPattern.test(filePath);
864
759
  }
865
-
866
760
  // @ts-expect-error implicit any on filePath/slug/worktree
867
761
  function isExpectedDraftPath(filePath, slug, worktree) {
868
- const missionDir = findMissionDir(slug, worktree);
869
- const missionPrefix = missionDir
870
- ? `${path.relative(worktree, missionDir)}/`
871
- : path.relative(worktree, missionDirForSlug(worktree, slug)).split(path.sep).join('/') + '/';
872
- return filePath.startsWith(missionPrefix) || isMissionTaskPath(filePath, slug);
762
+ const missionDir = (0, mission_utils_js_1.findMissionDir)(slug, worktree);
763
+ const missionPrefix = missionDir
764
+ ? `${path.relative(worktree, missionDir)}/`
765
+ : path.relative(worktree, (0, mission_utils_js_1.missionDirForSlug)(worktree, slug)).split(path.sep).join('/') + '/';
766
+ return filePath.startsWith(missionPrefix) || isMissionTaskPath(filePath, slug);
873
767
  }
874
-
875
768
  // @ts-expect-error implicit any on dirtyEntries/slug/worktree
876
769
  function classifyDraftEntries(dirtyEntries, slug, worktree) {
877
- const parsedEntries = dirtyEntries.map(parseDirtyEntry);
878
- const conflictEntries = parsedEntries.filter((/** @type {any} */ entry) => isUnmergedStatus(entry.status));
879
- const stagedEntries = parsedEntries.filter((/** @type {any} */ entry) => !isUnmergedStatus(entry.status));
880
- const expectedEntries = stagedEntries.filter((/** @type {any} */ entry) => isExpectedDraftPath(entry.filePath, slug, worktree));
881
- const unexpectedEntries = stagedEntries.filter((/** @type {any} */ entry) => !isExpectedDraftPath(entry.filePath, slug, worktree));
882
-
883
- return { conflictEntries, expectedEntries, unexpectedEntries };
770
+ const parsedEntries = dirtyEntries.map(parseDirtyEntry);
771
+ const conflictEntries = parsedEntries.filter((/** @type {any} */ entry) => isUnmergedStatus(entry.status));
772
+ const stagedEntries = parsedEntries.filter((/** @type {any} */ entry) => !isUnmergedStatus(entry.status));
773
+ const expectedEntries = stagedEntries.filter((/** @type {any} */ entry) => isExpectedDraftPath(entry.filePath, slug, worktree));
774
+ const unexpectedEntries = stagedEntries.filter((/** @type {any} */ entry) => !isExpectedDraftPath(entry.filePath, slug, worktree));
775
+ return { conflictEntries, expectedEntries, unexpectedEntries };
884
776
  }
885
-
886
777
  // @ts-expect-error implicit any on slug/worktree/conflictEntries
887
- function resolveMissionSpecificDraftConflicts({ slug, worktree, conflictEntries, gitImpl = git, logFn = fmt.log.plain }) {
888
- const sharedConflicts = conflictEntries.filter((/** @type {any} */ entry) => !isExpectedDraftPath(entry.filePath, slug, worktree));
889
- if (sharedConflicts.length > 0) {
890
- const area = findMissionArea(findMissionDir(slug, worktree) || missionDirForSlug(worktree, slug));
891
- const sharedFiles = sharedConflicts.map((/** @type {any} */ entry) => entry.filePath);
892
- throw new Error(
893
- `Draft safety harness found shared-file conflicts: ${sharedFiles.join(', ')}. ` +
894
- `Run "px resolve-conflict ${slug}" from ${worktree}, then re-run ${formatVerificationCommand(area, worktree)}.`
895
- );
896
- }
897
-
898
- if (conflictEntries.length === 0) {
899
- return;
900
- }
901
-
902
- logFn(fmt.status('WARN', 'Draft safety harness found mission-specific merge conflicts. Auto-resolving with --theirs:'));
903
- for (const entry of conflictEntries) {
904
- logFn(` ${entry.filePath}`);
905
- gitImpl(['-C', worktree, 'checkout', '--theirs', '--', entry.filePath]);
906
- gitImpl(['-C', worktree, 'add', '--', entry.filePath]);
907
- }
778
+ function resolveMissionSpecificDraftConflicts({ slug, worktree, conflictEntries, gitImpl = git_js_1.git, logFn = fmt.log.plain }) {
779
+ const sharedConflicts = conflictEntries.filter((/** @type {any} */ entry) => !isExpectedDraftPath(entry.filePath, slug, worktree));
780
+ if (sharedConflicts.length > 0) {
781
+ const area = (0, mission_utils_js_1.findMissionArea)((0, mission_utils_js_1.findMissionDir)(slug, worktree) || (0, mission_utils_js_1.missionDirForSlug)(worktree, slug));
782
+ const sharedFiles = sharedConflicts.map((/** @type {any} */ entry) => entry.filePath);
783
+ throw new Error(`Draft safety harness found shared-file conflicts: ${sharedFiles.join(', ')}. ` +
784
+ `Run "px resolve-conflict ${slug}" from ${worktree}, then re-run ${(0, verification_js_1.formatVerificationCommand)(area, worktree)}.`);
785
+ }
786
+ if (conflictEntries.length === 0) {
787
+ return;
788
+ }
789
+ logFn(fmt.status('WARN', 'Draft safety harness found mission-specific merge conflicts. Auto-resolving with --theirs:'));
790
+ for (const entry of conflictEntries) {
791
+ logFn(` ${entry.filePath}`);
792
+ gitImpl(['-C', worktree, 'checkout', '--theirs', '--', entry.filePath]);
793
+ gitImpl(['-C', worktree, 'add', '--', entry.filePath]);
794
+ }
908
795
  }
909
-
910
796
  // @ts-expect-error implicit any on slug/worktree/dirtyEntries
911
- function enforceDraftCommitSafety({ slug, worktree, dirtyEntries = getWorktreeStatus(worktree), gitImpl = git, logFn = fmt.log.plain, errorFn = fmt.log.plainError }) {
912
- if (dirtyEntries.length === 0) {
913
- logFn(fmt.status('PASS', 'Draft safety harness: no uncommitted changes left behind.'));
914
- return false;
915
- }
916
-
917
- logFn(fmt.status('WARN', 'Draft safety harness: draft agent left uncommitted changes. Creating fallback commit.'));
918
- for (const entry of dirtyEntries) {
919
- logFn(` ${entry}`);
920
- }
921
-
922
- const { conflictEntries, expectedEntries, unexpectedEntries } = classifyDraftEntries(dirtyEntries, slug, worktree);
923
- // @ts-expect-error errorFn not in type
924
- resolveMissionSpecificDraftConflicts({ slug, worktree, conflictEntries, gitImpl, logFn, errorFn });
925
-
926
- const deletedTaskEntries = [...expectedEntries, ...unexpectedEntries].filter(entry =>
927
- isMissionTaskPath(entry.filePath, slug) && isDeletedStatus(entry.status)
928
- );
929
- if (deletedTaskEntries.length > 0) {
930
- throw new Error(
931
- `Draft safety harness found deletion of the mission backlog task: ${deletedTaskEntries.map(entry => entry.filePath).join(', ')}. ` +
932
- 'Restore the task file and re-run the draft.'
933
- );
934
- }
935
-
936
- const renamedTaskEntries = [...expectedEntries, ...unexpectedEntries].filter(entry =>
937
- entry.sourcePath &&
938
- isMissionTaskPath(entry.sourcePath, slug) &&
939
- entry.filePath !== entry.sourcePath
940
- );
941
- if (renamedTaskEntries.length > 0) {
942
- throw new Error(
943
- `Draft safety harness found rename/move of the mission backlog task: ${renamedTaskEntries.map(entry => `${entry.sourcePath} -> ${entry.filePath}`).join(', ')}. ` +
944
- 'Restore the task file path and re-run the draft.'
945
- );
946
- }
947
-
948
- const stagePaths = [...expectedEntries, ...unexpectedEntries].map(entry => entry.filePath);
949
- if (unexpectedEntries.length > 0) {
950
- logFn(fmt.status('WARN', 'Draft safety harness: capturing unexpected dirty files alongside mission artifacts:'));
951
- for (const entry of unexpectedEntries) {
952
- logFn(` ${entry.status} ${entry.filePath}`);
797
+ function enforceDraftCommitSafety({ slug, worktree, dirtyEntries = (0, git_js_1.getWorktreeStatus)(worktree), gitImpl = git_js_1.git, logFn = fmt.log.plain, errorFn = fmt.log.plainError }) {
798
+ if (dirtyEntries.length === 0) {
799
+ logFn(fmt.status('PASS', 'Draft safety harness: no uncommitted changes left behind.'));
800
+ return false;
953
801
  }
954
- }
955
-
956
- if (stagePaths.length > 0) {
957
- gitImpl(['-C', worktree, 'add', '--', ...stagePaths]);
958
- }
959
- const commitMessage = fallbackDraftCommitMessage(slug);
960
- const commitResult = gitImpl([
961
- '-C',
962
- worktree,
963
- 'commit',
964
- '-m',
965
- commitMessage,
966
- '-m',
967
- 'Safety harness: capture draft worktree changes left uncommitted by the agent.'
968
- ]);
969
-
970
- if (commitResult.status !== 0) {
971
- throw new Error('Draft safety harness could not create fallback commit.');
972
- }
973
-
974
- logFn(fmt.status('PASS', `Draft safety harness committed remaining changes with "${commitMessage}".`));
975
- return true;
802
+ logFn(fmt.status('WARN', 'Draft safety harness: draft agent left uncommitted changes. Creating fallback commit.'));
803
+ for (const entry of dirtyEntries) {
804
+ logFn(` ${entry}`);
805
+ }
806
+ const { conflictEntries, expectedEntries, unexpectedEntries } = classifyDraftEntries(dirtyEntries, slug, worktree);
807
+ // @ts-expect-error errorFn not in type
808
+ resolveMissionSpecificDraftConflicts({ slug, worktree, conflictEntries, gitImpl, logFn, errorFn });
809
+ const deletedTaskEntries = [...expectedEntries, ...unexpectedEntries].filter(entry => isMissionTaskPath(entry.filePath, slug) && isDeletedStatus(entry.status));
810
+ if (deletedTaskEntries.length > 0) {
811
+ throw new Error(`Draft safety harness found deletion of the mission backlog task: ${deletedTaskEntries.map(entry => entry.filePath).join(', ')}. ` +
812
+ 'Restore the task file and re-run the draft.');
813
+ }
814
+ const renamedTaskEntries = [...expectedEntries, ...unexpectedEntries].filter(entry => entry.sourcePath &&
815
+ isMissionTaskPath(entry.sourcePath, slug) &&
816
+ entry.filePath !== entry.sourcePath);
817
+ if (renamedTaskEntries.length > 0) {
818
+ throw new Error(`Draft safety harness found rename/move of the mission backlog task: ${renamedTaskEntries.map(entry => `${entry.sourcePath} -> ${entry.filePath}`).join(', ')}. ` +
819
+ 'Restore the task file path and re-run the draft.');
820
+ }
821
+ const stagePaths = [...expectedEntries, ...unexpectedEntries].map(entry => entry.filePath);
822
+ if (unexpectedEntries.length > 0) {
823
+ logFn(fmt.status('WARN', 'Draft safety harness: capturing unexpected dirty files alongside mission artifacts:'));
824
+ for (const entry of unexpectedEntries) {
825
+ logFn(` ${entry.status} ${entry.filePath}`);
826
+ }
827
+ }
828
+ if (stagePaths.length > 0) {
829
+ gitImpl(['-C', worktree, 'add', '--', ...stagePaths]);
830
+ }
831
+ const commitMessage = fallbackDraftCommitMessage(slug);
832
+ const commitResult = gitImpl([
833
+ '-C',
834
+ worktree,
835
+ 'commit',
836
+ '-m',
837
+ commitMessage,
838
+ '-m',
839
+ 'Safety harness: capture draft worktree changes left uncommitted by the agent.'
840
+ ]);
841
+ if (commitResult.status !== 0) {
842
+ throw new Error('Draft safety harness could not create fallback commit.');
843
+ }
844
+ logFn(fmt.status('PASS', `Draft safety harness committed remaining changes with "${commitMessage}".`));
845
+ return true;
976
846
  }
977
-
978
847
  // @ts-expect-error implicit any on mainRepo
979
848
  function ensureRepoExists(mainRepo, exitFn = process.exit, errorFn = fmt.log.fail) {
980
- if (!fs.existsSync(mainRepo)) {
981
- errorFn(`Main repository not found at ${mainRepo}. Please ensure it exists or set PRIMARY_WORKTREE.`);
982
- exitFn(1);
983
- return false;
984
- }
985
- return true;
849
+ if (!fs.existsSync(mainRepo)) {
850
+ errorFn(`Main repository not found at ${mainRepo}. Please ensure it exists or set PRIMARY_WORKTREE.`);
851
+ exitFn(1);
852
+ return false;
853
+ }
854
+ return true;
986
855
  }
987
-
988
856
  /**
989
857
  * Record a draft-stage stats row after the draft agent completes. Token/usage
990
858
  * columns come from the agent result's telemetry (currently Codex only); other
@@ -993,51 +861,30 @@ function ensureRepoExists(mainRepo, exitFn = process.exit, errorFn = fmt.log.fai
993
861
  */
994
862
  // @ts-expect-error implicit any on slug/rootDir/agentFamily/result
995
863
  function recordDraftStats({ slug, rootDir, agentFamily, result, log = fmt.log.plain }) {
996
- if (!result) {return;}
997
- let durationMinutes = 0;
998
- if (result.startedAt && result.endedAt) {
999
- durationMinutes = (Date.parse(result.endedAt) - Date.parse(result.startedAt)) / 60000;
1000
- }
1001
- try {
1002
- const { row } = stats.recordStageStats({
1003
- slug,
1004
- stage: 'draft',
1005
- rootDir,
1006
- implementer: agentFamily,
1007
- model: resolveAgentModel(agentFamily, rootDir),
1008
- telemetry: result.telemetry || null,
1009
- durationMinutes,
1010
- });
1011
- log(fmt.status('INFO', `Draft stats recorded: ${slug} stage=draft provider=${row.provider} model=${row.model} input_tokens=${row.input_tokens} tool_calls=${row.tool_calls}`));
1012
- } catch (err) {
1013
- // Best-effort: never escalate to the fatal `error` channel.
1014
- log(fmt.status('WARN', `Could not record draft stats for ${slug}: ${/** @type {any} */ (err).message}`));
1015
- }
864
+ if (!result) {
865
+ return;
866
+ }
867
+ let durationMinutes = 0;
868
+ if (result.startedAt && result.endedAt) {
869
+ durationMinutes = (Date.parse(result.endedAt) - Date.parse(result.startedAt)) / 60000;
870
+ }
871
+ try {
872
+ const { row } = stats.recordStageStats({
873
+ slug,
874
+ stage: 'draft',
875
+ rootDir,
876
+ implementer: agentFamily,
877
+ model: (0, product_config_js_1.resolveAgentModel)(agentFamily, rootDir),
878
+ telemetry: result.telemetry || null,
879
+ durationMinutes,
880
+ });
881
+ log(fmt.status('INFO', `Draft stats recorded: ${slug} stage=draft provider=${row.provider} model=${row.model} input_tokens=${row.input_tokens} tool_calls=${row.tool_calls}`));
882
+ }
883
+ catch (err) {
884
+ // Best-effort: never escalate to the fatal `error` channel.
885
+ log(fmt.status('WARN', `Could not record draft stats for ${slug}: ${ /** @type {any} */(err).message}`));
886
+ }
1016
887
  }
1017
-
1018
- module.exports = draft;
1019
- module.exports.draft = draft;
1020
- module.exports.runDraftCommand = runDraftCommand;
1021
- module.exports.recordDraftStats = recordDraftStats;
1022
- module.exports.buildDraftPrompt = buildDraftPrompt;
1023
- module.exports.recordDraftImplementer = recordDraftImplementer;
1024
- module.exports.enforceDraftCommitSafety = enforceDraftCommitSafety;
1025
- module.exports.fallbackDraftCommitMessage = fallbackDraftCommitMessage;
1026
- module.exports.bootstrapBacklogTask = bootstrapBacklogTask;
1027
- module.exports.ensureGraphifyWorkspace = ensureGraphifyWorkspace;
1028
- module.exports.ensureGraphifyIgnore = ensureGraphifyIgnore;
1029
- module.exports.ensureMissionBranch = ensureMissionBranch;
1030
- module.exports.ensureMissionBaseBranchRecorded = ensureMissionBaseBranchRecorded;
1031
- module.exports.ensureWorktree = ensureWorktree;
1032
- module.exports.ensureMissionFile = ensureMissionFile;
1033
- module.exports.ensureDraftRepoConfigCommitted = ensureDraftRepoConfigCommitted;
1034
- module.exports.ensureRepoExists = ensureRepoExists;
1035
- module.exports.classifyDraftEntries = classifyDraftEntries;
1036
- module.exports.isUnmergedStatus = isUnmergedStatus;
1037
- module.exports.isDeletedStatus = isDeletedStatus;
1038
- module.exports.isMissionTaskPath = isMissionTaskPath;
1039
- module.exports.isExpectedDraftPath = isExpectedDraftPath;
1040
- module.exports.validateDraftClassification = validateDraftClassification;
1041
- module.exports.normalizeDraftClassification = normalizeDraftClassification;
1042
- module.exports.buildRestartPrompt = buildRestartPrompt;
1043
- module.exports.restartDraftAgent = restartDraftAgent;
888
+ /** @type {typeof draft & {draft: typeof draft, runDraftCommand: typeof runDraftCommand, recordDraftStats: typeof recordDraftStats, buildDraftPrompt: typeof buildDraftPrompt, recordDraftImplementer: typeof recordDraftImplementer, enforceDraftCommitSafety: typeof enforceDraftCommitSafety, fallbackDraftCommitMessage: typeof fallbackDraftCommitMessage, bootstrapBacklogTask: typeof bootstrapBacklogTask, ensureGraphifyWorkspace: typeof ensureGraphifyWorkspace, ensureGraphifyIgnore: typeof ensureGraphifyIgnore, ensureMissionBranch: typeof ensureMissionBranch, ensureMissionBaseBranchRecorded: typeof ensureMissionBaseBranchRecorded, ensureWorktree: typeof ensureWorktree, ensureMissionFile: typeof ensureMissionFile, ensureDraftRepoConfigCommitted: typeof ensureDraftRepoConfigCommitted, ensureRepoExists: typeof ensureRepoExists, classifyDraftEntries: typeof classifyDraftEntries, isUnmergedStatus: typeof isUnmergedStatus, isDeletedStatus: typeof isDeletedStatus, isMissionTaskPath: typeof isMissionTaskPath, isExpectedDraftPath: typeof isExpectedDraftPath, validateDraftClassification: typeof validateDraftClassification, normalizeDraftClassification: typeof normalizeDraftClassification, buildRestartPrompt: typeof buildRestartPrompt, restartDraftAgent: typeof restartDraftAgent}} */
889
+ const _draftExport = Object.assign(draft, { draft, runDraftCommand, recordDraftStats, buildDraftPrompt, recordDraftImplementer, enforceDraftCommitSafety, fallbackDraftCommitMessage, bootstrapBacklogTask, ensureGraphifyWorkspace, ensureGraphifyIgnore, ensureMissionBranch, ensureMissionBaseBranchRecorded, ensureWorktree, ensureMissionFile, ensureDraftRepoConfigCommitted, ensureRepoExists, classifyDraftEntries, isUnmergedStatus, isDeletedStatus, isMissionTaskPath, isExpectedDraftPath, validateDraftClassification, normalizeDraftClassification, buildRestartPrompt, restartDraftAgent });
890
+ module.exports = _draftExport;