@aperant/framework 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +152 -0
- package/dist/cli/commands/ci-watch.mjs +49 -2
- package/dist/cli/commands/ci-watch.mjs.map +1 -1
- package/dist/cli/commands/commit.mjs +3 -3
- package/dist/cli/commands/commit.mjs.map +1 -1
- package/dist/cli/commands/event.mjs +16 -16
- package/dist/cli/commands/event.mjs.map +1 -1
- package/dist/cli/commands/merge-integrate.mjs +3 -3
- package/dist/cli/commands/merge-integrate.mjs.map +1 -1
- package/dist/cli/commands/produce.d.mts +9 -0
- package/dist/cli/commands/produce.d.mts.map +1 -0
- package/dist/cli/commands/produce.mjs +1345 -0
- package/dist/cli/commands/produce.mjs.map +1 -0
- package/dist/cli/commands/task.d.mts +16 -1
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +434 -266
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/coordination/auto-emit-artifact-ready.mjs +5 -5
- package/dist/cli/coordination/auto-emit-artifact-ready.mjs.map +1 -1
- package/dist/cli/coordination/event-schema.d.mts +5 -3
- package/dist/cli/coordination/event-schema.d.mts.map +1 -1
- package/dist/cli/coordination/event-schema.mjs +245 -21
- package/dist/cli/coordination/event-schema.mjs.map +1 -1
- package/dist/cli/coordination/store.d.mts +2 -2
- package/dist/cli/coordination/store.mjs +4 -4
- package/dist/cli/coordination/store.mjs.map +1 -1
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +2 -0
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/help.d.mts.map +1 -1
- package/dist/cli/help.mjs +30 -0
- package/dist/cli/help.mjs.map +1 -1
- package/dist/cli/install/legacy-paths.d.mts.map +1 -1
- package/dist/cli/install/legacy-paths.mjs +2 -0
- package/dist/cli/install/legacy-paths.mjs.map +1 -1
- package/dist/cli/produce/blind-probe.d.mts +85 -0
- package/dist/cli/produce/blind-probe.d.mts.map +1 -0
- package/dist/cli/produce/blind-probe.mjs +217 -0
- package/dist/cli/produce/blind-probe.mjs.map +1 -0
- package/dist/cli/produce/claim.d.mts +188 -0
- package/dist/cli/produce/claim.d.mts.map +1 -0
- package/dist/cli/produce/claim.mjs +518 -0
- package/dist/cli/produce/claim.mjs.map +1 -0
- package/dist/cli/produce/done-gate.d.mts +87 -0
- package/dist/cli/produce/done-gate.d.mts.map +1 -0
- package/dist/cli/produce/done-gate.mjs +200 -0
- package/dist/cli/produce/done-gate.mjs.map +1 -0
- package/dist/cli/produce/events.d.mts +77 -0
- package/dist/cli/produce/events.d.mts.map +1 -0
- package/dist/cli/produce/events.mjs +126 -0
- package/dist/cli/produce/events.mjs.map +1 -0
- package/dist/cli/produce/evidence-oracle.d.mts +63 -0
- package/dist/cli/produce/evidence-oracle.d.mts.map +1 -0
- package/dist/cli/produce/evidence-oracle.mjs +122 -0
- package/dist/cli/produce/evidence-oracle.mjs.map +1 -0
- package/dist/cli/produce/ledger.d.mts +140 -0
- package/dist/cli/produce/ledger.d.mts.map +1 -0
- package/dist/cli/produce/ledger.mjs +272 -0
- package/dist/cli/produce/ledger.mjs.map +1 -0
- package/dist/cli/produce/probe-family.d.mts +53 -0
- package/dist/cli/produce/probe-family.d.mts.map +1 -0
- package/dist/cli/produce/probe-family.mjs +160 -0
- package/dist/cli/produce/probe-family.mjs.map +1 -0
- package/dist/cli/produce/projection.d.mts +55 -0
- package/dist/cli/produce/projection.d.mts.map +1 -0
- package/dist/cli/produce/projection.mjs +97 -0
- package/dist/cli/produce/projection.mjs.map +1 -0
- package/dist/cli/produce/run-id.d.mts +42 -0
- package/dist/cli/produce/run-id.d.mts.map +1 -0
- package/dist/cli/produce/run-id.mjs +79 -0
- package/dist/cli/produce/run-id.mjs.map +1 -0
- package/dist/cli/produce/saga.d.mts +180 -0
- package/dist/cli/produce/saga.d.mts.map +1 -0
- package/dist/cli/produce/saga.mjs +290 -0
- package/dist/cli/produce/saga.mjs.map +1 -0
- package/dist/cli/produce/scheduler.d.mts +165 -0
- package/dist/cli/produce/scheduler.d.mts.map +1 -0
- package/dist/cli/produce/scheduler.mjs +399 -0
- package/dist/cli/produce/scheduler.mjs.map +1 -0
- package/dist/cli/produce/setpoint.d.mts +52 -0
- package/dist/cli/produce/setpoint.d.mts.map +1 -0
- package/dist/cli/produce/setpoint.mjs +113 -0
- package/dist/cli/produce/setpoint.mjs.map +1 -0
- package/dist/cli/produce/verification-ttl.d.mts +75 -0
- package/dist/cli/produce/verification-ttl.d.mts.map +1 -0
- package/dist/cli/produce/verification-ttl.mjs +169 -0
- package/dist/cli/produce/verification-ttl.mjs.map +1 -0
- package/dist/cli/roadmap/{conductor-view.d.mts → showrunner-view.d.mts} +3 -3
- package/dist/cli/roadmap/showrunner-view.d.mts.map +1 -0
- package/dist/cli/roadmap/{conductor-view.mjs → showrunner-view.mjs} +7 -7
- package/dist/cli/roadmap/showrunner-view.mjs.map +1 -0
- package/dist/plugin/.claude-plugin/plugin.json +2 -1
- package/dist/plugin/skills/apt/SKILL.md +112 -38
- package/dist/plugin/skills/apt-debug/SKILL.md +14 -24
- package/dist/plugin/skills/apt-fan-out/SKILL.md +4 -4
- package/dist/plugin/skills/apt-handoff/SKILL.md +1 -1
- package/dist/plugin/skills/apt-plan/SKILL.md +5 -5
- package/dist/plugin/skills/apt-plan/adapters/{conductor.md → showrunner.md} +16 -16
- package/dist/plugin/skills/apt-produce/SKILL.md +606 -0
- package/dist/plugin/skills/apt-quick/SKILL.md +14 -22
- package/dist/plugin/skills/apt-run/SKILL.md +126 -3
- package/dist/plugin/skills/apt-ship/SKILL.md +2 -0
- package/dist/plugin/skills/apt-spar/SKILL.md +5 -3
- package/dist/plugin/skills/apt-watch-ci/SKILL.md +4 -4
- package/package.json +4 -4
- package/prompts/{conductor-framework-context.md → showrunner-framework-context.md} +1 -1
- package/prompts/{conductor-status-check.md → showrunner-status-check.md} +1 -1
- package/prompts/{conductor-sub-agent.md → showrunner-sub-agent.md} +6 -6
- package/prompts/{conductor-system.md → showrunner-system.md} +8 -8
- package/skills/apt/SKILL.md +112 -38
- package/skills/apt-debug/SKILL.md +14 -24
- package/skills/apt-fan-out/SKILL.md +4 -4
- package/skills/apt-handoff/SKILL.md +1 -1
- package/skills/apt-plan/SKILL.md +5 -5
- package/skills/apt-plan/adapters/{conductor.md → showrunner.md} +16 -16
- package/skills/apt-produce/SKILL.md +606 -0
- package/skills/apt-quick/SKILL.md +14 -22
- package/skills/apt-run/SKILL.md +126 -3
- package/skills/apt-ship/SKILL.md +2 -0
- package/skills/apt-spar/SKILL.md +5 -3
- package/skills/apt-watch-ci/SKILL.md +4 -4
- package/src/cli/commands/ci-watch.mjs +51 -2
- package/src/cli/commands/commit.mjs +3 -3
- package/src/cli/commands/event.mjs +16 -16
- package/src/cli/commands/merge-integrate.mjs +3 -3
- package/src/cli/commands/produce.mjs +1466 -0
- package/src/cli/commands/task.mjs +482 -285
- package/src/cli/coordination/auto-emit-artifact-ready.mjs +5 -5
- package/src/cli/coordination/event-schema.d.ts +4 -2
- package/src/cli/coordination/event-schema.mjs +276 -21
- package/src/cli/coordination/store.mjs +4 -4
- package/src/cli/dispatch.mjs +2 -0
- package/src/cli/help.mjs +30 -0
- package/src/cli/install/legacy-paths.mjs +2 -0
- package/src/cli/produce/blind-probe.mjs +245 -0
- package/src/cli/produce/claim.mjs +543 -0
- package/src/cli/produce/done-gate.mjs +238 -0
- package/src/cli/produce/events.mjs +131 -0
- package/src/cli/produce/evidence-oracle.mjs +133 -0
- package/src/cli/produce/ledger.mjs +284 -0
- package/src/cli/produce/probe-family.mjs +168 -0
- package/src/cli/produce/projection.mjs +105 -0
- package/src/cli/produce/run-id.mjs +84 -0
- package/src/cli/produce/saga.mjs +303 -0
- package/src/cli/produce/scheduler.mjs +423 -0
- package/src/cli/produce/setpoint.mjs +122 -0
- package/src/cli/produce/verification-ttl.mjs +191 -0
- package/src/cli/roadmap/showrunner-view.d.ts +10 -0
- package/src/cli/roadmap/{conductor-view.mjs → showrunner-view.mjs} +6 -6
- package/dist/cli/roadmap/conductor-view.d.mts.map +0 -1
- package/dist/cli/roadmap/conductor-view.mjs.map +0 -1
- package/src/cli/roadmap/conductor-view.d.ts +0 -10
|
@@ -38,6 +38,277 @@ import { atomicWriteJson } from '../util/io.mjs';
|
|
|
38
38
|
import { err, exitWith, ok } from '../util/result.mjs';
|
|
39
39
|
import { resolveMainRepoRoot } from '../util/state-paths.mjs';
|
|
40
40
|
import { cmdCiWatch } from './ci-watch.mjs';
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the effective autonomy for a `task create` / `task ensure`
|
|
43
|
+
* invocation (ID-04). Layered fallback mirroring
|
|
44
|
+
* worktree.mjs:resolveCreateAutonomy — a `--autonomy <n>` flag wins; else the
|
|
45
|
+
* project config's `autonomy.default`; else 1 (the conservative
|
|
46
|
+
* confirm-by-default level). A non-numeric / out-of-range flag or config value
|
|
47
|
+
* is ignored. This kills the hardcoded-1 footgun: a caller that omits
|
|
48
|
+
* --autonomy now inherits config.autonomy.default instead of silently
|
|
49
|
+
* forwarding 1 to the worktree hook (which would spuriously trip the
|
|
50
|
+
* non-default-base deferral).
|
|
51
|
+
* @param {Map<string, string>} flags
|
|
52
|
+
* @param {Record<string, unknown>} config already-loaded merged project config
|
|
53
|
+
* @returns {number}
|
|
54
|
+
*/
|
|
55
|
+
export function resolveEffectiveAutonomy(flags, config) {
|
|
56
|
+
const raw = flags.get('autonomy');
|
|
57
|
+
if (raw !== undefined && raw !== null && raw !== '') {
|
|
58
|
+
const n = Number.parseInt(String(raw), 10);
|
|
59
|
+
if (Number.isFinite(n) && n >= 0 && n <= 3)
|
|
60
|
+
return n;
|
|
61
|
+
}
|
|
62
|
+
const def = config?.autonomy?.default;
|
|
63
|
+
if (typeof def === 'number' && Number.isFinite(def) && def >= 0 && def <= 3)
|
|
64
|
+
return def;
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Shared create-new allocation routine for `task create` and `task ensure`
|
|
69
|
+
* (ID-02). Owns the explicit-id safety checks, the withFileLock critical
|
|
70
|
+
* section (collision disambiguation + taskDir mkdir + task-record write +
|
|
71
|
+
* worktree hook), and the post-lock side effects (index regen + team status +
|
|
72
|
+
* task.created event + re-read of the created record).
|
|
73
|
+
*
|
|
74
|
+
* When `ensureKey` is set (the idempotent `task ensure --intent create-new`
|
|
75
|
+
* path, ID-03), the lock first scans active_tasks for a record already
|
|
76
|
+
* carrying that ensure_key (or the deterministic id with a matching
|
|
77
|
+
* track+scope) and, on a hit, RETURNS the existing record + worktree WITHOUT
|
|
78
|
+
* creating a second task/worktree (reused:true) — short-circuiting the
|
|
79
|
+
* collision disambiguator. On a miss it creates and stamps ensure_key.
|
|
80
|
+
*
|
|
81
|
+
* @returns {{ error?: any, taskId?: string, taskDir?: string, createdTask?: any, reused?: boolean }}
|
|
82
|
+
*/
|
|
83
|
+
function internalCreateTask(ctx) {
|
|
84
|
+
const { stateRoot, statePath, targetDir, store, config, description, track, autonomy, executionMode, scope, milestoneId, phaseIdForRoadmap, roadmapRefUrl, dependsOn, explicitIdRaw, assignedTo, ensureKey, } = ctx;
|
|
85
|
+
const SAFE_ID_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
86
|
+
if (explicitIdRaw && !SAFE_ID_PATTERN.test(explicitIdRaw)) {
|
|
87
|
+
return {
|
|
88
|
+
error: err('[apt-tools] Invalid task id: must match /^[A-Za-z0-9._-]+$/', 1, {
|
|
89
|
+
command: 'task-create',
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// Secondary guard: the resolved task directory must stay inside the tasks
|
|
94
|
+
// base (SAFE_ID_PATTERN allows '.'/'..').
|
|
95
|
+
if (explicitIdRaw) {
|
|
96
|
+
const tasksBase = resolve(join(stateRoot, '.aperant', 'tasks'));
|
|
97
|
+
const candidateDir = resolve(getTaskDir(stateRoot, explicitIdRaw));
|
|
98
|
+
if (!candidateDir.startsWith(`${tasksBase}/`) && candidateDir !== tasksBase) {
|
|
99
|
+
return {
|
|
100
|
+
error: err('[apt-tools] Invalid task id: path escapes the tasks directory', 1, {
|
|
101
|
+
command: 'task-create',
|
|
102
|
+
}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
let taskId;
|
|
107
|
+
let taskDir;
|
|
108
|
+
let collisionOverflowErr = null;
|
|
109
|
+
let reused = false;
|
|
110
|
+
withFileLock(statePath, () => {
|
|
111
|
+
const raw = readFileSync(statePath, 'utf-8').trim();
|
|
112
|
+
const state = raw ? JSON.parse(raw) : {};
|
|
113
|
+
if (state.active_task !== undefined && state.active_tasks === undefined) {
|
|
114
|
+
state.active_tasks = {};
|
|
115
|
+
if (state.active_task) {
|
|
116
|
+
const oldId = state.active_task.id || 'legacy-task';
|
|
117
|
+
state.active_tasks[oldId] = state.active_task;
|
|
118
|
+
}
|
|
119
|
+
delete state.active_task;
|
|
120
|
+
}
|
|
121
|
+
if (!state.active_tasks)
|
|
122
|
+
state.active_tasks = {};
|
|
123
|
+
// ID-03 — idempotent reuse for `task ensure --intent create-new`. A
|
|
124
|
+
// record already carrying this ensure_key (or the deterministic id with
|
|
125
|
+
// a matching track+scope) short-circuits the collision disambiguator and
|
|
126
|
+
// returns the existing task + worktree.
|
|
127
|
+
if (ensureKey) {
|
|
128
|
+
const candidateId = explicitIdRaw || generateTaskId(description, config);
|
|
129
|
+
const existing = Object.values(state.active_tasks).find((t) => t &&
|
|
130
|
+
(t.ensure_key === ensureKey ||
|
|
131
|
+
(t.id === candidateId &&
|
|
132
|
+
(t.track || 'STANDARD') === track &&
|
|
133
|
+
(t.scope || 'project') === scope)));
|
|
134
|
+
if (existing) {
|
|
135
|
+
taskId = existing.id;
|
|
136
|
+
taskDir = getTaskDir(stateRoot, taskId);
|
|
137
|
+
reused = true;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Resolve taskId against the locked-in state snapshot.
|
|
142
|
+
if (explicitIdRaw) {
|
|
143
|
+
taskId = explicitIdRaw;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
const base = generateTaskId(description, config);
|
|
147
|
+
const activeIds = Object.keys(state.active_tasks);
|
|
148
|
+
const completedIds = Array.isArray(state.completed_tasks)
|
|
149
|
+
? state.completed_tasks.map((row) => row?.id).filter(Boolean)
|
|
150
|
+
: [];
|
|
151
|
+
const known = new Set([...activeIds, ...completedIds]);
|
|
152
|
+
const isTaken = (candidate) => known.has(candidate) || existsSync(getTaskDir(stateRoot, candidate));
|
|
153
|
+
let candidate = base;
|
|
154
|
+
let counter = 2;
|
|
155
|
+
while (isTaken(candidate)) {
|
|
156
|
+
if (counter > 100) {
|
|
157
|
+
collisionOverflowErr = err(`task create: collision counter exceeded 100 for base id "${base}" — manual --id required`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
candidate = `${base}-${counter}`;
|
|
161
|
+
counter++;
|
|
162
|
+
}
|
|
163
|
+
taskId = candidate;
|
|
164
|
+
}
|
|
165
|
+
taskDir = getTaskDir(stateRoot, taskId);
|
|
166
|
+
mkdirSync(taskDir, { recursive: true });
|
|
167
|
+
for (const subdir of ['events', 'locks']) {
|
|
168
|
+
mkdirSync(join(taskDir, subdir), { recursive: true });
|
|
169
|
+
}
|
|
170
|
+
const outputs = resolveDefaultOutputs({
|
|
171
|
+
id: taskId,
|
|
172
|
+
scope,
|
|
173
|
+
milestone_id: milestoneId,
|
|
174
|
+
phase_id: phaseIdForRoadmap,
|
|
175
|
+
});
|
|
176
|
+
state.active_tasks[taskId] = {
|
|
177
|
+
id: taskId,
|
|
178
|
+
description,
|
|
179
|
+
track,
|
|
180
|
+
autonomy,
|
|
181
|
+
...(executionMode ? { execution_mode: executionMode } : {}),
|
|
182
|
+
// ID-03 — stamp the ensure_key so a re-run can short-circuit.
|
|
183
|
+
...(ensureKey ? { ensure_key: ensureKey } : {}),
|
|
184
|
+
scope,
|
|
185
|
+
milestone_id: milestoneId,
|
|
186
|
+
phase_id: phaseIdForRoadmap,
|
|
187
|
+
roadmap_ref_url: roadmapRefUrl,
|
|
188
|
+
depends_on: dependsOn,
|
|
189
|
+
outputs,
|
|
190
|
+
assigned_to: assignedTo ?? null,
|
|
191
|
+
lifecycle_phase: 'planning',
|
|
192
|
+
history: [],
|
|
193
|
+
created_at: new Date().toISOString(),
|
|
194
|
+
progress: { subtasks_total: 0, subtasks_completed: 0, current_subtask: null },
|
|
195
|
+
};
|
|
196
|
+
// ── Task-level worktree hook ──────────────────────────────
|
|
197
|
+
const isolation = getTaskIsolationConfig(targetDir);
|
|
198
|
+
const host = detectHost();
|
|
199
|
+
if (isolation.worktree_per_task === true &&
|
|
200
|
+
host.capabilities.worktrees === true &&
|
|
201
|
+
!detectWorktree(targetDir).is_worktree) {
|
|
202
|
+
try {
|
|
203
|
+
const wtArgs = ['--task', taskId, '--scope', scope];
|
|
204
|
+
if (isolation.push_on_create)
|
|
205
|
+
wtArgs.push('--push');
|
|
206
|
+
if (isolation.from_branch && isolation.from_branch !== 'current') {
|
|
207
|
+
wtArgs.push('--from', isolation.from_branch);
|
|
208
|
+
}
|
|
209
|
+
// ID-04 — forward the EFFECTIVE autonomy (already resolved from
|
|
210
|
+
// config.autonomy.default) so a non-default from_branch:current base
|
|
211
|
+
// does not spuriously trip the worktree.mjs autonomy<=1 deferral.
|
|
212
|
+
wtArgs.push('--autonomy', String(autonomy));
|
|
213
|
+
const aptToolsBin = resolveAptToolsBin(targetDir);
|
|
214
|
+
const createRes = spawnSync(process.execPath, [aptToolsBin, 'worktree', 'create', targetDir, ...wtArgs], { encoding: 'utf-8' });
|
|
215
|
+
if (createRes.status === 0 && createRes.stdout) {
|
|
216
|
+
const wtOut = JSON.parse(createRes.stdout);
|
|
217
|
+
if (wtOut.status === 'requires_input' || wtOut.reason === 'non_default_base') {
|
|
218
|
+
state.active_tasks[taskId].worktree_warning =
|
|
219
|
+
`worktree deferred: non-default base — re-run worktree create with explicit --from (base would be '${wtOut.base_branch}', default '${wtOut.default_branch}')`;
|
|
220
|
+
state.last_activity = new Date().toISOString();
|
|
221
|
+
atomicWriteJson(statePath, state);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
state.active_tasks[taskId].worktree_path = wtOut.worktree_path;
|
|
225
|
+
state.active_tasks[taskId].branch = wtOut.branch;
|
|
226
|
+
state.active_tasks[taskId].base_branch = wtOut.base_branch;
|
|
227
|
+
state.active_tasks[taskId].remote_tracking = !!wtOut.remote_tracking;
|
|
228
|
+
if (wtOut.scope) {
|
|
229
|
+
state.active_tasks[taskId].worktree_scope = wtOut.scope;
|
|
230
|
+
}
|
|
231
|
+
if (wtOut.push_warning) {
|
|
232
|
+
state.active_tasks[taskId].push_warning = wtOut.push_warning;
|
|
233
|
+
}
|
|
234
|
+
if (wtOut.install && typeof wtOut.install === 'object') {
|
|
235
|
+
const inst = wtOut.install;
|
|
236
|
+
if (inst.install_status)
|
|
237
|
+
state.active_tasks[taskId].install_status = inst.install_status;
|
|
238
|
+
if (inst.install_log_path)
|
|
239
|
+
state.active_tasks[taskId].install_log_path = inst.install_log_path;
|
|
240
|
+
if (inst.install_pid !== undefined && inst.install_pid !== null)
|
|
241
|
+
state.active_tasks[taskId].install_pid = inst.install_pid;
|
|
242
|
+
if (Array.isArray(inst.install_ecosystems))
|
|
243
|
+
state.active_tasks[taskId].install_ecosystems = inst.install_ecosystems;
|
|
244
|
+
if (inst.install_started_at)
|
|
245
|
+
state.active_tasks[taskId].install_started_at = inst.install_started_at;
|
|
246
|
+
if (inst.install_finished_at)
|
|
247
|
+
state.active_tasks[taskId].install_finished_at = inst.install_finished_at;
|
|
248
|
+
if (inst.install_warning)
|
|
249
|
+
state.active_tasks[taskId].install_warning = inst.install_warning;
|
|
250
|
+
if (inst.install_per_ecosystem && typeof inst.install_per_ecosystem === 'object')
|
|
251
|
+
state.active_tasks[taskId].install_per_ecosystem = inst.install_per_ecosystem;
|
|
252
|
+
if (Array.isArray(inst.install_rejections))
|
|
253
|
+
state.active_tasks[taskId].install_rejections = inst.install_rejections;
|
|
254
|
+
}
|
|
255
|
+
if (wtOut.install_warning && !state.active_tasks[taskId].install_warning) {
|
|
256
|
+
state.active_tasks[taskId].install_warning = wtOut.install_warning;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
state.active_tasks[taskId].worktree_warning =
|
|
261
|
+
'worktree create failed: ' +
|
|
262
|
+
(createRes.stderr || createRes.stdout || 'unknown')
|
|
263
|
+
.toString()
|
|
264
|
+
.trim()
|
|
265
|
+
.split('\n')
|
|
266
|
+
.pop();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
state.active_tasks[taskId].worktree_warning = `worktree hook error: ${e.message}`;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
state.last_activity = new Date().toISOString();
|
|
274
|
+
atomicWriteJson(statePath, state);
|
|
275
|
+
}, { projectRoot: stateRoot });
|
|
276
|
+
if (collisionOverflowErr)
|
|
277
|
+
return { error: collisionOverflowErr };
|
|
278
|
+
// Post-lock side-effects only run for a genuinely NEW task — a reused
|
|
279
|
+
// ensure hit must not re-emit task.created or re-register team status.
|
|
280
|
+
if (!reused) {
|
|
281
|
+
regenerateTaskIndex(stateRoot);
|
|
282
|
+
updateTeamMember(targetDir);
|
|
283
|
+
store.upsertTeamStatus(taskId, {
|
|
284
|
+
description,
|
|
285
|
+
lifecycle_phase: 'planning',
|
|
286
|
+
scope,
|
|
287
|
+
});
|
|
288
|
+
store.appendEvent({
|
|
289
|
+
op: 'task.created',
|
|
290
|
+
task: taskId,
|
|
291
|
+
data: {
|
|
292
|
+
description,
|
|
293
|
+
track,
|
|
294
|
+
scope,
|
|
295
|
+
milestone_id: milestoneId,
|
|
296
|
+
phase_id: phaseIdForRoadmap,
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
let createdTask = null;
|
|
301
|
+
if (existsSync(statePath)) {
|
|
302
|
+
try {
|
|
303
|
+
const s = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
304
|
+
createdTask = s.active_tasks?.[taskId] || null;
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
/* best-effort */
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return { taskId, taskDir, createdTask, reused };
|
|
311
|
+
}
|
|
41
312
|
export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
42
313
|
if (!subcommand)
|
|
43
314
|
return err('Usage: apt-tools task <create|list|get|close|update|move|complete-subtask|narration-drain|prune> <project-dir> [flags]');
|
|
@@ -63,7 +334,6 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
63
334
|
if (!description)
|
|
64
335
|
return err('Required: --description "task description"');
|
|
65
336
|
const track = flags.get('track') || 'STANDARD';
|
|
66
|
-
const autonomy = parseInt(flags.get('autonomy') ?? '1', 10);
|
|
67
337
|
// C21 — first-class execution_mode field. Accepts the same values as
|
|
68
338
|
// the router's `--execution-mode` flag (validation lives in
|
|
69
339
|
// task/execution-mode.mjs — single source of truth). Unset is allowed
|
|
@@ -134,273 +404,33 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
134
404
|
catch {
|
|
135
405
|
config = {};
|
|
136
406
|
}
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
// rare so the loop should almost always terminate on the first try.
|
|
143
|
-
//
|
|
144
|
-
// BUG-2 fix: the candidate-selection scan, taskDir mkdir, and
|
|
145
|
-
// state.active_tasks write MUST all happen inside a single
|
|
146
|
-
// withFileLock critical section. Two concurrent task create calls
|
|
147
|
-
// in the multi-agent environment can otherwise both compute the
|
|
148
|
-
// same candidate against the same pre-lock snapshot of state and
|
|
149
|
-
// race into a duplicate-id write that silently overwrites the
|
|
150
|
-
// first task's record.
|
|
151
|
-
const SAFE_ID_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
407
|
+
// ID-04 — resolve effective autonomy from the loaded config AFTER it is
|
|
408
|
+
// available: --autonomy flag → config.autonomy.default → 1. Replaces the
|
|
409
|
+
// former hardcoded `?? '1'` so a caller omitting --autonomy inherits the
|
|
410
|
+
// project default instead of silently forwarding 1 to the worktree hook.
|
|
411
|
+
const autonomy = resolveEffectiveAutonomy(flags, config);
|
|
152
412
|
const explicitIdRaw = flags.get('id');
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// tasks base. The SAFE_ID_PATTERN above blocks `/` and most traversal
|
|
160
|
-
// but allows `.` and `..` as valid characters. A lone `..` id would
|
|
161
|
-
// escape via getTaskDir's join: `tasks/..` resolves to `.aperant/`.
|
|
162
|
-
if (explicitIdRaw) {
|
|
163
|
-
const tasksBase = resolve(join(stateRoot, '.aperant', 'tasks'));
|
|
164
|
-
const candidateDir = resolve(getTaskDir(stateRoot, explicitIdRaw));
|
|
165
|
-
if (!candidateDir.startsWith(`${tasksBase}/`) && candidateDir !== tasksBase) {
|
|
166
|
-
return err('[apt-tools] Invalid task id: path escapes the tasks directory', 1, {
|
|
167
|
-
command: 'task-create',
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
let taskId;
|
|
172
|
-
let taskDir;
|
|
173
|
-
let collisionOverflowErr = null;
|
|
174
|
-
withFileLock(statePath, () => {
|
|
175
|
-
// Read state under the lock so the collision scan, taskDir
|
|
176
|
-
// mkdir, and state.active_tasks write all observe the same
|
|
177
|
-
// snapshot. withFileLock seeds an empty file if statePath
|
|
178
|
-
// doesn't exist, so the JSON.parse fallback handles the
|
|
179
|
-
// first-ever task create cleanly.
|
|
180
|
-
const raw = readFileSync(statePath, 'utf-8').trim();
|
|
181
|
-
const state = raw ? JSON.parse(raw) : {};
|
|
182
|
-
// Migrate: if old format (active_task singular), convert
|
|
183
|
-
if (state.active_task !== undefined && state.active_tasks === undefined) {
|
|
184
|
-
state.active_tasks = {};
|
|
185
|
-
if (state.active_task) {
|
|
186
|
-
const oldId = state.active_task.id || 'legacy-task';
|
|
187
|
-
state.active_tasks[oldId] = state.active_task;
|
|
188
|
-
}
|
|
189
|
-
delete state.active_task;
|
|
190
|
-
}
|
|
191
|
-
if (!state.active_tasks)
|
|
192
|
-
state.active_tasks = {};
|
|
193
|
-
// Resolve taskId against the locked-in state snapshot.
|
|
194
|
-
if (explicitIdRaw) {
|
|
195
|
-
taskId = explicitIdRaw;
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
const base = generateTaskId(description, config);
|
|
199
|
-
const activeIds = Object.keys(state.active_tasks);
|
|
200
|
-
const completedIds = Array.isArray(state.completed_tasks)
|
|
201
|
-
? state.completed_tasks.map((row) => row?.id).filter(Boolean)
|
|
202
|
-
: [];
|
|
203
|
-
const known = new Set([...activeIds, ...completedIds]);
|
|
204
|
-
const isTaken = (candidate) => known.has(candidate) || existsSync(getTaskDir(stateRoot, candidate));
|
|
205
|
-
let candidate = base;
|
|
206
|
-
let counter = 2;
|
|
207
|
-
while (isTaken(candidate)) {
|
|
208
|
-
if (counter > 100) {
|
|
209
|
-
collisionOverflowErr = err(`task create: collision counter exceeded 100 for base id "${base}" — manual --id required`);
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
candidate = `${base}-${counter}`;
|
|
213
|
-
counter++;
|
|
214
|
-
}
|
|
215
|
-
taskId = candidate;
|
|
216
|
-
}
|
|
217
|
-
// Create task directory under the lock so a concurrent
|
|
218
|
-
// run can't observe (and then re-pick) a half-created id.
|
|
219
|
-
taskDir = getTaskDir(stateRoot, taskId);
|
|
220
|
-
mkdirSync(taskDir, { recursive: true });
|
|
221
|
-
for (const subdir of ['events', 'locks']) {
|
|
222
|
-
mkdirSync(join(taskDir, subdir), { recursive: true });
|
|
223
|
-
}
|
|
224
|
-
// Task-record lifecycle phase (NOT roadmap phase, NOT workflow phase).
|
|
225
|
-
// C28 D-02/D-03/D-08: scope + linkage + dependency-graph fields.
|
|
226
|
-
// R11 — resolve the outputs allowlist now so it is pinned at
|
|
227
|
-
// task creation time. If the planner edits the task record
|
|
228
|
-
// later (e.g. narrows the allowlist), it overwrites this.
|
|
229
|
-
const outputs = resolveDefaultOutputs({
|
|
230
|
-
id: taskId,
|
|
231
|
-
scope,
|
|
232
|
-
milestone_id: milestoneId,
|
|
233
|
-
phase_id: phaseIdForRoadmap,
|
|
234
|
-
});
|
|
235
|
-
state.active_tasks[taskId] = {
|
|
236
|
-
id: taskId,
|
|
237
|
-
description,
|
|
238
|
-
track,
|
|
239
|
-
autonomy,
|
|
240
|
-
// C21 — execution_mode is omitted entirely when unset so legacy
|
|
241
|
-
// readers (apt-plan SKILL.md 9a) continue to see `undefined`
|
|
242
|
-
// rather than `null` and fall through to the autonomy heuristic.
|
|
243
|
-
...(executionMode ? { execution_mode: executionMode } : {}),
|
|
244
|
-
scope,
|
|
245
|
-
milestone_id: milestoneId,
|
|
246
|
-
phase_id: phaseIdForRoadmap,
|
|
247
|
-
roadmap_ref_url: roadmapRefUrl,
|
|
248
|
-
depends_on: dependsOn,
|
|
249
|
-
outputs,
|
|
250
|
-
// FRAMEWORK-BUG-033 defect 2 — durable task→agent ownership.
|
|
251
|
-
// Defaults to null at create time; fan-out workers (or the
|
|
252
|
-
// `task update --assigned-to <agent-id>` path) populate it
|
|
253
|
-
// when they claim the task. Persists across short-lived CLI
|
|
254
|
-
// invocations unlike team-status/{agentId}.json (pid-keyed).
|
|
255
|
-
assigned_to: flags.has('assigned-to') ? flags.get('assigned-to') : null,
|
|
256
|
-
lifecycle_phase: 'planning',
|
|
257
|
-
history: [],
|
|
258
|
-
created_at: new Date().toISOString(),
|
|
259
|
-
progress: { subtasks_total: 0, subtasks_completed: 0, current_subtask: null },
|
|
260
|
-
};
|
|
261
|
-
// ── Task-level worktree hook ──────────────────────────────
|
|
262
|
-
// When task_isolation.worktree_per_task is enabled AND we're
|
|
263
|
-
// not already inside a worktree AND the host supports them,
|
|
264
|
-
// fork a branch + worktree and record its location on the
|
|
265
|
-
// task record. Any failure is downgraded to a warning so
|
|
266
|
-
// task-create never aborts.
|
|
267
|
-
const isolation = getTaskIsolationConfig(targetDir);
|
|
268
|
-
const host = detectHost();
|
|
269
|
-
if (isolation.worktree_per_task === true &&
|
|
270
|
-
host.capabilities.worktrees === true &&
|
|
271
|
-
!detectWorktree(targetDir).is_worktree) {
|
|
272
|
-
try {
|
|
273
|
-
const wtArgs = ['--task', taskId, '--scope', scope];
|
|
274
|
-
if (isolation.push_on_create)
|
|
275
|
-
wtArgs.push('--push');
|
|
276
|
-
if (isolation.from_branch && isolation.from_branch !== 'current') {
|
|
277
|
-
wtArgs.push('--from', isolation.from_branch);
|
|
278
|
-
}
|
|
279
|
-
// state-fork-fix ID-09b: forward the task's effective autonomy
|
|
280
|
-
// so `worktree create` can pick the 0-1 confirm vs. 2-3 log
|
|
281
|
-
// branch for a non-default `from_branch: current` base.
|
|
282
|
-
wtArgs.push('--autonomy', String(autonomy));
|
|
283
|
-
// Invoke inline so we capture the structured payload
|
|
284
|
-
// without shelling back out. We cheat by calling the
|
|
285
|
-
// worktree creator directly as a subprocess to keep
|
|
286
|
-
// its output discipline intact, then parse its JSON.
|
|
287
|
-
//
|
|
288
|
-
// D-03 — single source of truth for the apt-tools binary
|
|
289
|
-
// path (replaces the legacy process.argv[1] heuristic).
|
|
290
|
-
const aptToolsBin = resolveAptToolsBin(targetDir);
|
|
291
|
-
const createRes = spawnSync(process.execPath, [aptToolsBin, 'worktree', 'create', targetDir, ...wtArgs], { encoding: 'utf-8' });
|
|
292
|
-
if (createRes.status === 0 && createRes.stdout) {
|
|
293
|
-
const wtOut = JSON.parse(createRes.stdout);
|
|
294
|
-
// state-fork-fix ID-09b: a requires_input non-default-base
|
|
295
|
-
// return (autonomy <= 1) is NOT a created worktree — degrade
|
|
296
|
-
// it to a worktree_warning so task-create still SUCCEEDS
|
|
297
|
-
// (task created, worktree deferred) instead of recording a
|
|
298
|
-
// bogus undefined worktree_path. The operator re-runs
|
|
299
|
-
// `worktree create` with an explicit --from.
|
|
300
|
-
if (wtOut.status === 'requires_input' || wtOut.reason === 'non_default_base') {
|
|
301
|
-
state.active_tasks[taskId].worktree_warning =
|
|
302
|
-
`worktree deferred: non-default base — re-run worktree create with explicit --from (base would be '${wtOut.base_branch}', default '${wtOut.default_branch}')`;
|
|
303
|
-
state.last_activity = new Date().toISOString();
|
|
304
|
-
atomicWriteJson(statePath, state);
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
state.active_tasks[taskId].worktree_path = wtOut.worktree_path;
|
|
308
|
-
state.active_tasks[taskId].branch = wtOut.branch;
|
|
309
|
-
state.active_tasks[taskId].base_branch = wtOut.base_branch;
|
|
310
|
-
state.active_tasks[taskId].remote_tracking = !!wtOut.remote_tracking;
|
|
311
|
-
if (wtOut.scope) {
|
|
312
|
-
state.active_tasks[taskId].worktree_scope = wtOut.scope;
|
|
313
|
-
}
|
|
314
|
-
if (wtOut.push_warning) {
|
|
315
|
-
state.active_tasks[taskId].push_warning = wtOut.push_warning;
|
|
316
|
-
}
|
|
317
|
-
// C54 — merge install_* fields surfaced by the
|
|
318
|
-
// worktree-install hook in worktree.mjs. These fields are
|
|
319
|
-
// absent on pre-C54 default-config paths, so readers that
|
|
320
|
-
// don't know about install_* continue to work unchanged.
|
|
321
|
-
if (wtOut.install && typeof wtOut.install === 'object') {
|
|
322
|
-
const inst = wtOut.install;
|
|
323
|
-
if (inst.install_status)
|
|
324
|
-
state.active_tasks[taskId].install_status = inst.install_status;
|
|
325
|
-
if (inst.install_log_path)
|
|
326
|
-
state.active_tasks[taskId].install_log_path = inst.install_log_path;
|
|
327
|
-
if (inst.install_pid !== undefined && inst.install_pid !== null)
|
|
328
|
-
state.active_tasks[taskId].install_pid = inst.install_pid;
|
|
329
|
-
if (Array.isArray(inst.install_ecosystems))
|
|
330
|
-
state.active_tasks[taskId].install_ecosystems = inst.install_ecosystems;
|
|
331
|
-
if (inst.install_started_at)
|
|
332
|
-
state.active_tasks[taskId].install_started_at = inst.install_started_at;
|
|
333
|
-
if (inst.install_finished_at)
|
|
334
|
-
state.active_tasks[taskId].install_finished_at = inst.install_finished_at;
|
|
335
|
-
if (inst.install_warning)
|
|
336
|
-
state.active_tasks[taskId].install_warning = inst.install_warning;
|
|
337
|
-
// C54 review: persist the per-ecosystem breakdown and
|
|
338
|
-
// pre-spawn scanner rejections. worktree-install already
|
|
339
|
-
// wrote these directly via updateState; forwarding them
|
|
340
|
-
// through the stdout merge keeps the exposed fields
|
|
341
|
-
// consistent with the install envelope.
|
|
342
|
-
if (inst.install_per_ecosystem && typeof inst.install_per_ecosystem === 'object')
|
|
343
|
-
state.active_tasks[taskId].install_per_ecosystem = inst.install_per_ecosystem;
|
|
344
|
-
if (Array.isArray(inst.install_rejections))
|
|
345
|
-
state.active_tasks[taskId].install_rejections = inst.install_rejections;
|
|
346
|
-
}
|
|
347
|
-
if (wtOut.install_warning && !state.active_tasks[taskId].install_warning) {
|
|
348
|
-
state.active_tasks[taskId].install_warning = wtOut.install_warning;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
else {
|
|
352
|
-
state.active_tasks[taskId].worktree_warning =
|
|
353
|
-
'worktree create failed: ' +
|
|
354
|
-
(createRes.stderr || createRes.stdout || 'unknown')
|
|
355
|
-
.toString()
|
|
356
|
-
.trim()
|
|
357
|
-
.split('\n')
|
|
358
|
-
.pop();
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
catch (e) {
|
|
362
|
-
state.active_tasks[taskId].worktree_warning = `worktree hook error: ${e.message}`;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
state.last_activity = new Date().toISOString();
|
|
366
|
-
atomicWriteJson(statePath, state);
|
|
367
|
-
}, { projectRoot: stateRoot });
|
|
368
|
-
if (collisionOverflowErr)
|
|
369
|
-
return collisionOverflowErr;
|
|
370
|
-
regenerateTaskIndex(stateRoot);
|
|
371
|
-
// Team collaboration: register user + add to active board
|
|
372
|
-
updateTeamMember(targetDir);
|
|
373
|
-
store.upsertTeamStatus(taskId, {
|
|
413
|
+
const created = internalCreateTask({
|
|
414
|
+
stateRoot,
|
|
415
|
+
statePath,
|
|
416
|
+
targetDir,
|
|
417
|
+
store,
|
|
418
|
+
config,
|
|
374
419
|
description,
|
|
375
|
-
|
|
420
|
+
track,
|
|
421
|
+
autonomy,
|
|
422
|
+
executionMode,
|
|
376
423
|
scope,
|
|
424
|
+
milestoneId,
|
|
425
|
+
phaseIdForRoadmap,
|
|
426
|
+
roadmapRefUrl,
|
|
427
|
+
dependsOn,
|
|
428
|
+
explicitIdRaw,
|
|
429
|
+
assignedTo: flags.has('assigned-to') ? flags.get('assigned-to') : null,
|
|
377
430
|
});
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
store.appendEvent({
|
|
382
|
-
op: 'task.created',
|
|
383
|
-
task: taskId,
|
|
384
|
-
data: {
|
|
385
|
-
description,
|
|
386
|
-
track,
|
|
387
|
-
scope,
|
|
388
|
-
milestone_id: milestoneId,
|
|
389
|
-
phase_id: phaseIdForRoadmap,
|
|
390
|
-
},
|
|
391
|
-
});
|
|
392
|
-
// Re-read task record so we can surface the worktree block to the
|
|
393
|
-
// router skill without duplicating state.
|
|
394
|
-
let createdTask = null;
|
|
395
|
-
if (existsSync(statePath)) {
|
|
396
|
-
try {
|
|
397
|
-
const s = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
398
|
-
createdTask = s.active_tasks?.[taskId] || null;
|
|
399
|
-
}
|
|
400
|
-
catch {
|
|
401
|
-
/* best-effort */
|
|
402
|
-
}
|
|
403
|
-
}
|
|
431
|
+
if (created.error)
|
|
432
|
+
return created.error;
|
|
433
|
+
const { taskId, taskDir, createdTask } = created;
|
|
404
434
|
const createOut = {
|
|
405
435
|
status: 'ok',
|
|
406
436
|
command: 'task-create',
|
|
@@ -425,6 +455,144 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
425
455
|
}
|
|
426
456
|
return ok(createOut);
|
|
427
457
|
}
|
|
458
|
+
case 'ensure': {
|
|
459
|
+
// ID-02 — deterministic, idempotent allocator invoked by the /apt
|
|
460
|
+
// dispatch layer. The host emits a single schema-bounded
|
|
461
|
+
// allocation_intent bit; this command provisions (or no-ops)
|
|
462
|
+
// deterministically so worktree creation never depends on LLM prose.
|
|
463
|
+
const intent = flags.get('intent');
|
|
464
|
+
const VALID_INTENTS = ['create-new', 'require-existing', 'self-managed', 'none'];
|
|
465
|
+
if (!intent || !VALID_INTENTS.includes(intent)) {
|
|
466
|
+
return err(`Required: --intent <${VALID_INTENTS.join('|')}>`, 1, { command: 'task-ensure' });
|
|
467
|
+
}
|
|
468
|
+
// none / self-managed — allocate nothing.
|
|
469
|
+
if (intent === 'none' || intent === 'self-managed') {
|
|
470
|
+
return ok({
|
|
471
|
+
status: 'ok',
|
|
472
|
+
command: 'task-ensure',
|
|
473
|
+
intent,
|
|
474
|
+
allocated: false,
|
|
475
|
+
reason: intent === 'none'
|
|
476
|
+
? 'allocation_intent=none — brainstorm/investigation/status/help allocates no task or worktree'
|
|
477
|
+
: 'allocation_intent=self-managed — the skill owns its own task/worktree lifecycle',
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
// require-existing — resolve the active task; NEVER create one.
|
|
481
|
+
if (intent === 'require-existing') {
|
|
482
|
+
if (!existsSync(statePath)) {
|
|
483
|
+
return ok({
|
|
484
|
+
status: 'requires_input',
|
|
485
|
+
command: 'task-ensure',
|
|
486
|
+
intent,
|
|
487
|
+
allocated: false,
|
|
488
|
+
reason: 'no active tasks — require-existing cannot mint a new task',
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
const state = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
492
|
+
const active = state.active_tasks && typeof state.active_tasks === 'object'
|
|
493
|
+
? Object.values(state.active_tasks)
|
|
494
|
+
: [];
|
|
495
|
+
if (active.length === 1) {
|
|
496
|
+
const task = active[0];
|
|
497
|
+
const ensureOut = {
|
|
498
|
+
status: 'ok',
|
|
499
|
+
command: 'task-ensure',
|
|
500
|
+
intent,
|
|
501
|
+
allocated: false,
|
|
502
|
+
reused: true,
|
|
503
|
+
task_id: task.id,
|
|
504
|
+
task_dir: getTaskDir(stateRoot, task.id),
|
|
505
|
+
description: task.description,
|
|
506
|
+
track: task.track,
|
|
507
|
+
};
|
|
508
|
+
if (task.worktree_path) {
|
|
509
|
+
ensureOut.worktree = {
|
|
510
|
+
worktree_path: task.worktree_path,
|
|
511
|
+
branch: task.branch,
|
|
512
|
+
base_branch: task.base_branch,
|
|
513
|
+
remote_tracking: !!task.remote_tracking,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
return ok(ensureOut);
|
|
517
|
+
}
|
|
518
|
+
return ok({
|
|
519
|
+
status: 'requires_input',
|
|
520
|
+
command: 'task-ensure',
|
|
521
|
+
intent,
|
|
522
|
+
allocated: false,
|
|
523
|
+
reason: active.length === 0
|
|
524
|
+
? 'no active tasks — require-existing cannot mint a new task'
|
|
525
|
+
: `${active.length} active tasks — disambiguate before require-existing can resolve`,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
// create-new — idempotent allocation via the shared helper (ID-03/ID-04).
|
|
529
|
+
const description = flags.get('description') || flags.get('desc');
|
|
530
|
+
if (!description)
|
|
531
|
+
return err('Required: --description "task description"');
|
|
532
|
+
const track = flags.get('track') || 'STANDARD';
|
|
533
|
+
const scope = (flags.get('scope') || 'project').trim() || 'project';
|
|
534
|
+
let config;
|
|
535
|
+
try {
|
|
536
|
+
config = loadMergedProjectConfig(targetDir) ?? {};
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
config = {};
|
|
540
|
+
}
|
|
541
|
+
// ID-04 — resolve effective autonomy BEFORE forwarding to the worktree
|
|
542
|
+
// hook so a non-default from_branch base does not spuriously defer.
|
|
543
|
+
const autonomy = resolveEffectiveAutonomy(flags, config);
|
|
544
|
+
// ID-03 — ensure-key keyed on normalized description + scope + intent.
|
|
545
|
+
const normalizedDescription = description.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
546
|
+
const ensureKey = `${normalizedDescription}|${scope}|create-new`;
|
|
547
|
+
const allocated = internalCreateTask({
|
|
548
|
+
stateRoot,
|
|
549
|
+
statePath,
|
|
550
|
+
targetDir,
|
|
551
|
+
store,
|
|
552
|
+
config,
|
|
553
|
+
description,
|
|
554
|
+
track,
|
|
555
|
+
autonomy,
|
|
556
|
+
executionMode: null,
|
|
557
|
+
scope,
|
|
558
|
+
milestoneId: null,
|
|
559
|
+
phaseIdForRoadmap: null,
|
|
560
|
+
roadmapRefUrl: null,
|
|
561
|
+
dependsOn: [],
|
|
562
|
+
explicitIdRaw: flags.get('id'),
|
|
563
|
+
assignedTo: flags.has('assigned-to') ? flags.get('assigned-to') : null,
|
|
564
|
+
ensureKey,
|
|
565
|
+
});
|
|
566
|
+
if (allocated.error)
|
|
567
|
+
return allocated.error;
|
|
568
|
+
const ensureOut = {
|
|
569
|
+
status: 'ok',
|
|
570
|
+
command: 'task-ensure',
|
|
571
|
+
intent,
|
|
572
|
+
allocated: true,
|
|
573
|
+
reused: !!allocated.reused,
|
|
574
|
+
task_id: allocated.taskId,
|
|
575
|
+
task_dir: allocated.taskDir,
|
|
576
|
+
description,
|
|
577
|
+
track,
|
|
578
|
+
autonomy,
|
|
579
|
+
};
|
|
580
|
+
const t = allocated.createdTask;
|
|
581
|
+
if (t?.worktree_path) {
|
|
582
|
+
ensureOut.worktree = {
|
|
583
|
+
worktree_path: t.worktree_path,
|
|
584
|
+
branch: t.branch,
|
|
585
|
+
base_branch: t.base_branch,
|
|
586
|
+
remote_tracking: !!t.remote_tracking,
|
|
587
|
+
};
|
|
588
|
+
if (t.push_warning)
|
|
589
|
+
ensureOut.worktree.push_warning = t.push_warning;
|
|
590
|
+
}
|
|
591
|
+
if (t?.worktree_warning) {
|
|
592
|
+
ensureOut.worktree_warning = t.worktree_warning;
|
|
593
|
+
}
|
|
594
|
+
return ok(ensureOut);
|
|
595
|
+
}
|
|
428
596
|
case 'list': {
|
|
429
597
|
if (!existsSync(statePath)) {
|
|
430
598
|
return ok({ status: 'ok', command: 'task-list', tasks: [], count: 0 });
|
|
@@ -960,9 +1128,9 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
960
1128
|
}
|
|
961
1129
|
// FRAMEWORK-BUG-033 defect 2 — durable task→agent ownership.
|
|
962
1130
|
// Mutable owner field; fan-out workers write
|
|
963
|
-
// `fanout-${batch}-${task}` on claim,
|
|
1131
|
+
// `fanout-${batch}-${task}` on claim, showrunner reads to
|
|
964
1132
|
// resolve crash recovery (which worker owned T1 when the
|
|
965
|
-
//
|
|
1133
|
+
// showrunner died?).
|
|
966
1134
|
if (flags.has('assigned-to')) {
|
|
967
1135
|
task.assigned_to = flags.get('assigned-to');
|
|
968
1136
|
}
|