@aperant/framework 0.6.5 → 0.6.7
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 +155 -0
- package/README.md +17 -251
- package/dist/cli/commands/check-version.d.mts.map +1 -1
- package/dist/cli/commands/check-version.mjs +76 -1
- package/dist/cli/commands/check-version.mjs.map +1 -1
- package/dist/cli/commands/ci-watch.d.mts.map +1 -1
- package/dist/cli/commands/ci-watch.mjs +34 -4
- package/dist/cli/commands/ci-watch.mjs.map +1 -1
- package/dist/cli/commands/commit.d.mts.map +1 -1
- package/dist/cli/commands/commit.mjs +25 -8
- package/dist/cli/commands/commit.mjs.map +1 -1
- package/dist/cli/commands/event.d.mts.map +1 -1
- package/dist/cli/commands/event.mjs +66 -0
- package/dist/cli/commands/event.mjs.map +1 -1
- package/dist/cli/commands/init.d.mts +8 -1
- package/dist/cli/commands/init.d.mts.map +1 -1
- package/dist/cli/commands/init.mjs +91 -9
- package/dist/cli/commands/init.mjs.map +1 -1
- package/dist/cli/commands/modes.d.mts.map +1 -1
- package/dist/cli/commands/modes.mjs +2 -1
- package/dist/cli/commands/modes.mjs.map +1 -1
- package/dist/cli/commands/release-notes.d.mts +11 -0
- package/dist/cli/commands/release-notes.d.mts.map +1 -0
- package/dist/cli/commands/release-notes.mjs +173 -0
- package/dist/cli/commands/release-notes.mjs.map +1 -0
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +285 -139
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/commands/triage.d.mts.map +1 -1
- package/dist/cli/commands/triage.mjs +9 -5
- package/dist/cli/commands/triage.mjs.map +1 -1
- package/dist/cli/config/load.d.mts +14 -0
- package/dist/cli/config/load.d.mts.map +1 -1
- package/dist/cli/config/load.mjs +40 -0
- package/dist/cli/config/load.mjs.map +1 -1
- package/dist/cli/config/upgrade-gitignore.d.mts.map +1 -1
- package/dist/cli/config/upgrade-gitignore.mjs +6 -0
- package/dist/cli/config/upgrade-gitignore.mjs.map +1 -1
- package/dist/cli/consistency/parse-review.d.mts.map +1 -1
- package/dist/cli/consistency/parse-review.mjs +4 -1
- package/dist/cli/consistency/parse-review.mjs.map +1 -1
- package/dist/cli/coordination/auto-emit-artifact-ready.d.mts +16 -0
- package/dist/cli/coordination/auto-emit-artifact-ready.d.mts.map +1 -0
- package/dist/cli/coordination/auto-emit-artifact-ready.mjs +88 -0
- package/dist/cli/coordination/auto-emit-artifact-ready.mjs.map +1 -0
- package/dist/cli/coordination/event-schema.d.mts +16 -0
- package/dist/cli/coordination/event-schema.d.mts.map +1 -0
- package/dist/cli/coordination/event-schema.mjs +161 -0
- package/dist/cli/coordination/event-schema.mjs.map +1 -0
- package/dist/cli/coordination/store.d.mts +6 -0
- package/dist/cli/coordination/store.d.mts.map +1 -1
- package/dist/cli/coordination/store.mjs +14 -0
- 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/gate/gates/review-clean.d.mts.map +1 -1
- package/dist/cli/gate/gates/review-clean.mjs +4 -2
- package/dist/cli/gate/gates/review-clean.mjs.map +1 -1
- package/dist/cli/help.mjs +2 -2
- package/dist/cli/help.mjs.map +1 -1
- package/dist/cli/install/update-chips.d.mts +23 -0
- package/dist/cli/install/update-chips.d.mts.map +1 -1
- package/dist/cli/install/update-chips.mjs +60 -0
- package/dist/cli/install/update-chips.mjs.map +1 -1
- package/dist/cli/release-notes/compile.d.mts +38 -0
- package/dist/cli/release-notes/compile.d.mts.map +1 -0
- package/dist/cli/release-notes/compile.mjs +244 -0
- package/dist/cli/release-notes/compile.mjs.map +1 -0
- package/dist/cli/release-notes/draft.d.mts +73 -0
- package/dist/cli/release-notes/draft.d.mts.map +1 -0
- package/dist/cli/release-notes/draft.mjs +120 -0
- package/dist/cli/release-notes/draft.mjs.map +1 -0
- package/dist/cli/release-notes/output-dir.d.mts +20 -0
- package/dist/cli/release-notes/output-dir.d.mts.map +1 -0
- package/dist/cli/release-notes/output-dir.mjs +42 -0
- package/dist/cli/release-notes/output-dir.mjs.map +1 -0
- package/dist/cli/release-notes/persona-filter.d.mts +51 -0
- package/dist/cli/release-notes/persona-filter.d.mts.map +1 -0
- package/dist/cli/release-notes/persona-filter.mjs +127 -0
- package/dist/cli/release-notes/persona-filter.mjs.map +1 -0
- package/dist/cli/release-notes/publish.d.mts +23 -0
- package/dist/cli/release-notes/publish.d.mts.map +1 -0
- package/dist/cli/release-notes/publish.mjs +125 -0
- package/dist/cli/release-notes/publish.mjs.map +1 -0
- package/dist/cli/release-notes/ship-autodraft.d.mts +37 -0
- package/dist/cli/release-notes/ship-autodraft.d.mts.map +1 -0
- package/dist/cli/release-notes/ship-autodraft.mjs +97 -0
- package/dist/cli/release-notes/ship-autodraft.mjs.map +1 -0
- package/dist/cli/roadmap/conductor-view.d.mts +13 -0
- package/dist/cli/roadmap/conductor-view.d.mts.map +1 -0
- package/dist/cli/roadmap/conductor-view.mjs +31 -0
- package/dist/cli/roadmap/conductor-view.mjs.map +1 -0
- package/dist/cli/route/skill-discover.d.mts.map +1 -1
- package/dist/cli/route/skill-discover.mjs +2 -1
- package/dist/cli/route/skill-discover.mjs.map +1 -1
- package/dist/cli/task/ids.d.mts +7 -4
- package/dist/cli/task/ids.d.mts.map +1 -1
- package/dist/cli/task/ids.mjs +11 -10
- package/dist/cli/task/ids.mjs.map +1 -1
- package/dist/types/config.d.ts +11 -7
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +9 -1
- package/prompts/conductor-status-check.md +23 -0
- package/prompts/conductor-sub-agent.md +57 -0
- package/prompts/conductor-system.md +172 -0
- package/skills/apt-close-task/SKILL.md +25 -0
- package/skills/apt-diagram/SKILL.md +45 -9
- package/skills/apt-plan/SKILL.md +12 -0
- package/skills/apt-plan/adapters/conductor.md +98 -0
- package/skills/apt-release-notes/SKILL.md +193 -0
- package/skills/apt-release-notes/appendices/persona-voice.md +59 -0
- package/skills/apt-setup/SKILL.md +148 -3
- package/skills/apt-ship/SKILL.md +74 -12
- package/skills/apt-spar/SKILL.md +290 -0
- package/skills/apt-update/SKILL.md +51 -9
- package/skills/apt-watch-ci/SKILL.md +2 -2
- package/src/cli/commands/check-version.mjs +73 -1
- package/src/cli/commands/ci-watch.mjs +35 -4
- package/src/cli/commands/commit.mjs +27 -8
- package/src/cli/commands/event.mjs +68 -0
- package/src/cli/commands/init.mjs +101 -9
- package/src/cli/commands/modes.mjs +2 -1
- package/src/cli/commands/release-notes.mjs +187 -0
- package/src/cli/commands/task.mjs +305 -152
- package/src/cli/commands/triage.mjs +14 -5
- package/src/cli/config/load.mjs +37 -0
- package/src/cli/config/upgrade-gitignore.mjs +6 -0
- package/src/cli/consistency/parse-review.mjs +3 -1
- package/src/cli/coordination/auto-emit-artifact-ready.mjs +96 -0
- package/src/cli/coordination/event-schema.d.ts +13 -0
- package/src/cli/coordination/event-schema.mjs +174 -0
- package/src/cli/coordination/store.mjs +14 -0
- package/src/cli/dispatch.mjs +2 -0
- package/src/cli/gate/gates/review-clean.mjs +4 -2
- package/src/cli/help.mjs +2 -2
- package/src/cli/install/update-chips.mjs +57 -0
- package/src/cli/release-notes/compile.mjs +261 -0
- package/src/cli/release-notes/draft.mjs +133 -0
- package/src/cli/release-notes/output-dir.mjs +52 -0
- package/src/cli/release-notes/persona-filter.mjs +126 -0
- package/src/cli/release-notes/publish.mjs +128 -0
- package/src/cli/release-notes/ship-autodraft.mjs +106 -0
- package/src/cli/roadmap/conductor-view.d.ts +10 -0
- package/src/cli/roadmap/conductor-view.mjs +31 -0
- package/src/cli/route/skill-discover.mjs +2 -1
- package/src/cli/task/ids.mjs +15 -13
- package/templates/config.json +28 -3
- package/workflows/docs.md +12 -0
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { spawnSync } from 'node:child_process';
|
|
14
14
|
import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
15
15
|
import { join, resolve } from 'node:path';
|
|
16
|
-
import { getTaskIsolationConfig } from '../config/load.mjs';
|
|
16
|
+
import { getTaskIsolationConfig, loadProjectConfig } from '../config/load.mjs';
|
|
17
17
|
import { findMostRecentEventForTask } from '../coordination/event-log.mjs';
|
|
18
18
|
import { createCoordinationStore } from '../coordination/store.mjs';
|
|
19
19
|
import { getGitIdentity } from '../git/identity.mjs';
|
|
@@ -35,11 +35,12 @@ import { parseDuration } from '../util/duration.mjs';
|
|
|
35
35
|
import { withFileLock } from '../util/fs-lock.mjs';
|
|
36
36
|
import { atomicWriteJson } from '../util/io.mjs';
|
|
37
37
|
import { err, exitWith, ok } from '../util/result.mjs';
|
|
38
|
+
import { cmdCiWatch } from './ci-watch.mjs';
|
|
38
39
|
export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
39
40
|
if (!subcommand)
|
|
40
41
|
return err('Usage: apt-tools task <create|list|get|close|update|move|complete-subtask|narration-drain|prune> <project-dir> [flags]');
|
|
41
42
|
if (!projectDir)
|
|
42
|
-
return err(
|
|
43
|
+
return err(`Usage: apt-tools task ${subcommand} <project-dir> [flags]`);
|
|
43
44
|
const targetDir = resolve(projectDir);
|
|
44
45
|
const statePath = join(targetDir, '.aperant', 'state.json');
|
|
45
46
|
const flags = parseFlags(extraArgs);
|
|
@@ -116,151 +117,216 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
116
117
|
// Load config for date format preference
|
|
117
118
|
const configPath = join(targetDir, '.aperant', 'config.json');
|
|
118
119
|
const config = existsSync(configPath) ? JSON.parse(readFileSync(configPath, 'utf-8')) : {};
|
|
119
|
-
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
// User-supplied --id bypasses the collision guard (caller owns the
|
|
121
|
+
// uniqueness contract). Generated ids run through a numeric-counter
|
|
122
|
+
// disambiguator that appends -2, -3, … when the candidate already
|
|
123
|
+
// exists in state or on disk. Bounded at 100 iterations as a
|
|
124
|
+
// defensive ceiling; same-day same-slug collisions are vanishingly
|
|
125
|
+
// rare so the loop should almost always terminate on the first try.
|
|
126
|
+
//
|
|
127
|
+
// BUG-2 fix: the candidate-selection scan, taskDir mkdir, and
|
|
128
|
+
// state.active_tasks write MUST all happen inside a single
|
|
129
|
+
// withFileLock critical section. Two concurrent task create calls
|
|
130
|
+
// in the multi-agent environment can otherwise both compute the
|
|
131
|
+
// same candidate against the same pre-lock snapshot of state and
|
|
132
|
+
// race into a duplicate-id write that silently overwrites the
|
|
133
|
+
// first task's record.
|
|
134
|
+
const SAFE_ID_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
135
|
+
const explicitIdRaw = flags.get('id');
|
|
136
|
+
if (explicitIdRaw && !SAFE_ID_PATTERN.test(explicitIdRaw)) {
|
|
137
|
+
return err('[apt-tools] Invalid task id: must match /^[A-Za-z0-9._-]+$/', 1, {
|
|
138
|
+
command: 'task-create',
|
|
139
|
+
});
|
|
125
140
|
}
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
141
|
+
// Secondary guard: ensure the resolved task directory stays inside the
|
|
142
|
+
// tasks base. The SAFE_ID_PATTERN above blocks `/` and most traversal
|
|
143
|
+
// but allows `.` and `..` as valid characters. A lone `..` id would
|
|
144
|
+
// escape via getTaskDir's join: `tasks/..` resolves to `.aperant/`.
|
|
145
|
+
if (explicitIdRaw) {
|
|
146
|
+
const tasksBase = resolve(join(targetDir, '.aperant', 'tasks'));
|
|
147
|
+
const candidateDir = resolve(getTaskDir(targetDir, explicitIdRaw));
|
|
148
|
+
if (!candidateDir.startsWith(`${tasksBase}/`) && candidateDir !== tasksBase) {
|
|
149
|
+
return err('[apt-tools] Invalid task id: path escapes the tasks directory', 1, {
|
|
150
|
+
command: 'task-create',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
let taskId;
|
|
155
|
+
let taskDir;
|
|
156
|
+
let collisionOverflowErr = null;
|
|
157
|
+
withFileLock(statePath, () => {
|
|
158
|
+
// Read state under the lock so the collision scan, taskDir
|
|
159
|
+
// mkdir, and state.active_tasks write all observe the same
|
|
160
|
+
// snapshot. withFileLock seeds an empty file if statePath
|
|
161
|
+
// doesn't exist, so the JSON.parse fallback handles the
|
|
162
|
+
// first-ever task create cleanly.
|
|
163
|
+
const raw = readFileSync(statePath, 'utf-8').trim();
|
|
164
|
+
const state = raw ? JSON.parse(raw) : {};
|
|
165
|
+
// Migrate: if old format (active_task singular), convert
|
|
166
|
+
if (state.active_task !== undefined && state.active_tasks === undefined) {
|
|
167
|
+
state.active_tasks = {};
|
|
168
|
+
if (state.active_task) {
|
|
169
|
+
const oldId = state.active_task.id || 'legacy-task';
|
|
170
|
+
state.active_tasks[oldId] = state.active_task;
|
|
171
|
+
}
|
|
172
|
+
delete state.active_task;
|
|
173
|
+
}
|
|
174
|
+
if (!state.active_tasks)
|
|
175
|
+
state.active_tasks = {};
|
|
176
|
+
// Resolve taskId against the locked-in state snapshot.
|
|
177
|
+
if (explicitIdRaw) {
|
|
178
|
+
taskId = explicitIdRaw;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
const base = generateTaskId(description, config);
|
|
182
|
+
const activeIds = Object.keys(state.active_tasks);
|
|
183
|
+
const completedIds = Array.isArray(state.completed_tasks)
|
|
184
|
+
? state.completed_tasks.map((row) => row?.id).filter(Boolean)
|
|
185
|
+
: [];
|
|
186
|
+
const known = new Set([...activeIds, ...completedIds]);
|
|
187
|
+
const isTaken = (candidate) => known.has(candidate) || existsSync(getTaskDir(targetDir, candidate));
|
|
188
|
+
let candidate = base;
|
|
189
|
+
let counter = 2;
|
|
190
|
+
while (isTaken(candidate)) {
|
|
191
|
+
if (counter > 100) {
|
|
192
|
+
collisionOverflowErr = err(`task create: collision counter exceeded 100 for base id "${base}" — manual --id required`);
|
|
193
|
+
return;
|
|
136
194
|
}
|
|
137
|
-
|
|
195
|
+
candidate = `${base}-${counter}`;
|
|
196
|
+
counter++;
|
|
138
197
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
198
|
+
taskId = candidate;
|
|
199
|
+
}
|
|
200
|
+
// Create task directory under the lock so a concurrent
|
|
201
|
+
// run can't observe (and then re-pick) a half-created id.
|
|
202
|
+
taskDir = getTaskDir(targetDir, taskId);
|
|
203
|
+
mkdirSync(taskDir, { recursive: true });
|
|
204
|
+
for (const subdir of ['events', 'locks']) {
|
|
205
|
+
mkdirSync(join(taskDir, subdir), { recursive: true });
|
|
206
|
+
}
|
|
207
|
+
// Task-record lifecycle phase (NOT roadmap phase, NOT workflow phase).
|
|
208
|
+
// C28 D-02/D-03/D-08: scope + linkage + dependency-graph fields.
|
|
209
|
+
// R11 — resolve the outputs allowlist now so it is pinned at
|
|
210
|
+
// task creation time. If the planner edits the task record
|
|
211
|
+
// later (e.g. narrows the allowlist), it overwrites this.
|
|
212
|
+
const outputs = resolveDefaultOutputs({
|
|
213
|
+
id: taskId,
|
|
214
|
+
scope,
|
|
215
|
+
milestone_id: milestoneId,
|
|
216
|
+
phase_id: phaseIdForRoadmap,
|
|
217
|
+
});
|
|
218
|
+
state.active_tasks[taskId] = {
|
|
219
|
+
id: taskId,
|
|
220
|
+
description,
|
|
221
|
+
track,
|
|
222
|
+
autonomy,
|
|
223
|
+
// C21 — execution_mode is omitted entirely when unset so legacy
|
|
224
|
+
// readers (apt-plan SKILL.md 9a) continue to see `undefined`
|
|
225
|
+
// rather than `null` and fall through to the autonomy heuristic.
|
|
226
|
+
...(executionMode ? { execution_mode: executionMode } : {}),
|
|
227
|
+
scope,
|
|
228
|
+
milestone_id: milestoneId,
|
|
229
|
+
phase_id: phaseIdForRoadmap,
|
|
230
|
+
roadmap_ref_url: roadmapRefUrl,
|
|
231
|
+
depends_on: dependsOn,
|
|
232
|
+
outputs,
|
|
233
|
+
lifecycle_phase: 'planning',
|
|
234
|
+
history: [],
|
|
235
|
+
created_at: new Date().toISOString(),
|
|
236
|
+
progress: { subtasks_total: 0, subtasks_completed: 0, current_subtask: null },
|
|
237
|
+
};
|
|
238
|
+
// ── Task-level worktree hook ──────────────────────────────
|
|
239
|
+
// When task_isolation.worktree_per_task is enabled AND we're
|
|
240
|
+
// not already inside a worktree AND the host supports them,
|
|
241
|
+
// fork a branch + worktree and record its location on the
|
|
242
|
+
// task record. Any failure is downgraded to a warning so
|
|
243
|
+
// task-create never aborts.
|
|
244
|
+
const isolation = getTaskIsolationConfig(targetDir);
|
|
245
|
+
const host = detectHost();
|
|
246
|
+
if (isolation.worktree_per_task === true &&
|
|
247
|
+
host.capabilities.worktrees === true &&
|
|
248
|
+
!detectWorktree(targetDir).is_worktree) {
|
|
249
|
+
try {
|
|
250
|
+
const wtArgs = ['--task', taskId, '--scope', scope];
|
|
251
|
+
if (isolation.push_on_create)
|
|
252
|
+
wtArgs.push('--push');
|
|
253
|
+
if (isolation.from_branch && isolation.from_branch !== 'current') {
|
|
254
|
+
wtArgs.push('--from', isolation.from_branch);
|
|
255
|
+
}
|
|
256
|
+
// Invoke inline so we capture the structured payload
|
|
257
|
+
// without shelling back out. We cheat by calling the
|
|
258
|
+
// worktree creator directly as a subprocess to keep
|
|
259
|
+
// its output discipline intact, then parse its JSON.
|
|
260
|
+
//
|
|
261
|
+
// D-03 — single source of truth for the apt-tools binary
|
|
262
|
+
// path (replaces the legacy process.argv[1] heuristic).
|
|
263
|
+
const aptToolsBin = resolveAptToolsBin(targetDir);
|
|
264
|
+
const createRes = spawnSync(process.execPath, [aptToolsBin, 'worktree', 'create', targetDir, ...wtArgs], { encoding: 'utf-8' });
|
|
265
|
+
if (createRes.status === 0 && createRes.stdout) {
|
|
266
|
+
const wtOut = JSON.parse(createRes.stdout);
|
|
267
|
+
state.active_tasks[taskId].worktree_path = wtOut.worktree_path;
|
|
268
|
+
state.active_tasks[taskId].branch = wtOut.branch;
|
|
269
|
+
state.active_tasks[taskId].base_branch = wtOut.base_branch;
|
|
270
|
+
state.active_tasks[taskId].remote_tracking = !!wtOut.remote_tracking;
|
|
271
|
+
if (wtOut.scope) {
|
|
272
|
+
state.active_tasks[taskId].worktree_scope = wtOut.scope;
|
|
189
273
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
// worktree creator directly as a subprocess to keep
|
|
193
|
-
// its output discipline intact, then parse its JSON.
|
|
194
|
-
//
|
|
195
|
-
// D-03 — single source of truth for the apt-tools binary
|
|
196
|
-
// path (replaces the legacy process.argv[1] heuristic).
|
|
197
|
-
const aptToolsBin = resolveAptToolsBin(targetDir);
|
|
198
|
-
const createRes = spawnSync(process.execPath, [aptToolsBin, 'worktree', 'create', targetDir, ...wtArgs], { encoding: 'utf-8' });
|
|
199
|
-
if (createRes.status === 0 && createRes.stdout) {
|
|
200
|
-
const wtOut = JSON.parse(createRes.stdout);
|
|
201
|
-
state.active_tasks[taskId].worktree_path = wtOut.worktree_path;
|
|
202
|
-
state.active_tasks[taskId].branch = wtOut.branch;
|
|
203
|
-
state.active_tasks[taskId].base_branch = wtOut.base_branch;
|
|
204
|
-
state.active_tasks[taskId].remote_tracking = !!wtOut.remote_tracking;
|
|
205
|
-
if (wtOut.scope) {
|
|
206
|
-
state.active_tasks[taskId].worktree_scope = wtOut.scope;
|
|
207
|
-
}
|
|
208
|
-
if (wtOut.push_warning) {
|
|
209
|
-
state.active_tasks[taskId].push_warning = wtOut.push_warning;
|
|
210
|
-
}
|
|
211
|
-
// C54 — merge install_* fields surfaced by the
|
|
212
|
-
// worktree-install hook in worktree.mjs. These fields are
|
|
213
|
-
// absent on pre-C54 default-config paths, so readers that
|
|
214
|
-
// don't know about install_* continue to work unchanged.
|
|
215
|
-
if (wtOut.install && typeof wtOut.install === 'object') {
|
|
216
|
-
const inst = wtOut.install;
|
|
217
|
-
if (inst.install_status)
|
|
218
|
-
state.active_tasks[taskId].install_status = inst.install_status;
|
|
219
|
-
if (inst.install_log_path)
|
|
220
|
-
state.active_tasks[taskId].install_log_path = inst.install_log_path;
|
|
221
|
-
if (inst.install_pid !== undefined && inst.install_pid !== null)
|
|
222
|
-
state.active_tasks[taskId].install_pid = inst.install_pid;
|
|
223
|
-
if (Array.isArray(inst.install_ecosystems))
|
|
224
|
-
state.active_tasks[taskId].install_ecosystems = inst.install_ecosystems;
|
|
225
|
-
if (inst.install_started_at)
|
|
226
|
-
state.active_tasks[taskId].install_started_at = inst.install_started_at;
|
|
227
|
-
if (inst.install_finished_at)
|
|
228
|
-
state.active_tasks[taskId].install_finished_at = inst.install_finished_at;
|
|
229
|
-
if (inst.install_warning)
|
|
230
|
-
state.active_tasks[taskId].install_warning = inst.install_warning;
|
|
231
|
-
// C54 review: persist the per-ecosystem breakdown and
|
|
232
|
-
// pre-spawn scanner rejections. worktree-install already
|
|
233
|
-
// wrote these directly via updateState; forwarding them
|
|
234
|
-
// through the stdout merge keeps the exposed fields
|
|
235
|
-
// consistent with the install envelope.
|
|
236
|
-
if (inst.install_per_ecosystem &&
|
|
237
|
-
typeof inst.install_per_ecosystem === 'object')
|
|
238
|
-
state.active_tasks[taskId].install_per_ecosystem = inst.install_per_ecosystem;
|
|
239
|
-
if (Array.isArray(inst.install_rejections))
|
|
240
|
-
state.active_tasks[taskId].install_rejections = inst.install_rejections;
|
|
241
|
-
}
|
|
242
|
-
if (wtOut.install_warning && !state.active_tasks[taskId].install_warning) {
|
|
243
|
-
state.active_tasks[taskId].install_warning = wtOut.install_warning;
|
|
244
|
-
}
|
|
274
|
+
if (wtOut.push_warning) {
|
|
275
|
+
state.active_tasks[taskId].push_warning = wtOut.push_warning;
|
|
245
276
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
277
|
+
// C54 — merge install_* fields surfaced by the
|
|
278
|
+
// worktree-install hook in worktree.mjs. These fields are
|
|
279
|
+
// absent on pre-C54 default-config paths, so readers that
|
|
280
|
+
// don't know about install_* continue to work unchanged.
|
|
281
|
+
if (wtOut.install && typeof wtOut.install === 'object') {
|
|
282
|
+
const inst = wtOut.install;
|
|
283
|
+
if (inst.install_status)
|
|
284
|
+
state.active_tasks[taskId].install_status = inst.install_status;
|
|
285
|
+
if (inst.install_log_path)
|
|
286
|
+
state.active_tasks[taskId].install_log_path = inst.install_log_path;
|
|
287
|
+
if (inst.install_pid !== undefined && inst.install_pid !== null)
|
|
288
|
+
state.active_tasks[taskId].install_pid = inst.install_pid;
|
|
289
|
+
if (Array.isArray(inst.install_ecosystems))
|
|
290
|
+
state.active_tasks[taskId].install_ecosystems = inst.install_ecosystems;
|
|
291
|
+
if (inst.install_started_at)
|
|
292
|
+
state.active_tasks[taskId].install_started_at = inst.install_started_at;
|
|
293
|
+
if (inst.install_finished_at)
|
|
294
|
+
state.active_tasks[taskId].install_finished_at = inst.install_finished_at;
|
|
295
|
+
if (inst.install_warning)
|
|
296
|
+
state.active_tasks[taskId].install_warning = inst.install_warning;
|
|
297
|
+
// C54 review: persist the per-ecosystem breakdown and
|
|
298
|
+
// pre-spawn scanner rejections. worktree-install already
|
|
299
|
+
// wrote these directly via updateState; forwarding them
|
|
300
|
+
// through the stdout merge keeps the exposed fields
|
|
301
|
+
// consistent with the install envelope.
|
|
302
|
+
if (inst.install_per_ecosystem && typeof inst.install_per_ecosystem === 'object')
|
|
303
|
+
state.active_tasks[taskId].install_per_ecosystem = inst.install_per_ecosystem;
|
|
304
|
+
if (Array.isArray(inst.install_rejections))
|
|
305
|
+
state.active_tasks[taskId].install_rejections = inst.install_rejections;
|
|
306
|
+
}
|
|
307
|
+
if (wtOut.install_warning && !state.active_tasks[taskId].install_warning) {
|
|
308
|
+
state.active_tasks[taskId].install_warning = wtOut.install_warning;
|
|
254
309
|
}
|
|
255
310
|
}
|
|
256
|
-
|
|
257
|
-
state.active_tasks[taskId].worktree_warning =
|
|
311
|
+
else {
|
|
312
|
+
state.active_tasks[taskId].worktree_warning =
|
|
313
|
+
'worktree create failed: ' +
|
|
314
|
+
(createRes.stderr || createRes.stdout || 'unknown')
|
|
315
|
+
.toString()
|
|
316
|
+
.trim()
|
|
317
|
+
.split('\n')
|
|
318
|
+
.pop();
|
|
258
319
|
}
|
|
259
320
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
321
|
+
catch (e) {
|
|
322
|
+
state.active_tasks[taskId].worktree_warning = `worktree hook error: ${e.message}`;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
state.last_activity = new Date().toISOString();
|
|
326
|
+
atomicWriteJson(statePath, state);
|
|
327
|
+
}, { projectRoot: targetDir });
|
|
328
|
+
if (collisionOverflowErr)
|
|
329
|
+
return collisionOverflowErr;
|
|
264
330
|
regenerateTaskIndex(targetDir);
|
|
265
331
|
// Team collaboration: register user + add to active board
|
|
266
332
|
updateTeamMember(targetDir);
|
|
@@ -736,6 +802,12 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
736
802
|
// This collapses the two-call apt:ship contract into a single
|
|
737
803
|
// load-bearing call so orchestrators can't drop the lifecycle
|
|
738
804
|
// transition. Idempotent / no-op on any other phase.
|
|
805
|
+
let ciWatchResult = {
|
|
806
|
+
ci_watch_armed: false,
|
|
807
|
+
ci_watch_skip_reason: null,
|
|
808
|
+
ci_watch: null,
|
|
809
|
+
};
|
|
810
|
+
let prUrlForCiWatch = null;
|
|
739
811
|
if (flags.has('pr-url')) {
|
|
740
812
|
const url = flags.get('pr-url');
|
|
741
813
|
if (!url || typeof url !== 'string') {
|
|
@@ -769,6 +841,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
769
841
|
},
|
|
770
842
|
});
|
|
771
843
|
}
|
|
844
|
+
prUrlForCiWatch = url;
|
|
772
845
|
}
|
|
773
846
|
if (flags.has('subtasks-total'))
|
|
774
847
|
task.progress.subtasks_total = parseInt(flags.get('subtasks-total'), 10);
|
|
@@ -823,6 +896,9 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
823
896
|
}
|
|
824
897
|
state.last_activity = new Date().toISOString();
|
|
825
898
|
atomicWriteJson(statePath, state);
|
|
899
|
+
if (prUrlForCiWatch) {
|
|
900
|
+
ciWatchResult = armCiWatchAfterPrUrl(targetDir, task, prUrlForCiWatch);
|
|
901
|
+
}
|
|
826
902
|
regenerateTaskIndex(targetDir);
|
|
827
903
|
// Team collaboration: sync active board with updated state
|
|
828
904
|
updateTeamMember(targetDir);
|
|
@@ -838,6 +914,12 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
838
914
|
const updateOut = { status: 'ok', command: 'task-update', task_id: taskId, task };
|
|
839
915
|
if (lifecycleTransition)
|
|
840
916
|
updateOut.transition = lifecycleTransition;
|
|
917
|
+
if (flags.has('pr-url')) {
|
|
918
|
+
updateOut.ci_watch_armed = ciWatchResult.ci_watch_armed;
|
|
919
|
+
updateOut.ci_watch_skip_reason = ciWatchResult.ci_watch_skip_reason;
|
|
920
|
+
if (ciWatchResult.ci_watch)
|
|
921
|
+
updateOut.ci_watch = ciWatchResult.ci_watch;
|
|
922
|
+
}
|
|
841
923
|
updateResult = ok(updateOut);
|
|
842
924
|
}, { projectRoot: targetDir });
|
|
843
925
|
return updateError ?? updateResult;
|
|
@@ -1321,7 +1403,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1321
1403
|
const pruned = [];
|
|
1322
1404
|
const skipped = [];
|
|
1323
1405
|
for (const row of state.completed_tasks || []) {
|
|
1324
|
-
if (!row
|
|
1406
|
+
if (!row?.id)
|
|
1325
1407
|
continue;
|
|
1326
1408
|
const rowScope = row.scope || 'project';
|
|
1327
1409
|
if (scopeFilter && rowScope !== scopeFilter) {
|
|
@@ -1395,6 +1477,70 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1395
1477
|
return err(`Unknown task subcommand: ${subcommand}. Expected: create, list, get, close, update, move, complete-subtask, close-merged, narration-drain, prune`);
|
|
1396
1478
|
}
|
|
1397
1479
|
}
|
|
1480
|
+
function armCiWatchAfterPrUrl(targetDir, task, prUrl) {
|
|
1481
|
+
const pr = extractPrNumber(prUrl);
|
|
1482
|
+
if (!pr) {
|
|
1483
|
+
return {
|
|
1484
|
+
ci_watch_armed: false,
|
|
1485
|
+
ci_watch_skip_reason: 'invalid-pr-url',
|
|
1486
|
+
ci_watch: null,
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
const host = detectHost();
|
|
1490
|
+
const defaultMode = host.capabilities?.cron ? 'auto' : 'never';
|
|
1491
|
+
const config = loadProjectConfig(targetDir);
|
|
1492
|
+
const configuredMode = config?.ci_watch?.after_ship || defaultMode;
|
|
1493
|
+
const mode = configuredMode === 'ask' && Number(task.autonomy) === 3 ? 'auto' : configuredMode;
|
|
1494
|
+
if (mode === 'never') {
|
|
1495
|
+
return {
|
|
1496
|
+
ci_watch_armed: false,
|
|
1497
|
+
ci_watch_skip_reason: 'disabled',
|
|
1498
|
+
ci_watch: null,
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
if (mode === 'ask') {
|
|
1502
|
+
return {
|
|
1503
|
+
ci_watch_armed: false,
|
|
1504
|
+
ci_watch_skip_reason: 'ask-deferred',
|
|
1505
|
+
ci_watch: null,
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
if (mode !== 'auto') {
|
|
1509
|
+
return {
|
|
1510
|
+
ci_watch_armed: false,
|
|
1511
|
+
ci_watch_skip_reason: `invalid-config:${mode}`,
|
|
1512
|
+
ci_watch: null,
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
const result = cmdCiWatch('start', targetDir, [
|
|
1516
|
+
'--pr',
|
|
1517
|
+
String(pr),
|
|
1518
|
+
'--task-id',
|
|
1519
|
+
task.id,
|
|
1520
|
+
'--host',
|
|
1521
|
+
host.cli || 'unknown',
|
|
1522
|
+
]);
|
|
1523
|
+
const envelope = result?.envelope || {};
|
|
1524
|
+
if (envelope.status === 'ok') {
|
|
1525
|
+
return {
|
|
1526
|
+
ci_watch_armed: true,
|
|
1527
|
+
ci_watch_skip_reason: null,
|
|
1528
|
+
ci_watch: envelope,
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
if (envelope.status === 'busy' && envelope.reason === 'already-armed') {
|
|
1532
|
+
return {
|
|
1533
|
+
ci_watch_armed: true,
|
|
1534
|
+
ci_watch_skip_reason: 'already-armed',
|
|
1535
|
+
ci_watch: envelope,
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
return {
|
|
1539
|
+
ci_watch_armed: false,
|
|
1540
|
+
ci_watch_skip_reason: envelope.reason || 'start-failed',
|
|
1541
|
+
ci_watch: envelope,
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1398
1544
|
/**
|
|
1399
1545
|
* Extract a PR number from a GitHub URL like https://github.com/owner/repo/pull/123
|
|
1400
1546
|
* or a short form like `owner/repo#123`. Returns null if not parseable.
|