@aperant/framework 0.6.4 → 0.6.6
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 +38 -0
- package/README.md +54 -234
- package/agents/apt-improver.md +99 -0
- package/agents/apt-planner.md +115 -10
- package/dist/__test-helpers/run-cmd.d.mts +4 -2
- package/dist/__test-helpers/run-cmd.d.mts.map +1 -1
- package/dist/__test-helpers/run-cmd.mjs +56 -15
- package/dist/__test-helpers/run-cmd.mjs.map +1 -1
- package/dist/cli/ci-watch/lock.d.mts +53 -0
- package/dist/cli/ci-watch/lock.d.mts.map +1 -0
- package/dist/cli/ci-watch/lock.mjs +172 -0
- package/dist/cli/ci-watch/lock.mjs.map +1 -0
- package/dist/cli/ci-watch/state.d.mts +36 -0
- package/dist/cli/ci-watch/state.d.mts.map +1 -0
- package/dist/cli/ci-watch/state.mjs +103 -0
- package/dist/cli/ci-watch/state.mjs.map +1 -0
- package/dist/cli/ci-watch/stop-matrix.d.mts +58 -0
- package/dist/cli/ci-watch/stop-matrix.d.mts.map +1 -0
- package/dist/cli/ci-watch/stop-matrix.mjs +164 -0
- package/dist/cli/ci-watch/stop-matrix.mjs.map +1 -0
- package/dist/cli/cli-wrappers/ci-watch.d.mts +2 -0
- package/dist/cli/cli-wrappers/ci-watch.d.mts.map +1 -0
- package/dist/cli/cli-wrappers/ci-watch.mjs +9 -0
- package/dist/cli/cli-wrappers/ci-watch.mjs.map +1 -0
- package/dist/cli/commands/adr.d.mts +5 -0
- package/dist/cli/commands/adr.d.mts.map +1 -0
- package/dist/cli/commands/adr.mjs +228 -0
- package/dist/cli/commands/adr.mjs.map +1 -0
- package/dist/cli/commands/ci-watch.d.mts +7 -0
- package/dist/cli/commands/ci-watch.d.mts.map +1 -0
- package/dist/cli/commands/ci-watch.mjs +465 -0
- package/dist/cli/commands/ci-watch.mjs.map +1 -0
- 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/context.d.mts +7 -0
- package/dist/cli/commands/context.d.mts.map +1 -0
- package/dist/cli/commands/context.mjs +224 -0
- package/dist/cli/commands/context.mjs.map +1 -0
- package/dist/cli/commands/event.d.mts.map +1 -1
- package/dist/cli/commands/event.mjs +125 -24
- package/dist/cli/commands/event.mjs.map +1 -1
- package/dist/cli/commands/host-detect.d.mts +1 -1
- package/dist/cli/commands/host-detect.d.mts.map +1 -1
- package/dist/cli/commands/host-detect.mjs +30 -3
- package/dist/cli/commands/host-detect.mjs.map +1 -1
- package/dist/cli/commands/modes.d.mts +13 -0
- package/dist/cli/commands/modes.d.mts.map +1 -0
- package/dist/cli/commands/modes.mjs +221 -0
- package/dist/cli/commands/modes.mjs.map +1 -0
- package/dist/cli/commands/route.mjs +1 -1
- package/dist/cli/commands/route.mjs.map +1 -1
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +393 -145
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/commands/tokens.d.mts.map +1 -1
- package/dist/cli/commands/tokens.mjs +150 -6
- package/dist/cli/commands/tokens.mjs.map +1 -1
- package/dist/cli/commands/triage.d.mts +8 -0
- package/dist/cli/commands/triage.d.mts.map +1 -0
- package/dist/cli/commands/triage.mjs +263 -0
- package/dist/cli/commands/triage.mjs.map +1 -0
- package/dist/cli/config/post-merge-sweep.d.mts +18 -0
- package/dist/cli/config/post-merge-sweep.d.mts.map +1 -1
- package/dist/cli/config/post-merge-sweep.mjs +48 -3
- package/dist/cli/config/post-merge-sweep.mjs.map +1 -1
- package/dist/cli/consistency/registry.d.mts +11 -0
- package/dist/cli/consistency/registry.d.mts.map +1 -1
- package/dist/cli/consistency/registry.mjs +13 -0
- package/dist/cli/consistency/registry.mjs.map +1 -1
- package/dist/cli/coordination/agent-identity.d.mts +9 -0
- package/dist/cli/coordination/agent-identity.d.mts.map +1 -1
- package/dist/cli/coordination/agent-identity.mjs +11 -0
- package/dist/cli/coordination/agent-identity.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-log.d.mts +27 -0
- package/dist/cli/coordination/event-log.d.mts.map +1 -1
- package/dist/cli/coordination/event-log.mjs +72 -2
- package/dist/cli/coordination/event-log.mjs.map +1 -1
- 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 +14 -1
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/gate/registry.d.mts +11 -0
- package/dist/cli/gate/registry.d.mts.map +1 -1
- package/dist/cli/gate/registry.mjs +13 -0
- package/dist/cli/gate/registry.mjs.map +1 -1
- package/dist/cli/help.d.mts.map +1 -1
- package/dist/cli/help.mjs +3 -2
- package/dist/cli/help.mjs.map +1 -1
- package/dist/cli/host/detect.d.mts +1 -0
- package/dist/cli/host/detect.d.mts.map +1 -1
- package/dist/cli/host/detect.mjs +5 -0
- package/dist/cli/host/detect.mjs.map +1 -1
- 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/envelope.d.mts +68 -4
- package/dist/cli/route/envelope.d.mts.map +1 -1
- package/dist/cli/route/envelope.mjs +140 -103
- package/dist/cli/route/envelope.mjs.map +1 -1
- package/dist/cli/route/skill-discover.d.mts +9 -0
- package/dist/cli/route/skill-discover.d.mts.map +1 -1
- package/dist/cli/route/skill-discover.mjs +13 -1
- package/dist/cli/route/skill-discover.mjs.map +1 -1
- package/dist/cli/skill-author/contract.d.mts +1 -0
- package/dist/cli/skill-author/contract.d.mts.map +1 -1
- package/dist/cli/skill-author/contract.mjs +7 -0
- package/dist/cli/skill-author/contract.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/cli/task/worktree-cleanup.d.mts +9 -1
- package/dist/cli/task/worktree-cleanup.d.mts.map +1 -1
- package/dist/cli/task/worktree-cleanup.mjs +190 -9
- package/dist/cli/task/worktree-cleanup.mjs.map +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/agents/apt-planner.md +1 -1
- package/dist/plugin/skills/apt/SKILL.md +111 -5
- package/dist/plugin/skills/apt-author-skill/SKILL.md +11 -0
- package/dist/plugin/skills/apt-bootstrap/SKILL.md +1 -0
- package/dist/plugin/skills/apt-classify/SKILL.md +1 -0
- package/dist/plugin/skills/apt-close-task/SKILL.md +1 -0
- package/dist/plugin/skills/apt-create-docs/SKILL.md +1 -0
- package/dist/plugin/skills/apt-debug/SKILL.md +2 -0
- package/dist/plugin/skills/apt-design/SKILL.md +2 -0
- package/dist/plugin/skills/apt-discuss/SKILL.md +2 -0
- package/dist/plugin/skills/apt-docs/SKILL.md +2 -0
- package/dist/plugin/skills/apt-execute/SKILL.md +1 -0
- package/dist/plugin/skills/apt-mockup/SKILL.md +2 -0
- package/dist/plugin/skills/apt-pause/SKILL.md +1 -0
- package/dist/plugin/skills/apt-personas/SKILL.md +1 -0
- package/dist/plugin/skills/apt-plan/SKILL.md +2 -0
- package/dist/plugin/skills/apt-pr-review/SKILL.md +1 -0
- package/dist/plugin/skills/apt-quick/SKILL.md +2 -0
- package/dist/plugin/skills/apt-resume/SKILL.md +1 -0
- package/dist/plugin/skills/apt-review/SKILL.md +1 -0
- package/dist/plugin/skills/apt-roadmap/SKILL.md +1 -0
- package/dist/plugin/skills/apt-roundtable/SKILL.md +2 -0
- package/dist/plugin/skills/apt-run/SKILL.md +1 -0
- package/dist/plugin/skills/apt-scan/SKILL.md +1 -0
- package/dist/plugin/skills/apt-setup/SKILL.md +1 -0
- package/dist/plugin/skills/apt-ship/SKILL.md +6 -5
- package/dist/plugin/skills/apt-stress-test/SKILL.md +1 -0
- package/dist/plugin/skills/apt-terminal/SKILL.md +1 -0
- package/dist/plugin/skills/apt-update/SKILL.md +5 -0
- package/dist/plugin/skills/apt-verify/SKILL.md +1 -0
- package/dist/plugin/skills/apt-verify-proof/SKILL.md +1 -0
- package/dist/types/config.d.ts +85 -0
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +9 -1
- package/prompts/coder.md +2 -0
- package/prompts/conductor-status-check.md +23 -0
- package/prompts/conductor-sub-agent.md +57 -0
- package/prompts/conductor-system.md +172 -0
- package/prompts/planner.md +12 -0
- package/prompts/spec_writer.md +9 -0
- package/skills/apt/SKILL.md +1 -0
- package/skills/apt-close-task/SKILL.md +32 -1
- package/skills/apt-debug/SKILL.md +39 -6
- package/skills/apt-debug/appendices/diagnose-discipline.md +119 -0
- package/skills/apt-diagram/SKILL.md +342 -0
- package/skills/apt-diagram/appendices/design-discipline.md +97 -0
- package/skills/apt-discuss/SKILL.md +25 -0
- package/skills/apt-discuss/appendices/grill-discipline.md +104 -0
- package/skills/apt-discuss/appendices/zoom-out-helper.md +79 -0
- package/skills/apt-execute/SKILL.md +48 -5
- package/skills/apt-execute/appendices/tdd-mode.md +107 -0
- package/skills/apt-improve/DEEPENING.md +84 -0
- package/skills/apt-improve/INTERFACE-DESIGN.md +97 -0
- package/skills/apt-improve/LANGUAGE.md +104 -0
- package/skills/apt-improve/SKILL.md +141 -0
- package/skills/apt-plan/SKILL.md +159 -4
- package/skills/apt-plan/adapters/conductor.md +98 -0
- package/skills/apt-planner.md +42 -1
- package/skills/apt-pr-review/SKILL.md +46 -16
- package/skills/apt-prototype/LOGIC.md +109 -0
- package/skills/apt-prototype/SKILL.md +143 -0
- package/skills/apt-prototype/UI.md +90 -0
- package/skills/apt-quick/SKILL.md +30 -0
- package/skills/apt-review/SKILL.md +2 -0
- package/skills/apt-run/SKILL.md +32 -4
- package/skills/apt-setup/SKILL.md +130 -2
- package/skills/apt-ship/SKILL.md +63 -1
- package/skills/apt-spar/SKILL.md +290 -0
- package/skills/apt-triage/AGENT-BRIEF.md +84 -0
- package/skills/apt-triage/OUT-OF-SCOPE.md +75 -0
- package/skills/apt-triage/SKILL.md +169 -0
- package/skills/apt-verify/SKILL.md +3 -0
- package/skills/apt-verify-proof/SKILL.md +3 -0
- package/skills/apt-watch-ci/SKILL.md +163 -0
- package/skills/apt-zoom-out/SKILL.md +130 -0
- package/src/cli/ci-watch/lock.mjs +178 -0
- package/src/cli/ci-watch/state.mjs +103 -0
- package/src/cli/ci-watch/stop-matrix.mjs +181 -0
- package/src/cli/cli-wrappers/ci-watch.mjs +9 -0
- package/src/cli/commands/adr.mjs +243 -0
- package/src/cli/commands/ci-watch.mjs +503 -0
- package/src/cli/commands/commit.mjs +27 -8
- package/src/cli/commands/context.mjs +244 -0
- package/src/cli/commands/event.mjs +131 -24
- package/src/cli/commands/host-detect.mjs +33 -7
- package/src/cli/commands/modes.mjs +216 -0
- package/src/cli/commands/route.mjs +1 -1
- package/src/cli/commands/task.mjs +430 -170
- package/src/cli/commands/tokens.mjs +157 -6
- package/src/cli/commands/triage.mjs +286 -0
- package/src/cli/config/post-merge-sweep.mjs +49 -3
- package/src/cli/consistency/registry.mjs +14 -0
- package/src/cli/coordination/agent-identity.mjs +12 -0
- package/src/cli/coordination/auto-emit-artifact-ready.mjs +96 -0
- package/src/cli/coordination/event-log.mjs +73 -2
- 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 +15 -1
- package/src/cli/gate/registry.mjs +14 -0
- package/src/cli/help.mjs +3 -2
- package/src/cli/host/detect.mjs +5 -0
- package/src/cli/roadmap/conductor-view.d.ts +10 -0
- package/src/cli/roadmap/conductor-view.mjs +31 -0
- package/src/cli/route/envelope.mjs +140 -106
- package/src/cli/route/skill-discover.mjs +14 -1
- package/src/cli/skill-author/contract.mjs +7 -0
- package/src/cli/task/ids.mjs +15 -13
- package/src/cli/task/worktree-cleanup.mjs +191 -9
- package/templates/adr-format.md +56 -0
- package/templates/config.json +4 -0
- package/templates/context-format.md +34 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* commands/task.mjs — per-task isolated directories for multi-agent concurrency (C29).
|
|
3
3
|
*
|
|
4
|
-
* task subcommand: create | list | get | close | update | move | complete-subtask | prune
|
|
4
|
+
* task subcommand: create | list | get | close | update | move | complete-subtask | narration-drain | prune
|
|
5
5
|
*
|
|
6
6
|
* Layer 1 (post-CLI-layering refactor, subtask 9). cmdTask returns
|
|
7
7
|
* { envelope, exitCode } — it MUST NOT call process.exit, output(), or fail()
|
|
@@ -13,7 +13,8 @@
|
|
|
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
|
+
import { findMostRecentEventForTask } from '../coordination/event-log.mjs';
|
|
17
18
|
import { createCoordinationStore } from '../coordination/store.mjs';
|
|
18
19
|
import { getGitIdentity } from '../git/identity.mjs';
|
|
19
20
|
import { detectWorktree } from '../git/worktree-detect.mjs';
|
|
@@ -34,11 +35,12 @@ import { parseDuration } from '../util/duration.mjs';
|
|
|
34
35
|
import { withFileLock } from '../util/fs-lock.mjs';
|
|
35
36
|
import { atomicWriteJson } from '../util/io.mjs';
|
|
36
37
|
import { err, exitWith, ok } from '../util/result.mjs';
|
|
38
|
+
import { cmdCiWatch } from './ci-watch.mjs';
|
|
37
39
|
export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
38
40
|
if (!subcommand)
|
|
39
|
-
return err('Usage: apt-tools task <create|list|get|close|update|move|complete-subtask|prune> <project-dir> [flags]');
|
|
41
|
+
return err('Usage: apt-tools task <create|list|get|close|update|move|complete-subtask|narration-drain|prune> <project-dir> [flags]');
|
|
40
42
|
if (!projectDir)
|
|
41
|
-
return err(
|
|
43
|
+
return err(`Usage: apt-tools task ${subcommand} <project-dir> [flags]`);
|
|
42
44
|
const targetDir = resolve(projectDir);
|
|
43
45
|
const statePath = join(targetDir, '.aperant', 'state.json');
|
|
44
46
|
const flags = parseFlags(extraArgs);
|
|
@@ -115,151 +117,216 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
115
117
|
// Load config for date format preference
|
|
116
118
|
const configPath = join(targetDir, '.aperant', 'config.json');
|
|
117
119
|
const config = existsSync(configPath) ? JSON.parse(readFileSync(configPath, 'utf-8')) : {};
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
});
|
|
124
140
|
}
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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;
|
|
135
194
|
}
|
|
136
|
-
|
|
195
|
+
candidate = `${base}-${counter}`;
|
|
196
|
+
counter++;
|
|
137
197
|
}
|
|
138
|
-
|
|
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
|
-
|
|
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;
|
|
188
273
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// worktree creator directly as a subprocess to keep
|
|
192
|
-
// its output discipline intact, then parse its JSON.
|
|
193
|
-
//
|
|
194
|
-
// D-03 — single source of truth for the apt-tools binary
|
|
195
|
-
// path (replaces the legacy process.argv[1] heuristic).
|
|
196
|
-
const aptToolsBin = resolveAptToolsBin(targetDir);
|
|
197
|
-
const createRes = spawnSync(process.execPath, [aptToolsBin, 'worktree', 'create', targetDir, ...wtArgs], { encoding: 'utf-8' });
|
|
198
|
-
if (createRes.status === 0 && createRes.stdout) {
|
|
199
|
-
const wtOut = JSON.parse(createRes.stdout);
|
|
200
|
-
state.active_tasks[taskId].worktree_path = wtOut.worktree_path;
|
|
201
|
-
state.active_tasks[taskId].branch = wtOut.branch;
|
|
202
|
-
state.active_tasks[taskId].base_branch = wtOut.base_branch;
|
|
203
|
-
state.active_tasks[taskId].remote_tracking = !!wtOut.remote_tracking;
|
|
204
|
-
if (wtOut.scope) {
|
|
205
|
-
state.active_tasks[taskId].worktree_scope = wtOut.scope;
|
|
206
|
-
}
|
|
207
|
-
if (wtOut.push_warning) {
|
|
208
|
-
state.active_tasks[taskId].push_warning = wtOut.push_warning;
|
|
209
|
-
}
|
|
210
|
-
// C54 — merge install_* fields surfaced by the
|
|
211
|
-
// worktree-install hook in worktree.mjs. These fields are
|
|
212
|
-
// absent on pre-C54 default-config paths, so readers that
|
|
213
|
-
// don't know about install_* continue to work unchanged.
|
|
214
|
-
if (wtOut.install && typeof wtOut.install === 'object') {
|
|
215
|
-
const inst = wtOut.install;
|
|
216
|
-
if (inst.install_status)
|
|
217
|
-
state.active_tasks[taskId].install_status = inst.install_status;
|
|
218
|
-
if (inst.install_log_path)
|
|
219
|
-
state.active_tasks[taskId].install_log_path = inst.install_log_path;
|
|
220
|
-
if (inst.install_pid !== undefined && inst.install_pid !== null)
|
|
221
|
-
state.active_tasks[taskId].install_pid = inst.install_pid;
|
|
222
|
-
if (Array.isArray(inst.install_ecosystems))
|
|
223
|
-
state.active_tasks[taskId].install_ecosystems = inst.install_ecosystems;
|
|
224
|
-
if (inst.install_started_at)
|
|
225
|
-
state.active_tasks[taskId].install_started_at = inst.install_started_at;
|
|
226
|
-
if (inst.install_finished_at)
|
|
227
|
-
state.active_tasks[taskId].install_finished_at = inst.install_finished_at;
|
|
228
|
-
if (inst.install_warning)
|
|
229
|
-
state.active_tasks[taskId].install_warning = inst.install_warning;
|
|
230
|
-
// C54 review: persist the per-ecosystem breakdown and
|
|
231
|
-
// pre-spawn scanner rejections. worktree-install already
|
|
232
|
-
// wrote these directly via updateState; forwarding them
|
|
233
|
-
// through the stdout merge keeps the exposed fields
|
|
234
|
-
// consistent with the install envelope.
|
|
235
|
-
if (inst.install_per_ecosystem &&
|
|
236
|
-
typeof inst.install_per_ecosystem === 'object')
|
|
237
|
-
state.active_tasks[taskId].install_per_ecosystem = inst.install_per_ecosystem;
|
|
238
|
-
if (Array.isArray(inst.install_rejections))
|
|
239
|
-
state.active_tasks[taskId].install_rejections = inst.install_rejections;
|
|
240
|
-
}
|
|
241
|
-
if (wtOut.install_warning && !state.active_tasks[taskId].install_warning) {
|
|
242
|
-
state.active_tasks[taskId].install_warning = wtOut.install_warning;
|
|
243
|
-
}
|
|
274
|
+
if (wtOut.push_warning) {
|
|
275
|
+
state.active_tasks[taskId].push_warning = wtOut.push_warning;
|
|
244
276
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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;
|
|
253
309
|
}
|
|
254
310
|
}
|
|
255
|
-
|
|
256
|
-
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();
|
|
257
319
|
}
|
|
258
320
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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;
|
|
263
330
|
regenerateTaskIndex(targetDir);
|
|
264
331
|
// Team collaboration: register user + add to active board
|
|
265
332
|
updateTeamMember(targetDir);
|
|
@@ -416,6 +483,11 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
416
483
|
if (!taskId)
|
|
417
484
|
return err('Required: --id <task-id>');
|
|
418
485
|
const verdict = flags.get('verdict') || 'completed';
|
|
486
|
+
// Internal flag set by `task close-merged` when
|
|
487
|
+
// GitHub confirmed state === 'MERGED'. Threads through to
|
|
488
|
+
// computeWorktreeCleanup so cleanup skips local merge-back and
|
|
489
|
+
// runs fetch + ff-only instead.
|
|
490
|
+
const prMerged = flags.has('pr-merged-remotely');
|
|
419
491
|
if (!existsSync(statePath))
|
|
420
492
|
return err('No state.json found');
|
|
421
493
|
// withFileLock callback can produce either an error envelope (early
|
|
@@ -472,11 +544,14 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
472
544
|
});
|
|
473
545
|
// Worktree cleanup hook — honors task_isolation.cleanup_on_close
|
|
474
546
|
// (never | ask | auto). See src/cli/task/worktree-cleanup.mjs.
|
|
547
|
+
// prMerged threads from `task close-merged`
|
|
548
|
+
// via --pr-merged-remotely; when true, cleanup runs fetch +
|
|
549
|
+
// ff-only instead of local merge-back.
|
|
475
550
|
let cleanup = null;
|
|
476
551
|
if (task.worktree_path) {
|
|
477
552
|
const isolation = getTaskIsolationConfig(targetDir);
|
|
478
553
|
const mode = isolation.cleanup_on_close || 'ask';
|
|
479
|
-
cleanup = computeWorktreeCleanup(task, mode, targetDir);
|
|
554
|
+
cleanup = computeWorktreeCleanup(task, mode, targetDir, { prMerged });
|
|
480
555
|
}
|
|
481
556
|
// Best-effort team sync: commit shared bookkeeping so teammates
|
|
482
557
|
// don't have to pick up orphaned diffs. Never fails task close.
|
|
@@ -653,6 +728,22 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
653
728
|
let lifecycleTransition = null;
|
|
654
729
|
if (flags.has('lifecycle-phase')) {
|
|
655
730
|
task.lifecycle_phase = flags.get('lifecycle-phase');
|
|
731
|
+
// R6 (roundtable 2026-05-14) — compute elapsed_ms since the
|
|
732
|
+
// previous LIFECYCLE event (or task.created) for phase-boundary
|
|
733
|
+
// telemetry. The 'task.lifecycle.' opPrefix is critical: without
|
|
734
|
+
// it, the most-recent event lookup picks up high-frequency events
|
|
735
|
+
// like task.tokens.recorded, which would silently corrupt
|
|
736
|
+
// elapsed_ms attribution (PR #115 review finding LOG-001).
|
|
737
|
+
const prev = findMostRecentEventForTask(targetDir, taskId, 'task.lifecycle.');
|
|
738
|
+
let prevTime = prev ? new Date(prev.t).getTime() : null;
|
|
739
|
+
// LOG-003: when no prior lifecycle event exists (first transition),
|
|
740
|
+
// fall back to task.created_at so planning phase duration is captured.
|
|
741
|
+
if (!prevTime && task.created_at) {
|
|
742
|
+
const createdMs = new Date(task.created_at).getTime();
|
|
743
|
+
if (Number.isFinite(createdMs))
|
|
744
|
+
prevTime = createdMs;
|
|
745
|
+
}
|
|
746
|
+
const elapsed_ms = prevTime ? Date.now() - prevTime : null;
|
|
656
747
|
// R10 — emit the lifecycle transition event even when the
|
|
657
748
|
// side-effect helper decides the phase spec is a no-op; the
|
|
658
749
|
// caller still changed task.lifecycle_phase.
|
|
@@ -664,6 +755,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
664
755
|
scope: task.scope || 'project',
|
|
665
756
|
milestone_id: task.milestone_id ?? null,
|
|
666
757
|
phase_id: task.phase_id ?? null,
|
|
758
|
+
elapsed_ms,
|
|
667
759
|
},
|
|
668
760
|
});
|
|
669
761
|
// C56 B5 — exhaustive lifecycle→roadmap-phase map. `null`
|
|
@@ -710,6 +802,12 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
710
802
|
// This collapses the two-call apt:ship contract into a single
|
|
711
803
|
// load-bearing call so orchestrators can't drop the lifecycle
|
|
712
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;
|
|
713
811
|
if (flags.has('pr-url')) {
|
|
714
812
|
const url = flags.get('pr-url');
|
|
715
813
|
if (!url || typeof url !== 'string') {
|
|
@@ -743,6 +841,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
743
841
|
},
|
|
744
842
|
});
|
|
745
843
|
}
|
|
844
|
+
prUrlForCiWatch = url;
|
|
746
845
|
}
|
|
747
846
|
if (flags.has('subtasks-total'))
|
|
748
847
|
task.progress.subtasks_total = parseInt(flags.get('subtasks-total'), 10);
|
|
@@ -797,6 +896,9 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
797
896
|
}
|
|
798
897
|
state.last_activity = new Date().toISOString();
|
|
799
898
|
atomicWriteJson(statePath, state);
|
|
899
|
+
if (prUrlForCiWatch) {
|
|
900
|
+
ciWatchResult = armCiWatchAfterPrUrl(targetDir, task, prUrlForCiWatch);
|
|
901
|
+
}
|
|
800
902
|
regenerateTaskIndex(targetDir);
|
|
801
903
|
// Team collaboration: sync active board with updated state
|
|
802
904
|
updateTeamMember(targetDir);
|
|
@@ -812,6 +914,12 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
812
914
|
const updateOut = { status: 'ok', command: 'task-update', task_id: taskId, task };
|
|
813
915
|
if (lifecycleTransition)
|
|
814
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
|
+
}
|
|
815
923
|
updateResult = ok(updateOut);
|
|
816
924
|
}, { projectRoot: targetDir });
|
|
817
925
|
return updateError ?? updateResult;
|
|
@@ -1126,7 +1234,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1126
1234
|
// Drive the full close path via an in-process CLI call so all of
|
|
1127
1235
|
// task close's side effects (team-sync, worktree cleanup, phase flip
|
|
1128
1236
|
// when auto_close_phase is on, event emission, INDEX regen) run.
|
|
1129
|
-
const
|
|
1237
|
+
const closeArgs = [
|
|
1130
1238
|
resolve(import.meta.dirname ?? '.', '..', '..', '..', 'bin', 'apt-tools.mjs'),
|
|
1131
1239
|
'task',
|
|
1132
1240
|
'close',
|
|
@@ -1135,7 +1243,18 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1135
1243
|
id,
|
|
1136
1244
|
'--verdict',
|
|
1137
1245
|
closeVerdict,
|
|
1138
|
-
]
|
|
1246
|
+
];
|
|
1247
|
+
// Only signal pr_merged_remotely when GitHub
|
|
1248
|
+
// confirmed MERGED. CLOSED-not-merged is a rejection path; leaving
|
|
1249
|
+
// cleanup behavior unchanged there is intentional (spec §11 R3).
|
|
1250
|
+
if (parsed.state === 'MERGED') {
|
|
1251
|
+
closeArgs.push('--pr-merged-remotely');
|
|
1252
|
+
}
|
|
1253
|
+
const closeRes = spawnSync('node', closeArgs, {
|
|
1254
|
+
cwd: targetDir,
|
|
1255
|
+
encoding: 'utf-8',
|
|
1256
|
+
env: { ...process.env },
|
|
1257
|
+
});
|
|
1139
1258
|
if (closeRes.status !== 0) {
|
|
1140
1259
|
errors.push({
|
|
1141
1260
|
id,
|
|
@@ -1185,6 +1304,33 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1185
1304
|
verdict: closeVerdict,
|
|
1186
1305
|
mergedAt: parsed.mergedAt ?? null,
|
|
1187
1306
|
});
|
|
1307
|
+
// Append a pending_narration[] ledger entry on successful merged close
|
|
1308
|
+
// so /apt:close-task --narrate-only can drain it later. The ledger is
|
|
1309
|
+
// additive-only; failure to write must never undo the close above
|
|
1310
|
+
// (R4 ledger leak — caller can manually invoke the narrator).
|
|
1311
|
+
if (closeVerdict === 'approved') {
|
|
1312
|
+
try {
|
|
1313
|
+
withFileLock(statePath, () => {
|
|
1314
|
+
const fresh = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
1315
|
+
if (!Array.isArray(fresh.pending_narration))
|
|
1316
|
+
fresh.pending_narration = [];
|
|
1317
|
+
fresh.pending_narration.push({
|
|
1318
|
+
task_id: id,
|
|
1319
|
+
scope: task.scope || 'project',
|
|
1320
|
+
phase_id: task.phase_id ?? null,
|
|
1321
|
+
closed_at: new Date().toISOString(),
|
|
1322
|
+
pr_number: parsed.number ?? null,
|
|
1323
|
+
});
|
|
1324
|
+
atomicWriteJson(statePath, fresh);
|
|
1325
|
+
}, { projectRoot: targetDir });
|
|
1326
|
+
}
|
|
1327
|
+
catch (e) {
|
|
1328
|
+
errors.push({
|
|
1329
|
+
id,
|
|
1330
|
+
reason: `ledger-write-failed: ${e.message ?? String(e)}`,
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1188
1334
|
}
|
|
1189
1335
|
return ok({
|
|
1190
1336
|
status: 'ok',
|
|
@@ -1195,6 +1341,44 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1195
1341
|
errors,
|
|
1196
1342
|
});
|
|
1197
1343
|
}
|
|
1344
|
+
case 'narration-drain': {
|
|
1345
|
+
// Remove a single pending_narration[] row under the same withFileLock
|
|
1346
|
+
// guard task close uses. Caller (apt:close-task --narrate-only) invokes
|
|
1347
|
+
// this once per drained entry AFTER its background narrator spawn
|
|
1348
|
+
// returns — so a spawn failure leaves the row in place for retry.
|
|
1349
|
+
const taskId = flags.get('task');
|
|
1350
|
+
if (!taskId)
|
|
1351
|
+
return err('Required: --task <task-id>');
|
|
1352
|
+
if (!existsSync(statePath)) {
|
|
1353
|
+
return ok({
|
|
1354
|
+
status: 'ok',
|
|
1355
|
+
command: 'task-narration-drain',
|
|
1356
|
+
removed: null,
|
|
1357
|
+
remaining: 0,
|
|
1358
|
+
reason: 'no-state',
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
let removed = null;
|
|
1362
|
+
let remaining = 0;
|
|
1363
|
+
withFileLock(statePath, () => {
|
|
1364
|
+
const state = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
1365
|
+
const ledger = Array.isArray(state.pending_narration) ? state.pending_narration : [];
|
|
1366
|
+
const idx = ledger.findIndex((row) => row && row.task_id === taskId);
|
|
1367
|
+
if (idx >= 0) {
|
|
1368
|
+
removed = ledger[idx];
|
|
1369
|
+
ledger.splice(idx, 1);
|
|
1370
|
+
state.pending_narration = ledger;
|
|
1371
|
+
atomicWriteJson(statePath, state);
|
|
1372
|
+
}
|
|
1373
|
+
remaining = ledger.length;
|
|
1374
|
+
}, { projectRoot: targetDir });
|
|
1375
|
+
return ok({
|
|
1376
|
+
status: 'ok',
|
|
1377
|
+
command: 'task-narration-drain',
|
|
1378
|
+
removed,
|
|
1379
|
+
remaining,
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1198
1382
|
case 'prune': {
|
|
1199
1383
|
// C56 A4 — retention verb. Removes `.aperant/tasks/{id}/` directories
|
|
1200
1384
|
// whose task row is in `state.completed_tasks[]` AND whose completion
|
|
@@ -1219,7 +1403,7 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1219
1403
|
const pruned = [];
|
|
1220
1404
|
const skipped = [];
|
|
1221
1405
|
for (const row of state.completed_tasks || []) {
|
|
1222
|
-
if (!row
|
|
1406
|
+
if (!row?.id)
|
|
1223
1407
|
continue;
|
|
1224
1408
|
const rowScope = row.scope || 'project';
|
|
1225
1409
|
if (scopeFilter && rowScope !== scopeFilter) {
|
|
@@ -1290,8 +1474,72 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
|
|
|
1290
1474
|
});
|
|
1291
1475
|
}
|
|
1292
1476
|
default:
|
|
1293
|
-
return err(`Unknown task subcommand: ${subcommand}. Expected: create, list, get, close, update, move, complete-subtask, close-merged, prune`);
|
|
1477
|
+
return err(`Unknown task subcommand: ${subcommand}. Expected: create, list, get, close, update, move, complete-subtask, close-merged, narration-drain, prune`);
|
|
1478
|
+
}
|
|
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
|
+
};
|
|
1294
1537
|
}
|
|
1538
|
+
return {
|
|
1539
|
+
ci_watch_armed: false,
|
|
1540
|
+
ci_watch_skip_reason: envelope.reason || 'start-failed',
|
|
1541
|
+
ci_watch: envelope,
|
|
1542
|
+
};
|
|
1295
1543
|
}
|
|
1296
1544
|
/**
|
|
1297
1545
|
* Extract a PR number from a GitHub URL like https://github.com/owner/repo/pull/123
|