@planu/cli 4.11.7 ā 4.11.8
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 +10 -0
- package/dist/cli/commands/serve.js +4 -0
- package/dist/config/license-plans.json +1 -0
- package/dist/engine/browser-validator.js +26 -21
- package/dist/engine/crash-shield/file-collector.d.ts +20 -3
- package/dist/engine/crash-shield/file-collector.js +137 -8
- package/dist/engine/crash-shield/index.d.ts +18 -1
- package/dist/engine/crash-shield/index.js +58 -17
- package/dist/engine/dogfooding/runtime-gap-detector.d.ts +3 -0
- package/dist/engine/dogfooding/runtime-gap-detector.js +386 -0
- package/dist/engine/figma/visual-qa.d.ts +2 -1
- package/dist/engine/figma/visual-qa.js +8 -7
- package/dist/engine/qa-gate.js +2 -1
- package/dist/engine/spec-state-machine/transition-spec.d.ts +16 -1
- package/dist/engine/spec-state-machine/transition-spec.js +19 -4
- package/dist/engine/triagier/classifier.d.ts +2 -2
- package/dist/engine/triagier/classifier.js +12 -15
- package/dist/index.js +12 -4
- package/dist/storage/approval-operation-lock.d.ts +10 -0
- package/dist/storage/approval-operation-lock.js +44 -0
- package/dist/storage/approval-store.d.ts +2 -0
- package/dist/storage/approval-store.js +9 -1
- package/dist/storage/spec-store.d.ts +29 -2
- package/dist/storage/spec-store.js +307 -7
- package/dist/tools/approval-handler.js +255 -124
- package/dist/tools/browser-validate-handler.js +17 -3
- package/dist/tools/dogfood-watch.d.ts +6 -0
- package/dist/tools/dogfood-watch.js +48 -0
- package/dist/tools/figma/visual-qa.js +2 -1
- package/dist/tools/tool-registry/core-tools.js +12 -0
- package/dist/tools/tool-registry/group-quality-compliance.js +12 -1
- package/dist/tools/update-status/file-sync.js +3 -2
- package/dist/tools/update-status/index.d.ts +2 -0
- package/dist/tools/update-status/index.js +1083 -821
- package/dist/tools/update-status/response-builder.js +11 -0
- package/dist/tools/update-status/side-effects.d.ts +16 -1
- package/dist/tools/update-status/side-effects.js +140 -0
- package/dist/tools/update-status/transition-guard.js +1 -1
- package/dist/tools/update-status-actions.d.ts +10 -2
- package/dist/tools/update-status-actions.js +166 -192
- package/dist/tools/update-status-convention-gate.d.ts +3 -1
- package/dist/tools/update-status-convention-gate.js +135 -7
- package/dist/types/browser-validator.d.ts +2 -0
- package/dist/types/dogfooding.d.ts +34 -0
- package/dist/types/dogfooding.js +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/spec/core.d.ts +28 -1
- package/package.json +25 -25
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
|
@@ -86,6 +86,12 @@ export function buildStatusResponse(result, specId, currentStatus, newStatus, re
|
|
|
86
86
|
const lines = [
|
|
87
87
|
`**SPEC-${specId}** moved: ${fromEmoji} \`${currentStatus}\` ā ${toEmoji} \`${newStatus}\``,
|
|
88
88
|
];
|
|
89
|
+
if (r.committed && r.transitionId) {
|
|
90
|
+
lines.push(`\nTransition committed: \`${r.transitionId}\``);
|
|
91
|
+
}
|
|
92
|
+
if (r.pendingBackgroundActions && r.pendingBackgroundActions.length > 0) {
|
|
93
|
+
lines.push(`\nBackground actions queued: ${r.pendingBackgroundActions.join(', ')}`);
|
|
94
|
+
}
|
|
89
95
|
if (r.autoBranch) {
|
|
90
96
|
lines.push(`\nšæ Branch created: \`${r.autoBranch}\``);
|
|
91
97
|
}
|
|
@@ -221,6 +227,11 @@ export function buildStatusResponse(result, specId, currentStatus, newStatus, re
|
|
|
221
227
|
specId,
|
|
222
228
|
previousStatus: currentStatus,
|
|
223
229
|
newStatus,
|
|
230
|
+
committed: r.committed ?? undefined,
|
|
231
|
+
transitionId: r.transitionId ?? undefined,
|
|
232
|
+
committedAt: r.committedAt ?? undefined,
|
|
233
|
+
idempotent: r.idempotent ?? undefined,
|
|
234
|
+
pendingBackgroundActions: r.pendingBackgroundActions ?? undefined,
|
|
224
235
|
autoBranch: r.autoBranch ?? undefined,
|
|
225
236
|
protectedBranchWarning: r.protectedBranchWarning ?? undefined,
|
|
226
237
|
mergeWarning: r.mergeWarning ?? undefined,
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import type { SpecStatus } from '../../types/index.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type knowledgeStore } from '../../storage/index.js';
|
|
3
3
|
import type { Spec } from '../../types/spec/core.js';
|
|
4
4
|
import type { RunCascadeResult } from '../../types/cascade-hooks.js';
|
|
5
|
+
export interface PostCommitTask {
|
|
6
|
+
name: string;
|
|
7
|
+
run: (signal?: AbortSignal) => Promise<unknown>;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Queue optional work after a durable transition. Returns immediately and
|
|
12
|
+
* deduplicates retries by transition identity for the lifetime of the process.
|
|
13
|
+
*/
|
|
14
|
+
export declare function queuePostCommitTasks(args: {
|
|
15
|
+
projectId: string;
|
|
16
|
+
specId: string;
|
|
17
|
+
transitionId: string;
|
|
18
|
+
tasks: PostCommitTask[];
|
|
19
|
+
}): string[];
|
|
5
20
|
/**
|
|
6
21
|
* SPEC-585: Autopush on done if configured.
|
|
7
22
|
* Returns the push result for inclusion in autopilotSummary (or null when disabled/skipped).
|
|
@@ -1,4 +1,144 @@
|
|
|
1
|
+
import { specStore } from '../../storage/index.js';
|
|
1
2
|
import { runCascade } from '../../engine/cascade-hooks/runner.js';
|
|
3
|
+
import { appendAutopilotLogEntry } from '../../storage/autopilot-log-store.js';
|
|
4
|
+
const DEFAULT_POST_COMMIT_TASK_TIMEOUT_MS = 10_000;
|
|
5
|
+
const MIN_POST_COMMIT_LEASE_MS = 30_000;
|
|
6
|
+
const MAX_DEDUPED_TRANSITIONS = 1_000;
|
|
7
|
+
const queuedTransitions = new Map();
|
|
8
|
+
function rememberTransition(transitionKey) {
|
|
9
|
+
if (queuedTransitions.has(transitionKey)) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
if (queuedTransitions.size >= MAX_DEDUPED_TRANSITIONS) {
|
|
13
|
+
const oldest = queuedTransitions.keys().next().value;
|
|
14
|
+
if (oldest !== undefined) {
|
|
15
|
+
queuedTransitions.delete(oldest);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
queuedTransitions.set(transitionKey, Date.now());
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
async function runBoundedPostCommitTask(args) {
|
|
22
|
+
const { projectId, specId, transitionId, task } = args;
|
|
23
|
+
const timeoutMs = task.timeoutMs ?? DEFAULT_POST_COMMIT_TASK_TIMEOUT_MS;
|
|
24
|
+
const leaseMs = Math.max(MIN_POST_COMMIT_LEASE_MS, timeoutMs * 3);
|
|
25
|
+
const claim = await specStore.claimPostCommitTask(projectId, specId, transitionId, task.name, {
|
|
26
|
+
leaseMs,
|
|
27
|
+
});
|
|
28
|
+
if (!claim.claimed) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const startedAt = Date.now();
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
let timer;
|
|
34
|
+
let heartbeat;
|
|
35
|
+
try {
|
|
36
|
+
heartbeat = setInterval(() => {
|
|
37
|
+
void specStore
|
|
38
|
+
.renewPostCommitTaskLease(projectId, specId, transitionId, task.name, claim.executionId, {
|
|
39
|
+
leaseMs,
|
|
40
|
+
})
|
|
41
|
+
.catch((error) => {
|
|
42
|
+
console.warn('[planu:post-commit] failed to renew task lease', {
|
|
43
|
+
specId,
|
|
44
|
+
transitionId,
|
|
45
|
+
task: task.name,
|
|
46
|
+
error: error instanceof Error ? error.message : String(error),
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}, Math.max(1_000, Math.floor(leaseMs / 3)));
|
|
50
|
+
heartbeat.unref();
|
|
51
|
+
const runPromise = Promise.resolve().then(() => task.run(controller.signal));
|
|
52
|
+
const timeout = new Promise((resolve) => {
|
|
53
|
+
timer = setTimeout(() => {
|
|
54
|
+
controller.abort(new Error(`Timed out after ${String(timeoutMs)}ms`));
|
|
55
|
+
resolve('timeout');
|
|
56
|
+
}, timeoutMs);
|
|
57
|
+
timer.unref();
|
|
58
|
+
});
|
|
59
|
+
const firstResult = await Promise.race([runPromise.then(() => 'completed'), timeout]);
|
|
60
|
+
if (firstResult === 'timeout') {
|
|
61
|
+
await runPromise;
|
|
62
|
+
await specStore.settlePostCommitTask(projectId, specId, transitionId, task.name, claim.executionId, { status: 'done' });
|
|
63
|
+
try {
|
|
64
|
+
await appendAutopilotLogEntry(projectId, {
|
|
65
|
+
specId,
|
|
66
|
+
hookName: `post-commit:${task.name}:${transitionId}`,
|
|
67
|
+
result: 'fail',
|
|
68
|
+
error: `Timed out after ${String(timeoutMs)}ms, then completed successfully; retry suppressed.`,
|
|
69
|
+
timestamp: new Date().toISOString(),
|
|
70
|
+
durationMs: Date.now() - startedAt,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (logError) {
|
|
74
|
+
console.warn('[planu:post-commit] failed to record late task completion', {
|
|
75
|
+
specId,
|
|
76
|
+
transitionId,
|
|
77
|
+
task: task.name,
|
|
78
|
+
error: logError instanceof Error ? logError.message : String(logError),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
await specStore.settlePostCommitTask(projectId, specId, transitionId, task.name, claim.executionId, { status: 'done' });
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
87
|
+
await specStore
|
|
88
|
+
.settlePostCommitTask(projectId, specId, transitionId, task.name, claim.executionId, {
|
|
89
|
+
status: 'failed',
|
|
90
|
+
error: message,
|
|
91
|
+
})
|
|
92
|
+
.catch((settleError) => {
|
|
93
|
+
console.warn('[planu:post-commit] failed to persist task failure', {
|
|
94
|
+
specId,
|
|
95
|
+
transitionId,
|
|
96
|
+
task: task.name,
|
|
97
|
+
error: settleError instanceof Error ? settleError.message : String(settleError),
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
await appendAutopilotLogEntry(projectId, {
|
|
102
|
+
specId,
|
|
103
|
+
hookName: `post-commit:${task.name}:${transitionId}`,
|
|
104
|
+
result: 'fail',
|
|
105
|
+
error: message,
|
|
106
|
+
timestamp: new Date().toISOString(),
|
|
107
|
+
durationMs: Date.now() - startedAt,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (logError) {
|
|
111
|
+
console.warn('[planu:post-commit] failed to record task failure', {
|
|
112
|
+
specId,
|
|
113
|
+
transitionId,
|
|
114
|
+
task: task.name,
|
|
115
|
+
error: logError instanceof Error ? logError.message : String(logError),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
if (timer !== undefined) {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
}
|
|
123
|
+
if (heartbeat !== undefined) {
|
|
124
|
+
clearInterval(heartbeat);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Queue optional work after a durable transition. Returns immediately and
|
|
130
|
+
* deduplicates retries by transition identity for the lifetime of the process.
|
|
131
|
+
*/
|
|
132
|
+
export function queuePostCommitTasks(args) {
|
|
133
|
+
const transitionKey = `${args.projectId}\0${args.specId}\0${args.transitionId}`;
|
|
134
|
+
if (!rememberTransition(transitionKey)) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
void Promise.allSettled(args.tasks.map((task) => runBoundedPostCommitTask({ ...args, task }))).finally(() => {
|
|
138
|
+
queuedTransitions.delete(transitionKey);
|
|
139
|
+
});
|
|
140
|
+
return args.tasks.map((task) => task.name);
|
|
141
|
+
}
|
|
2
142
|
/**
|
|
3
143
|
* SPEC-585: Autopush on done if configured.
|
|
4
144
|
* Returns the push result for inclusion in autopilotSummary (or null when disabled/skipped).
|
|
@@ -21,7 +21,7 @@ import { getResolvedChallengeEvidence, requiredResolvedChallenges, } from '../ch
|
|
|
21
21
|
export const VALID_TRANSITIONS = {
|
|
22
22
|
draft: ['review', 'discarded'],
|
|
23
23
|
review: ['draft', 'approved', 'discarded'],
|
|
24
|
-
approved: ['review', 'implementing', 'discarded'],
|
|
24
|
+
approved: ['draft', 'review', 'implementing', 'discarded'],
|
|
25
25
|
implementing: ['approved', 'done', 'discarded'],
|
|
26
26
|
done: ['implementing'], // SPEC-733: reverse transition with mandatory reason
|
|
27
27
|
discarded: ['draft'], // SPEC-733: reverse transition with mandatory reason
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { ConstitutionViolation } from '../types/index.js';
|
|
2
|
-
export declare function runImplementingActions(projectId: string, specId: string
|
|
2
|
+
export declare function runImplementingActions(projectId: string, specId: string, options?: {
|
|
3
|
+
deferSideEffects?: boolean;
|
|
4
|
+
}): Promise<{
|
|
3
5
|
autoBranch: string | undefined;
|
|
4
6
|
protectedBranchWarning: string | null;
|
|
5
7
|
suggestions: string[];
|
|
6
8
|
autopilotSummary: string[];
|
|
7
9
|
}>;
|
|
8
|
-
|
|
10
|
+
/** Create the implementation branch and refresh derived context after commit. */
|
|
11
|
+
export declare function runImplementingSideEffects(projectId: string, specId: string): Promise<string | undefined>;
|
|
12
|
+
export declare function runDoneActions(projectId: string, specId: string, gitBranch: string | undefined, options?: {
|
|
13
|
+
deferSideEffects?: boolean;
|
|
14
|
+
}): Promise<{
|
|
9
15
|
mergeWarning: string | null;
|
|
10
16
|
prSuggestion: {
|
|
11
17
|
title: string;
|
|
@@ -15,6 +21,8 @@ export declare function runDoneActions(projectId: string, specId: string, gitBra
|
|
|
15
21
|
uncommittedWarning: string | null;
|
|
16
22
|
autopilotSummary: string[];
|
|
17
23
|
}>;
|
|
24
|
+
/** Run cleanup and state refreshes only after the done transition is durable. */
|
|
25
|
+
export declare function runDoneSideEffects(projectId: string, specId: string, gitBranch: string | undefined): Promise<void>;
|
|
18
26
|
/**
|
|
19
27
|
* Best-effort constitution compliance check for status transitions.
|
|
20
28
|
* Returns warnings (never blocks the transition).
|
|
@@ -2,7 +2,13 @@ import { specStore, knowledgeStore } from '../storage/index.js';
|
|
|
2
2
|
import { checkConstitutionCompliance } from './create-spec-tech.js';
|
|
3
3
|
import { withAudit } from '../engine/autopilot/audit-logger.js';
|
|
4
4
|
import { hasPending, markPending, clearPending } from '../engine/autopilot/cascade-deduplicator.js';
|
|
5
|
-
|
|
5
|
+
function normalizeRejectedReasons(results) {
|
|
6
|
+
return results.map((result) => {
|
|
7
|
+
const reason = result.reason;
|
|
8
|
+
return reason instanceof Error ? reason : new Error(String(reason));
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export async function runImplementingActions(projectId, specId, options = {}) {
|
|
6
12
|
let autoBranch;
|
|
7
13
|
let protectedBranchWarning = null;
|
|
8
14
|
// Check if on a protected branch
|
|
@@ -19,7 +25,24 @@ export async function runImplementingActions(projectId, specId) {
|
|
|
19
25
|
catch {
|
|
20
26
|
// Not a git repo ā skip check
|
|
21
27
|
}
|
|
22
|
-
|
|
28
|
+
if (!options.deferSideEffects) {
|
|
29
|
+
try {
|
|
30
|
+
autoBranch = await runImplementingSideEffects(projectId, specId);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Legacy direct callers keep best-effort behavior.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
autoBranch,
|
|
38
|
+
protectedBranchWarning,
|
|
39
|
+
suggestions: [],
|
|
40
|
+
autopilotSummary: [],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Create the implementation branch and refresh derived context after commit. */
|
|
44
|
+
export async function runImplementingSideEffects(projectId, specId) {
|
|
45
|
+
let autoBranch;
|
|
23
46
|
const foundSpec = await specStore.getSpec(projectId, specId);
|
|
24
47
|
if (foundSpec && !foundSpec.gitBranch) {
|
|
25
48
|
let branchResult;
|
|
@@ -46,98 +69,89 @@ export async function runImplementingActions(projectId, specId) {
|
|
|
46
69
|
// -------------------------------------------------------------------------
|
|
47
70
|
// SPEC-600: Fire-and-forget cascade ā recommend_model, orchestration plan, context check
|
|
48
71
|
// -------------------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
(
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
try {
|
|
73
|
+
const { resolveProjectPath: resolveCascadePath } = await import('./git/git-helpers.js');
|
|
74
|
+
const projectPath = await resolveCascadePath(projectId);
|
|
75
|
+
const spec = foundSpec ?? (await specStore.getSpec(projectId, specId));
|
|
76
|
+
const results = await Promise.allSettled([
|
|
77
|
+
// SPEC-633: Write session state when spec moves to implementing
|
|
78
|
+
(async () => {
|
|
79
|
+
const { writeSpecSessionState } = await import('../engine/session-state/writer.js');
|
|
80
|
+
await writeSpecSessionState(projectPath, specId, spec?.title ?? specId, 'moved to implementing').catch(() => {
|
|
81
|
+
/* best-effort */
|
|
82
|
+
});
|
|
83
|
+
})(),
|
|
84
|
+
// SPEC-635: Regenerate context.md
|
|
85
|
+
(async () => {
|
|
86
|
+
const { generateContextMd } = await import('../engine/context-md-generator.js');
|
|
87
|
+
await generateContextMd(projectPath, projectId).catch(() => {
|
|
88
|
+
/* best-effort */
|
|
89
|
+
});
|
|
90
|
+
})(),
|
|
91
|
+
// SPEC-656: Recalculate epicProgress when spec moves to implementing
|
|
92
|
+
(async () => {
|
|
93
|
+
try {
|
|
94
|
+
const { updateEpicProgressInSession } = await import('../engine/session-state/writer.js');
|
|
95
|
+
const allSpecs = await specStore.listSpecs(projectId);
|
|
96
|
+
await updateEpicProgressInSession(projectPath, allSpecs).catch(() => {
|
|
67
97
|
/* best-effort */
|
|
68
98
|
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
/* best-effort */
|
|
102
|
+
}
|
|
103
|
+
})(),
|
|
104
|
+
// recommend_model ā suggest haiku/sonnet/opus based on spec complexity
|
|
105
|
+
(async () => {
|
|
106
|
+
try {
|
|
107
|
+
const { handleRecommendModel } = await import('./recommend-model-handler.js');
|
|
108
|
+
const result = handleRecommendModel({ specId });
|
|
109
|
+
void result;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
/* best-effort */
|
|
113
|
+
}
|
|
114
|
+
})(),
|
|
115
|
+
// generate_orchestration_plan ā if cross-module or high difficulty
|
|
116
|
+
(async () => {
|
|
117
|
+
try {
|
|
118
|
+
const scope = spec?.scope ?? '';
|
|
119
|
+
const difficulty = spec?.difficulty ?? 1;
|
|
120
|
+
if (scope === 'cross-module' || difficulty >= 3) {
|
|
121
|
+
const { handleGenerateOrchestrationScript } = await import('./generate-orchestration-script.js');
|
|
122
|
+
handleGenerateOrchestrationScript({
|
|
123
|
+
specIds: [specId],
|
|
124
|
+
projectPath,
|
|
77
125
|
});
|
|
78
126
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const scope = spec?.scope ?? '';
|
|
100
|
-
const difficulty = spec?.difficulty ?? 1;
|
|
101
|
-
if (scope === 'cross-module' || difficulty >= 3) {
|
|
102
|
-
const { handleGenerateOrchestrationScript } = await import('./generate-orchestration-script.js');
|
|
103
|
-
handleGenerateOrchestrationScript({
|
|
104
|
-
specIds: [specId],
|
|
105
|
-
projectPath,
|
|
106
|
-
});
|
|
107
|
-
autopilotSummary.push('generate_orchestration_plan: auto-generated');
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
/* best-effort */
|
|
112
|
-
}
|
|
113
|
-
})(),
|
|
114
|
-
// context_window_status ā check context weight and warn
|
|
115
|
-
(async () => {
|
|
116
|
-
try {
|
|
117
|
-
const { handleContextWindowStatus } = await import('./context-manager-handler.js');
|
|
118
|
-
const ctxResult = await handleContextWindowStatus({ projectPath });
|
|
119
|
-
if (!ctxResult.isError) {
|
|
120
|
-
autopilotSummary.push('context_window_status: checked');
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
/* best-effort */
|
|
125
|
-
}
|
|
126
|
-
})(),
|
|
127
|
-
]);
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
// Project path unavailable ā skip cascade silently
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
/* best-effort */
|
|
130
|
+
}
|
|
131
|
+
})(),
|
|
132
|
+
// context_window_status ā check context weight and warn
|
|
133
|
+
(async () => {
|
|
134
|
+
try {
|
|
135
|
+
const { handleContextWindowStatus } = await import('./context-manager-handler.js');
|
|
136
|
+
const ctxResult = await handleContextWindowStatus({ projectPath });
|
|
137
|
+
void ctxResult;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
/* best-effort */
|
|
141
|
+
}
|
|
142
|
+
})(),
|
|
143
|
+
]);
|
|
144
|
+
const failures = results.filter((result) => result.status === 'rejected');
|
|
145
|
+
if (failures.length > 0) {
|
|
146
|
+
throw new AggregateError(normalizeRejectedReasons(failures), `${String(failures.length)} implementing side effect(s) failed`);
|
|
131
147
|
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
autopilotSummary,
|
|
138
|
-
};
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
151
|
+
}
|
|
152
|
+
return autoBranch;
|
|
139
153
|
}
|
|
140
|
-
export async function runDoneActions(projectId, specId, gitBranch) {
|
|
154
|
+
export async function runDoneActions(projectId, specId, gitBranch, options = {}) {
|
|
141
155
|
// -------------------------------------------------------------------------
|
|
142
156
|
// SPEC-628: Group A ā fast synchronous checks only (< 100ms each)
|
|
143
157
|
// Validation removed: already done by runValidateGate in index.ts (BATCH B)
|
|
@@ -198,112 +212,15 @@ export async function runDoneActions(projectId, specId, gitBranch) {
|
|
|
198
212
|
})());
|
|
199
213
|
mergeWarning = mergeResult;
|
|
200
214
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const { resolveProjectPath } = await import('./git/git-helpers.js');
|
|
207
|
-
const projectPath = await resolveProjectPath(projectId);
|
|
208
|
-
// AC-01: Cleanup worktree, local branch, and remote branch for done spec (best-effort)
|
|
209
|
-
// SPEC-491: Sweep planu/ to remove legacy files
|
|
210
|
-
// SPEC-496: Auto-generate portable session context snapshot
|
|
211
|
-
void Promise.allSettled([
|
|
212
|
-
(async () => {
|
|
213
|
-
const { cleanupSpecOnDone } = await import('./git/cleanup-ops.js');
|
|
214
|
-
await withAudit(projectPath, 'update_status(done)', 'cleanupSpecOnDone', () => cleanupSpecOnDone(projectPath, specId, gitBranch)).catch(() => {
|
|
215
|
-
/* best-effort ā never blocks status transition */
|
|
216
|
-
});
|
|
217
|
-
})(),
|
|
218
|
-
// SPEC-633: Remove spec from session.json index when done
|
|
219
|
-
(async () => {
|
|
220
|
-
const { removeSpecFromSession } = await import('../engine/session-state/writer.js');
|
|
221
|
-
await removeSpecFromSession(projectPath, specId).catch(() => {
|
|
222
|
-
/* best-effort */
|
|
223
|
-
});
|
|
224
|
-
})(),
|
|
225
|
-
// SPEC-656: Recalculate epicProgress after status change
|
|
226
|
-
(async () => {
|
|
227
|
-
try {
|
|
228
|
-
const { updateEpicProgressInSession } = await import('../engine/session-state/writer.js');
|
|
229
|
-
const allSpecs = await specStore.listSpecs(projectId);
|
|
230
|
-
await updateEpicProgressInSession(projectPath, allSpecs).catch(() => {
|
|
231
|
-
/* best-effort */
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
catch {
|
|
235
|
-
/* best-effort */
|
|
236
|
-
}
|
|
237
|
-
})(),
|
|
238
|
-
// SPEC-635: Regenerate context.md after status change
|
|
239
|
-
(async () => {
|
|
240
|
-
const { generateContextMd } = await import('../engine/context-md-generator.js');
|
|
241
|
-
await generateContextMd(projectPath, projectId).catch(() => {
|
|
242
|
-
/* best-effort */
|
|
243
|
-
});
|
|
244
|
-
})(),
|
|
245
|
-
(async () => {
|
|
246
|
-
const { join } = await import('node:path');
|
|
247
|
-
const { cleanPlanuRoot } = await import('../engine/spec-migrator/planu-root-cleaner.js');
|
|
248
|
-
await cleanPlanuRoot(join(projectPath, 'planu')).catch(() => {
|
|
249
|
-
/* best-effort */
|
|
250
|
-
});
|
|
251
|
-
})(),
|
|
252
|
-
// SPEC-1002: Auto-cleanup stale branches/worktrees when spec reaches done
|
|
253
|
-
(async () => {
|
|
254
|
-
try {
|
|
255
|
-
const { runHousekeepingSweep } = await import('../engine/housekeeping/index.js');
|
|
256
|
-
await runHousekeepingSweep({ projectPath, dryRun: false }).catch(() => {
|
|
257
|
-
/* best-effort ā never blocks status transition */
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
catch {
|
|
261
|
-
/* best-effort */
|
|
262
|
-
}
|
|
263
|
-
})(),
|
|
264
|
-
// SPEC-660: Dedup ā session.json update runs once per spec within 5s window
|
|
265
|
-
(async () => {
|
|
266
|
-
if (hasPending(specId, 'generateSessionContext')) {
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
markPending(specId, 'generateSessionContext');
|
|
270
|
-
try {
|
|
271
|
-
const { generateSessionContext } = await import('../engine/session-context-generator.js');
|
|
272
|
-
await generateSessionContext(projectPath, projectId).catch(() => {
|
|
273
|
-
/* best-effort ā never blocks status transition */
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
finally {
|
|
277
|
-
clearPending(specId, 'generateSessionContext');
|
|
278
|
-
}
|
|
279
|
-
})(),
|
|
280
|
-
// SPEC-629: Delete ephemeral prompt.md ā file served its purpose once implementing starts
|
|
281
|
-
(async () => {
|
|
282
|
-
/* v8 ignore start */
|
|
283
|
-
try {
|
|
284
|
-
const { glob } = await import('glob');
|
|
285
|
-
const { unlink } = await import('node:fs/promises');
|
|
286
|
-
const specFiles = await glob(`planu/specs/${specId}-*/prompt.md`, { cwd: projectPath, absolute: true });
|
|
287
|
-
const exactFile = await glob(`planu/specs/${specId}/prompt.md`, { cwd: projectPath, absolute: true });
|
|
288
|
-
await Promise.allSettled([...specFiles, ...exactFile].map((f) => unlink(f).catch(() => {
|
|
289
|
-
/* best-effort */
|
|
290
|
-
})));
|
|
291
|
-
}
|
|
292
|
-
catch {
|
|
293
|
-
/* best-effort ā never blocks status transition */
|
|
294
|
-
}
|
|
295
|
-
/* v8 ignore stop */
|
|
296
|
-
})(),
|
|
297
|
-
]);
|
|
298
|
-
}
|
|
299
|
-
catch {
|
|
300
|
-
// Project path unavailable ā skip all fire-and-forget cleanup silently
|
|
301
|
-
}
|
|
302
|
-
})();
|
|
215
|
+
if (!options.deferSideEffects) {
|
|
216
|
+
void runDoneSideEffects(projectId, specId, gitBranch).catch(() => {
|
|
217
|
+
// Legacy direct callers keep best-effort behavior.
|
|
218
|
+
});
|
|
219
|
+
}
|
|
303
220
|
// SPEC-649: slim cascade ā 2 actions only
|
|
304
221
|
const autopilotSummary = [
|
|
305
|
-
'session.json:
|
|
306
|
-
'releases/pending.json:
|
|
222
|
+
'session.json: refresh queued',
|
|
223
|
+
'releases/pending.json: refresh queued',
|
|
307
224
|
];
|
|
308
225
|
return {
|
|
309
226
|
mergeWarning,
|
|
@@ -313,6 +230,63 @@ export async function runDoneActions(projectId, specId, gitBranch) {
|
|
|
313
230
|
autopilotSummary,
|
|
314
231
|
};
|
|
315
232
|
}
|
|
233
|
+
/** Run cleanup and state refreshes only after the done transition is durable. */
|
|
234
|
+
export async function runDoneSideEffects(projectId, specId, gitBranch) {
|
|
235
|
+
const { resolveProjectPath } = await import('./git/git-helpers.js');
|
|
236
|
+
const projectPath = await resolveProjectPath(projectId);
|
|
237
|
+
const results = await Promise.allSettled([
|
|
238
|
+
(async () => {
|
|
239
|
+
const { cleanupSpecOnDone } = await import('./git/cleanup-ops.js');
|
|
240
|
+
await withAudit(projectPath, 'update_status(done)', 'cleanupSpecOnDone', () => cleanupSpecOnDone(projectPath, specId, gitBranch));
|
|
241
|
+
})(),
|
|
242
|
+
(async () => {
|
|
243
|
+
const { removeSpecFromSession } = await import('../engine/session-state/writer.js');
|
|
244
|
+
await removeSpecFromSession(projectPath, specId);
|
|
245
|
+
})(),
|
|
246
|
+
(async () => {
|
|
247
|
+
const { updateEpicProgressInSession } = await import('../engine/session-state/writer.js');
|
|
248
|
+
const allSpecs = await specStore.listSpecs(projectId);
|
|
249
|
+
await updateEpicProgressInSession(projectPath, allSpecs);
|
|
250
|
+
})(),
|
|
251
|
+
(async () => {
|
|
252
|
+
const { generateContextMd } = await import('../engine/context-md-generator.js');
|
|
253
|
+
await generateContextMd(projectPath, projectId);
|
|
254
|
+
})(),
|
|
255
|
+
(async () => {
|
|
256
|
+
const { join } = await import('node:path');
|
|
257
|
+
const { cleanPlanuRoot } = await import('../engine/spec-migrator/planu-root-cleaner.js');
|
|
258
|
+
await cleanPlanuRoot(join(projectPath, 'planu'));
|
|
259
|
+
})(),
|
|
260
|
+
(async () => {
|
|
261
|
+
const { runHousekeepingSweep } = await import('../engine/housekeeping/index.js');
|
|
262
|
+
await runHousekeepingSweep({ projectPath, dryRun: false });
|
|
263
|
+
})(),
|
|
264
|
+
(async () => {
|
|
265
|
+
if (hasPending(specId, 'generateSessionContext')) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
markPending(specId, 'generateSessionContext');
|
|
269
|
+
try {
|
|
270
|
+
const { generateSessionContext } = await import('../engine/session-context-generator.js');
|
|
271
|
+
await generateSessionContext(projectPath, projectId);
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
clearPending(specId, 'generateSessionContext');
|
|
275
|
+
}
|
|
276
|
+
})(),
|
|
277
|
+
(async () => {
|
|
278
|
+
const { glob } = await import('glob');
|
|
279
|
+
const { unlink } = await import('node:fs/promises');
|
|
280
|
+
const specFiles = await glob(`planu/specs/${specId}-*/prompt.md`, { cwd: projectPath, absolute: true });
|
|
281
|
+
const exactFiles = await glob(`planu/specs/${specId}/prompt.md`, { cwd: projectPath, absolute: true });
|
|
282
|
+
await Promise.allSettled([...specFiles, ...exactFiles].map((file) => unlink(file)));
|
|
283
|
+
})(),
|
|
284
|
+
]);
|
|
285
|
+
const failures = results.filter((result) => result.status === 'rejected');
|
|
286
|
+
if (failures.length > 0) {
|
|
287
|
+
throw new AggregateError(normalizeRejectedReasons(failures), `${String(failures.length)} done side effect(s) failed`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
316
290
|
/**
|
|
317
291
|
* Best-effort constitution compliance check for status transitions.
|
|
318
292
|
* Returns warnings (never blocks the transition).
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { SpecStatus, ComplianceGateResult } from '../types/index.js';
|
|
2
|
+
/** Test-only reset to prevent process-cache state leaking between cases. */
|
|
3
|
+
export declare function resetProcessValidationCacheForTests(): void;
|
|
2
4
|
/**
|
|
3
5
|
* Runs non-blocking transition checks not already covered by the authoritative validate report.
|
|
4
6
|
* in parallel. Extracts complexity from handleUpdateStatus.
|
|
5
7
|
*/
|
|
6
|
-
export declare function runComplianceGates(projectId: string, specTitle: string, specTags: string[], newStatus: SpecStatus): Promise<ComplianceGateResult>;
|
|
8
|
+
export declare function runComplianceGates(projectId: string, specTitle: string, specTags: string[], newStatus: SpecStatus, specId?: string): Promise<ComplianceGateResult>;
|
|
7
9
|
//# sourceMappingURL=update-status-convention-gate.d.ts.map
|