@agent-plan/core 0.2.19 → 0.2.21
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/dist/display-status.d.ts +5 -3
- package/dist/display-status.d.ts.map +1 -1
- package/dist/display-status.js +14 -5
- package/dist/plan-store.d.ts +11 -3
- package/dist/plan-store.d.ts.map +1 -1
- package/dist/plan-store.js +151 -22
- package/dist/recap.d.ts.map +1 -1
- package/dist/recap.js +63 -5
- package/dist/schema.d.ts +1159 -248
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +39 -7
- package/dist/task-selection.d.ts.map +1 -1
- package/dist/task-selection.js +30 -22
- package/package.json +1 -1
package/dist/display-status.d.ts
CHANGED
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
*
|
|
15
15
|
* These are NEVER persisted: they are computed on demand from children's
|
|
16
16
|
* canonical workflow statuses. The canonical workflow model
|
|
17
|
-
* (`planned | in-progress | waiting | blocked | deferred | done |
|
|
18
|
-
* rejected`)
|
|
17
|
+
* (`planned | in-progress | paused | waiting | blocked | deferred | done |
|
|
18
|
+
* canceled | rejected`) includes paused leaf work without implying active execution.
|
|
19
19
|
*/
|
|
20
20
|
import type { TaskStatus, PhaseStatus } from "./schema.js";
|
|
21
21
|
/**
|
|
22
22
|
* Canonical workflow status values used by tasks, phases, and features.
|
|
23
23
|
* Mirrors the union of TaskStatus and PhaseStatus used in the planner data.
|
|
24
24
|
*/
|
|
25
|
-
export type WorkflowStatus = "planned" | "in-progress" | "waiting" | "blocked" | "deferred" | "done" | "canceled" | "rejected";
|
|
25
|
+
export type WorkflowStatus = "planned" | "in-progress" | "paused" | "waiting" | "blocked" | "deferred" | "done" | "canceled" | "rejected";
|
|
26
26
|
/**
|
|
27
27
|
* Display status adds parent-only presentation states on top of workflow
|
|
28
28
|
* statuses. Only parent entities (feature, phase) use the full union; leaf
|
|
@@ -33,6 +33,7 @@ export type DisplayStatus = WorkflowStatus | "started" | "closed";
|
|
|
33
33
|
export interface StatusBreakdown {
|
|
34
34
|
planned: number;
|
|
35
35
|
inProgress: number;
|
|
36
|
+
paused: number;
|
|
36
37
|
waiting: number;
|
|
37
38
|
blocked: number;
|
|
38
39
|
deferred: number;
|
|
@@ -67,6 +68,7 @@ export declare function countBreakdown(statuses: readonly WorkflowStatus[]): Sta
|
|
|
67
68
|
* - all `rejected` → `rejected`
|
|
68
69
|
* - otherwise → `closed` (mixed terminal outcomes)
|
|
69
70
|
* 3. Compute `unfinished = meaningful ∩ OPEN`.
|
|
71
|
+
* - homogeneous `paused` → `paused`
|
|
70
72
|
* - homogeneous `waiting` → `waiting`
|
|
71
73
|
* - homogeneous `blocked` → `blocked`
|
|
72
74
|
* - homogeneous `deferred` → `deferred`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"display-status.d.ts","sourceRoot":"","sources":["../src/display-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE3D;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB,SAAS,GACT,aAAa,GACb,SAAS,GACT,SAAS,GACT,UAAU,GACV,MAAM,GACN,UAAU,GACV,UAAU,CAAC;AAEf;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,cAAc,GAAG,SAAS,GAAG,QAAQ,CAAC;AAElE,+CAA+C;AAC/C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,yDAAyD;AACzD,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,aAAa,EAAE,aAAa,CAAC;IAC7B,wDAAwD;IACxD,SAAS,EAAE,eAAe,CAAC;IAC3B;+CAC2C;IAC3C,UAAU,EAAE,OAAO,CAAC;IACpB,8DAA8D;IAC9D,aAAa,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAMD,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,GAAG,eAAe,
|
|
1
|
+
{"version":3,"file":"display-status.d.ts","sourceRoot":"","sources":["../src/display-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE3D;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB,SAAS,GACT,aAAa,GACb,QAAQ,GACR,SAAS,GACT,SAAS,GACT,UAAU,GACV,MAAM,GACN,UAAU,GACV,UAAU,CAAC;AAEf;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,cAAc,GAAG,SAAS,GAAG,QAAQ,CAAC;AAElE,+CAA+C;AAC/C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,yDAAyD;AACzD,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,aAAa,EAAE,aAAa,CAAC;IAC7B,wDAAwD;IACxD,SAAS,EAAE,eAAe,CAAC;IAC3B;+CAC2C;IAC3C,UAAU,EAAE,OAAO,CAAC;IACpB,8DAA8D;IAC9D,aAAa,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAMD,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,GAAG,eAAe,CAmBnF;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,SAAS,cAAc,EAAE,GAAG,aAAa,CAsE3F;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAerE;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,UAAU,GAAG,WAAW,GAAG,cAAc,CAIpF"}
|
package/dist/display-status.js
CHANGED
|
@@ -14,16 +14,16 @@
|
|
|
14
14
|
*
|
|
15
15
|
* These are NEVER persisted: they are computed on demand from children's
|
|
16
16
|
* canonical workflow statuses. The canonical workflow model
|
|
17
|
-
* (`planned | in-progress | waiting | blocked | deferred | done |
|
|
18
|
-
* rejected`)
|
|
17
|
+
* (`planned | in-progress | paused | waiting | blocked | deferred | done |
|
|
18
|
+
* canceled | rejected`) includes paused leaf work without implying active execution.
|
|
19
19
|
*/
|
|
20
20
|
const ACTIVE = new Set(["in-progress"]);
|
|
21
|
-
const OPEN = new Set(["planned", "waiting", "blocked", "deferred"]);
|
|
21
|
+
const OPEN = new Set(["planned", "paused", "waiting", "blocked", "deferred"]);
|
|
22
22
|
const TERMINAL = new Set(["done", "canceled", "rejected"]);
|
|
23
23
|
/** Count children per canonical workflow status. Pure; does not mutate input. */
|
|
24
24
|
export function countBreakdown(statuses) {
|
|
25
25
|
const breakdown = {
|
|
26
|
-
planned: 0, inProgress: 0, waiting: 0, blocked: 0,
|
|
26
|
+
planned: 0, inProgress: 0, paused: 0, waiting: 0, blocked: 0,
|
|
27
27
|
deferred: 0, done: 0, canceled: 0, rejected: 0,
|
|
28
28
|
};
|
|
29
29
|
for (const s of statuses) {
|
|
@@ -34,6 +34,9 @@ export function countBreakdown(statuses) {
|
|
|
34
34
|
case "in-progress":
|
|
35
35
|
breakdown.inProgress++;
|
|
36
36
|
break;
|
|
37
|
+
case "paused":
|
|
38
|
+
breakdown.paused++;
|
|
39
|
+
break;
|
|
37
40
|
case "waiting":
|
|
38
41
|
breakdown.waiting++;
|
|
39
42
|
break;
|
|
@@ -57,7 +60,7 @@ export function countBreakdown(statuses) {
|
|
|
57
60
|
return breakdown;
|
|
58
61
|
}
|
|
59
62
|
function emptyBreakdown() {
|
|
60
|
-
return { planned: 0, inProgress: 0, waiting: 0, blocked: 0, deferred: 0, done: 0, canceled: 0, rejected: 0 };
|
|
63
|
+
return { planned: 0, inProgress: 0, paused: 0, waiting: 0, blocked: 0, deferred: 0, done: 0, canceled: 0, rejected: 0 };
|
|
61
64
|
}
|
|
62
65
|
/**
|
|
63
66
|
* Derive the parent display snapshot from its children's canonical statuses.
|
|
@@ -70,6 +73,7 @@ function emptyBreakdown() {
|
|
|
70
73
|
* - all `rejected` → `rejected`
|
|
71
74
|
* - otherwise → `closed` (mixed terminal outcomes)
|
|
72
75
|
* 3. Compute `unfinished = meaningful ∩ OPEN`.
|
|
76
|
+
* - homogeneous `paused` → `paused`
|
|
73
77
|
* - homogeneous `waiting` → `waiting`
|
|
74
78
|
* - homogeneous `blocked` → `blocked`
|
|
75
79
|
* - homogeneous `deferred` → `deferred`
|
|
@@ -121,6 +125,10 @@ export function deriveParentDisplay(childStatuses) {
|
|
|
121
125
|
}
|
|
122
126
|
// 3. Unfinished meaningful remainder.
|
|
123
127
|
const unfinished = meaningful.filter((s) => OPEN.has(s));
|
|
128
|
+
const allPaused = unfinished.length > 0 && unfinished.every((s) => s === "paused");
|
|
129
|
+
if (allPaused) {
|
|
130
|
+
return { displayStatus: "paused", breakdown, hasStarted, totalChildren, meaningfulChildren };
|
|
131
|
+
}
|
|
124
132
|
const allWaiting = unfinished.length > 0 && unfinished.every((s) => s === "waiting");
|
|
125
133
|
if (allWaiting) {
|
|
126
134
|
return { displayStatus: "waiting", breakdown, hasStarted, totalChildren, meaningfulChildren };
|
|
@@ -150,6 +158,7 @@ export function toWorkflowStatus(value) {
|
|
|
150
158
|
switch (value) {
|
|
151
159
|
case "planned":
|
|
152
160
|
case "in-progress":
|
|
161
|
+
case "paused":
|
|
153
162
|
case "waiting":
|
|
154
163
|
case "blocked":
|
|
155
164
|
case "deferred":
|
package/dist/plan-store.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { type Feature, type FeaturesDocument, type Manifest, type Phase, type PlanWorkspace, type Project, type RequirementsDocument, type Requirement, type ActivityEntry, type ActivityLog, type CodebaseProfile, type ResumeFocus, type WorkDeviation } from "./schema.js";
|
|
2
|
+
import { type Feature, type FeaturesDocument, type Manifest, type Phase, type Task, type TaskPauseSnapshot, type PlanWorkspace, type Project, type RequirementsDocument, type Requirement, type ActivityEntry, type ActivityLog, type CodebaseProfile, type ResumeFocus, type WorkDeviation } from "./schema.js";
|
|
3
3
|
import { type ParentDisplay } from "./display-status.js";
|
|
4
4
|
export interface PlanStoreErrorDetails {
|
|
5
5
|
path?: string;
|
|
@@ -117,6 +117,10 @@ export declare class PlanStore {
|
|
|
117
117
|
runBatch<T>(fn: () => Promise<T>): Promise<T>;
|
|
118
118
|
private maybeAutoSync;
|
|
119
119
|
private normalizeTasks;
|
|
120
|
+
/** Stamp description edits independently from generic entity mutations.
|
|
121
|
+
* Legacy entities cannot reveal their historical description-edit time, so
|
|
122
|
+
* their creation time is the earliest truthful fallback. */
|
|
123
|
+
private stampDescriptionUpdatedAt;
|
|
120
124
|
private normalizeFeaturesDocument;
|
|
121
125
|
private normalizePhaseDocument;
|
|
122
126
|
private normalizeStructureSnapshot;
|
|
@@ -316,8 +320,8 @@ export declare class PlanStore {
|
|
|
316
320
|
updateProject(updater: (p: Project) => Project): Promise<Project>;
|
|
317
321
|
/** Persist an explicitly approved work deviation without coupling it to a harness. */
|
|
318
322
|
addWorkDeviation(deviation: WorkDeviation): Promise<Project>;
|
|
319
|
-
/**
|
|
320
|
-
setWorkDeviationState(id: string, state: "active" | "resolved" | "canceled", timestamp?: string): Promise<Project>;
|
|
323
|
+
/** Advance a deviation while retaining its complete audit and return stack. */
|
|
324
|
+
setWorkDeviationState(id: string, state: "active" | "resume-required" | "resolved" | "resumed" | "canceled", timestamp?: string): Promise<Project>;
|
|
321
325
|
updateFeatures(updater: (f: FeaturesDocument) => FeaturesDocument): Promise<FeaturesDocument>;
|
|
322
326
|
updateRequirements(updater: (r: RequirementsDocument) => RequirementsDocument): Promise<RequirementsDocument>;
|
|
323
327
|
saveProject(project: Project): Promise<void>;
|
|
@@ -332,6 +336,10 @@ export declare class PlanStore {
|
|
|
332
336
|
* task_create / phase_update calls on the SAME phaseId so batch operations
|
|
333
337
|
* don't lose tasks (last-write-wins race condition). */
|
|
334
338
|
updatePhase(phaseId: string, updater: (phase: Phase) => Phase): Promise<Phase>;
|
|
339
|
+
/** Pause active work with a mandatory durable checkpoint. */
|
|
340
|
+
pauseTask(phaseId: string, taskId: string, input: TaskPauseSnapshot): Promise<Task>;
|
|
341
|
+
/** Resume a paused task without resetting its original startedAt. */
|
|
342
|
+
resumeTask(phaseId: string, taskId: string, timestamp?: string): Promise<Task>;
|
|
335
343
|
/** Get the handoff text for a phase ("" if none). Throws if phase missing. */
|
|
336
344
|
getPhaseHandoff(phaseId: string): Promise<string>;
|
|
337
345
|
/** Set the handoff text for a phase + stamp handoffUpdatedAt. A completed or
|
package/dist/plan-store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan-store.d.ts","sourceRoot":"","sources":["../src/plan-store.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAY,MAAM,KAAK,CAAC;AAclC,OAAO,EAGL,KAAK,OAAO,EACZ,KAAK,gBAAgB,EAGrB,KAAK,QAAQ,EAEb,KAAK,KAAK,
|
|
1
|
+
{"version":3,"file":"plan-store.d.ts","sourceRoot":"","sources":["../src/plan-store.ts"],"names":[],"mappings":"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"}
|
package/dist/plan-store.js
CHANGED
|
@@ -14,7 +14,7 @@ const PLANNER_GITIGNORE = [
|
|
|
14
14
|
"generated/",
|
|
15
15
|
"",
|
|
16
16
|
].join("\n");
|
|
17
|
-
import { CodebaseProfileSchema, FeatureSchema, FeaturesDocumentSchema, ManifestSchema, PhaseSchema, ProjectSchema, RequirementsDocumentSchema, ResumeFocusSchema, ActivityLogSchema, TimestampSchema, } from "./schema.js";
|
|
17
|
+
import { CodebaseProfileSchema, FeatureSchema, FeaturesDocumentSchema, ManifestSchema, PhaseSchema, TaskPauseSnapshotSchema, ProjectSchema, RequirementsDocumentSchema, ResumeFocusSchema, ActivityLogSchema, TimestampSchema, } from "./schema.js";
|
|
18
18
|
import { createFeatureId, createPhaseId, createRequirementId, createShortId, createStatusLogEntryId, createTaskId, formatPhaseRef, isLegacyPhaseId } from "./naming.js";
|
|
19
19
|
import { deriveParentDisplay, fromCanonicalStatus } from "./display-status.js";
|
|
20
20
|
function nowISO() {
|
|
@@ -518,23 +518,43 @@ export class PlanStore {
|
|
|
518
518
|
normalizeTasks(tasks) {
|
|
519
519
|
// Numbers are a STABLE global sequence (assigned once at create from project.nextTaskNumber).
|
|
520
520
|
// Do NOT renumber here — renumbering would break references after deletions.
|
|
521
|
-
|
|
521
|
+
const normalized = tasks.map((task) => task.description && !task.descriptionUpdatedAt
|
|
522
|
+
? { ...task, descriptionUpdatedAt: task.createdAt }
|
|
523
|
+
: task);
|
|
524
|
+
return { tasks: normalized, changed: normalized.some((task, index) => task !== tasks[index]) };
|
|
525
|
+
}
|
|
526
|
+
/** Stamp description edits independently from generic entity mutations.
|
|
527
|
+
* Legacy entities cannot reveal their historical description-edit time, so
|
|
528
|
+
* their creation time is the earliest truthful fallback. */
|
|
529
|
+
stampDescriptionUpdatedAt(entity, previousDescription, timestamp) {
|
|
530
|
+
if (!entity.description)
|
|
531
|
+
return { ...entity, descriptionUpdatedAt: "" };
|
|
532
|
+
if (previousDescription === undefined || previousDescription !== entity.description) {
|
|
533
|
+
return { ...entity, descriptionUpdatedAt: timestamp };
|
|
534
|
+
}
|
|
535
|
+
return entity.descriptionUpdatedAt ? entity : { ...entity, descriptionUpdatedAt: entity.createdAt };
|
|
522
536
|
}
|
|
523
537
|
normalizeFeaturesDocument(doc) {
|
|
524
538
|
// Numbers are a STABLE global sequence (assigned once at create from project.nextFeatureNumber).
|
|
525
|
-
|
|
539
|
+
const features = doc.features.map((feature) => feature.description && !feature.descriptionUpdatedAt
|
|
540
|
+
? { ...feature, descriptionUpdatedAt: feature.createdAt }
|
|
541
|
+
: feature);
|
|
542
|
+
return { doc: { ...doc, features }, changed: features.some((feature, index) => feature !== doc.features[index]) };
|
|
526
543
|
}
|
|
527
544
|
normalizePhaseDocument(phase) {
|
|
528
545
|
const { tasks, changed } = this.normalizeTasks(phase.tasks);
|
|
529
546
|
const nextTaskIds = tasks.map((task) => task.id);
|
|
530
547
|
const taskIdsChanged = nextTaskIds.length !== phase.taskIds.length || nextTaskIds.some((id, index) => id !== phase.taskIds[index]);
|
|
548
|
+
const descriptionUpdatedAt = phase.description && !phase.descriptionUpdatedAt ? phase.createdAt : phase.descriptionUpdatedAt;
|
|
549
|
+
const descriptionTimestampChanged = descriptionUpdatedAt !== phase.descriptionUpdatedAt;
|
|
531
550
|
return {
|
|
532
551
|
phase: {
|
|
533
552
|
...phase,
|
|
553
|
+
descriptionUpdatedAt,
|
|
534
554
|
tasks,
|
|
535
555
|
taskIds: nextTaskIds,
|
|
536
556
|
},
|
|
537
|
-
changed: changed || taskIdsChanged,
|
|
557
|
+
changed: changed || taskIdsChanged || descriptionTimestampChanged,
|
|
538
558
|
};
|
|
539
559
|
}
|
|
540
560
|
normalizeStructureSnapshot(featuresDoc, phases) {
|
|
@@ -1013,7 +1033,10 @@ export class PlanStore {
|
|
|
1013
1033
|
const raws = await this.loadRawFeatures();
|
|
1014
1034
|
const phases = await this.loadAllPhases();
|
|
1015
1035
|
const features = raws.map((f) => ({ ...f, status: this.deriveFeatureStatus(f.id, phases) }));
|
|
1016
|
-
|
|
1036
|
+
// Keep legacy feature descriptions displayable without rewriting on read.
|
|
1037
|
+
// Phase/task reads already apply the equivalent creation-date fallback.
|
|
1038
|
+
const normalizedFeatures = this.normalizeFeaturesDocument({ features }).doc.features;
|
|
1039
|
+
return this.normalizeStructureSnapshot({ features: normalizedFeatures }, phases).features;
|
|
1017
1040
|
}
|
|
1018
1041
|
async loadCodebaseProfile() {
|
|
1019
1042
|
try {
|
|
@@ -1569,27 +1592,32 @@ export class PlanStore {
|
|
|
1569
1592
|
return "done";
|
|
1570
1593
|
const hasDone = meaningful.some((s) => s === "done");
|
|
1571
1594
|
const hasActive = meaningful.some((s) => s === "in-progress");
|
|
1595
|
+
const hasPaused = meaningful.some((s) => s === "paused");
|
|
1572
1596
|
const hasPlanned = meaningful.some((s) => s === "planned");
|
|
1573
1597
|
const hasBlocked = meaningful.some((s) => s === "blocked");
|
|
1574
1598
|
const hasWaiting = meaningful.some((s) => s === "waiting");
|
|
1575
1599
|
const hasDeferred = meaningful.some((s) => s === "deferred");
|
|
1576
1600
|
if (hasActive)
|
|
1577
1601
|
return "in-progress";
|
|
1578
|
-
// If completed work exists and the ONLY remaining meaningful work is
|
|
1579
|
-
//
|
|
1580
|
-
if (hasDone && !hasPlanned && !hasBlocked && !hasWaiting && hasDeferred)
|
|
1602
|
+
// If completed work exists and the ONLY remaining meaningful work is paused
|
|
1603
|
+
// or deferred, do not imply active execution.
|
|
1604
|
+
if (hasDone && !hasPlanned && !hasBlocked && !hasWaiting && !hasDeferred && hasPaused)
|
|
1605
|
+
return "paused";
|
|
1606
|
+
if (hasDone && !hasPlanned && !hasBlocked && !hasWaiting && !hasPaused && hasDeferred)
|
|
1581
1607
|
return "deferred";
|
|
1582
1608
|
// Partial completion with remaining planned/blocked/waiting work still means
|
|
1583
1609
|
// the phase has genuinely started and is not terminal yet.
|
|
1584
1610
|
if (hasDone)
|
|
1585
1611
|
return "in-progress";
|
|
1586
|
-
// No progress at all ⇒ surface the stall / not-started state
|
|
1612
|
+
// No progress at all ⇒ surface the stall / not-started state.
|
|
1587
1613
|
if (hasBlocked)
|
|
1588
1614
|
return "blocked";
|
|
1589
1615
|
if (hasWaiting)
|
|
1590
1616
|
return "waiting";
|
|
1591
1617
|
if (hasDeferred)
|
|
1592
1618
|
return "deferred";
|
|
1619
|
+
if (hasPaused)
|
|
1620
|
+
return "paused";
|
|
1593
1621
|
return "planned";
|
|
1594
1622
|
}
|
|
1595
1623
|
deriveFeatureStatus(featureId, phases) {
|
|
@@ -1605,14 +1633,17 @@ export class PlanStore {
|
|
|
1605
1633
|
return "done";
|
|
1606
1634
|
const hasDone = meaningful.some((s) => s === "done");
|
|
1607
1635
|
const hasActive = meaningful.some((s) => s === "discovery" || s === "in-progress");
|
|
1636
|
+
const hasPaused = meaningful.some((s) => s === "paused");
|
|
1608
1637
|
const hasPlanned = meaningful.some((s) => s === "planned");
|
|
1609
1638
|
const hasBlocked = meaningful.some((s) => s === "blocked");
|
|
1610
1639
|
const hasWaiting = meaningful.some((s) => s === "waiting");
|
|
1611
1640
|
const hasDeferred = meaningful.some((s) => s === "deferred");
|
|
1612
1641
|
if (hasActive)
|
|
1613
1642
|
return "in-progress";
|
|
1614
|
-
// Same rule as phases: done + deferred-only remainder is
|
|
1615
|
-
if (hasDone && !hasPlanned && !hasBlocked && !hasWaiting && hasDeferred)
|
|
1643
|
+
// Same rule as phases: done + paused/deferred-only remainder is non-active.
|
|
1644
|
+
if (hasDone && !hasPlanned && !hasBlocked && !hasWaiting && !hasDeferred && hasPaused)
|
|
1645
|
+
return "paused";
|
|
1646
|
+
if (hasDone && !hasPlanned && !hasBlocked && !hasWaiting && !hasPaused && hasDeferred)
|
|
1616
1647
|
return "deferred";
|
|
1617
1648
|
if (hasDone)
|
|
1618
1649
|
return "in-progress";
|
|
@@ -1623,6 +1654,8 @@ export class PlanStore {
|
|
|
1623
1654
|
return "waiting";
|
|
1624
1655
|
if (hasDeferred)
|
|
1625
1656
|
return "deferred";
|
|
1657
|
+
if (hasPaused)
|
|
1658
|
+
return "paused";
|
|
1626
1659
|
return "planned";
|
|
1627
1660
|
}
|
|
1628
1661
|
async syncStatuses() {
|
|
@@ -1712,7 +1745,7 @@ export class PlanStore {
|
|
|
1712
1745
|
workDeviations: [...project.workDeviations, deviation],
|
|
1713
1746
|
}));
|
|
1714
1747
|
}
|
|
1715
|
-
/**
|
|
1748
|
+
/** Advance a deviation while retaining its complete audit and return stack. */
|
|
1716
1749
|
async setWorkDeviationState(id, state, timestamp = nowISO()) {
|
|
1717
1750
|
return this.updateProject((project) => ({
|
|
1718
1751
|
...project,
|
|
@@ -1720,7 +1753,9 @@ export class PlanStore {
|
|
|
1720
1753
|
...deviation,
|
|
1721
1754
|
state,
|
|
1722
1755
|
activatedAt: state === "active" ? timestamp : deviation.activatedAt,
|
|
1756
|
+
resumeRequiredAt: state === "resume-required" ? timestamp : deviation.resumeRequiredAt,
|
|
1723
1757
|
resolvedAt: state === "resolved" || state === "canceled" ? timestamp : deviation.resolvedAt,
|
|
1758
|
+
resumedAt: state === "resumed" ? timestamp : deviation.resumedAt,
|
|
1724
1759
|
}),
|
|
1725
1760
|
}));
|
|
1726
1761
|
}
|
|
@@ -1728,7 +1763,15 @@ export class PlanStore {
|
|
|
1728
1763
|
const updated = await this.withFeaturesLock(async () => {
|
|
1729
1764
|
await this.migrateLegacy();
|
|
1730
1765
|
const current = await this.loadFeatures();
|
|
1731
|
-
|
|
1766
|
+
// Updaters commonly mutate `current` in place, so snapshot descriptions
|
|
1767
|
+
// before invoking them rather than comparing object references afterward.
|
|
1768
|
+
const previousDescriptions = new Map(current.features.map((feature) => [feature.id, feature.description]));
|
|
1769
|
+
const timestamp = nowISO();
|
|
1770
|
+
const candidate = updater(current);
|
|
1771
|
+
const stamped = {
|
|
1772
|
+
features: candidate.features.map((feature) => this.stampDescriptionUpdatedAt(feature, previousDescriptions.get(feature.id), timestamp)),
|
|
1773
|
+
};
|
|
1774
|
+
const upd = this.normalizeFeaturesDocument(stamped).doc;
|
|
1732
1775
|
await this.saveFeaturesRaw(upd);
|
|
1733
1776
|
return upd;
|
|
1734
1777
|
});
|
|
@@ -1749,7 +1792,11 @@ export class PlanStore {
|
|
|
1749
1792
|
async saveFeatures(features) {
|
|
1750
1793
|
await this.withFeaturesLock(async () => {
|
|
1751
1794
|
await this.migrateLegacy();
|
|
1752
|
-
await this.
|
|
1795
|
+
const previousDescriptions = new Map((await this.loadRawFeatures()).map((feature) => [feature.id, feature.description]));
|
|
1796
|
+
const timestamp = nowISO();
|
|
1797
|
+
await this.saveFeaturesRaw({
|
|
1798
|
+
features: features.features.map((feature) => this.stampDescriptionUpdatedAt(feature, previousDescriptions.get(feature.id), timestamp)),
|
|
1799
|
+
});
|
|
1753
1800
|
});
|
|
1754
1801
|
await this.touchTimestamp();
|
|
1755
1802
|
await this.maybeAutoSync();
|
|
@@ -1782,8 +1829,9 @@ export class PlanStore {
|
|
|
1782
1829
|
async saveFeature(feature) {
|
|
1783
1830
|
await this.withFeaturesLock(async () => {
|
|
1784
1831
|
await this.migrateLegacy();
|
|
1832
|
+
const previous = await readJson(this.featurePath(feature.id), FeatureSchema).catch(() => null);
|
|
1785
1833
|
await mkdir(this.featuresDir(), { recursive: true });
|
|
1786
|
-
const parsed = FeatureSchema.parse(feature);
|
|
1834
|
+
const parsed = FeatureSchema.parse(this.stampDescriptionUpdatedAt(feature, previous?.description, nowISO()));
|
|
1787
1835
|
await atomicWriteJson(this.featurePath(parsed.id), parsed, this.root);
|
|
1788
1836
|
});
|
|
1789
1837
|
await this.touchTimestamp();
|
|
@@ -1795,6 +1843,8 @@ export class PlanStore {
|
|
|
1795
1843
|
await this.touchTimestamp();
|
|
1796
1844
|
}
|
|
1797
1845
|
async savePhase(phase) {
|
|
1846
|
+
const previous = await this.loadPhase(phase.id).catch(() => null);
|
|
1847
|
+
const timestamp = nowISO();
|
|
1798
1848
|
const features = await this.loadRawFeatures();
|
|
1799
1849
|
const resolvedFeatureId = resolveStoredFeatureId(features, phase.featureId);
|
|
1800
1850
|
// Referential integrity: if a featureId is present but cannot be resolved
|
|
@@ -1810,7 +1860,10 @@ export class PlanStore {
|
|
|
1810
1860
|
const normalizedInput = resolvedFeatureId && resolvedFeatureId !== phase.featureId
|
|
1811
1861
|
? { ...phase, featureId: resolvedFeatureId }
|
|
1812
1862
|
: phase;
|
|
1813
|
-
const
|
|
1863
|
+
const previousTaskDescriptions = new Map((previous?.tasks ?? []).map((task) => [task.id, task.description]));
|
|
1864
|
+
const timestamped = this.stampDescriptionUpdatedAt(normalizedInput, previous?.description, timestamp);
|
|
1865
|
+
timestamped.tasks = timestamped.tasks.map((task) => this.stampDescriptionUpdatedAt(task, previousTaskDescriptions.get(task.id), timestamp));
|
|
1866
|
+
const parsed = PhaseSchema.parse(this.normalizePhaseDocument(timestamped).phase);
|
|
1814
1867
|
await mkdir(this.phasesDir(), { recursive: true });
|
|
1815
1868
|
await atomicWriteJson(this.phasePath(parsed.id), parsed, this.root);
|
|
1816
1869
|
await this.touchTimestamp();
|
|
@@ -1827,20 +1880,96 @@ export class PlanStore {
|
|
|
1827
1880
|
// not persisted); the return value is re-derived for the caller.
|
|
1828
1881
|
const raw = await atomicUpdateJson(this.phasePath(phaseId), PhaseSchema, (rawPhase) => {
|
|
1829
1882
|
const current = { ...rawPhase, status: this.derivePhaseStatus(rawPhase.tasks) };
|
|
1883
|
+
// The updater may mutate `current`, therefore snapshot descriptions first.
|
|
1884
|
+
const previousDescription = current.description;
|
|
1885
|
+
const previousTaskDescriptions = new Map(current.tasks.map((task) => [task.id, task.description]));
|
|
1886
|
+
const timestamp = nowISO();
|
|
1830
1887
|
const next = updater(current);
|
|
1831
|
-
const
|
|
1888
|
+
const timestamped = this.stampDescriptionUpdatedAt(next, previousDescription, timestamp);
|
|
1889
|
+
timestamped.tasks = timestamped.tasks.map((task) => this.stampDescriptionUpdatedAt(task, previousTaskDescriptions.get(task.id), timestamp));
|
|
1890
|
+
const resolvedFeatureId = resolveStoredFeatureId(features, timestamped.featureId);
|
|
1832
1891
|
// Referential integrity: reject orphan featureId.
|
|
1833
|
-
if (
|
|
1834
|
-
throw new PlanStoreError(`Cannot update phase: featureId "${
|
|
1892
|
+
if (timestamped.featureId && timestamped.featureId.trim() && !resolvedFeatureId) {
|
|
1893
|
+
throw new PlanStoreError(`Cannot update phase: featureId "${timestamped.featureId}" does not match any existing feature.`);
|
|
1835
1894
|
}
|
|
1836
|
-
const normalizedInput = resolvedFeatureId && resolvedFeatureId !==
|
|
1837
|
-
? { ...
|
|
1838
|
-
:
|
|
1895
|
+
const normalizedInput = resolvedFeatureId && resolvedFeatureId !== timestamped.featureId
|
|
1896
|
+
? { ...timestamped, featureId: resolvedFeatureId }
|
|
1897
|
+
: timestamped;
|
|
1839
1898
|
return this.normalizePhaseDocument(normalizedInput).phase;
|
|
1840
1899
|
});
|
|
1841
1900
|
await this.maybeAutoSync();
|
|
1842
1901
|
return { ...raw, status: this.derivePhaseStatus(raw.tasks) };
|
|
1843
1902
|
}
|
|
1903
|
+
/** Pause active work with a mandatory durable checkpoint. */
|
|
1904
|
+
async pauseTask(phaseId, taskId, input) {
|
|
1905
|
+
const snapshot = TaskPauseSnapshotSchema.parse(input);
|
|
1906
|
+
let paused;
|
|
1907
|
+
await this.updatePhase(phaseId, (phase) => {
|
|
1908
|
+
const task = phase.tasks.find((candidate) => candidate.id === taskId);
|
|
1909
|
+
if (!task)
|
|
1910
|
+
throw new PlanStoreError(`Task ${taskId} does not belong to phase ${phaseId}.`);
|
|
1911
|
+
if (task.status !== "in-progress") {
|
|
1912
|
+
throw new PlanStoreError(`Task ${taskId} cannot be paused from ${task.status}; only in-progress work can be paused.`);
|
|
1913
|
+
}
|
|
1914
|
+
const description = [
|
|
1915
|
+
`Reason: ${snapshot.reason}`,
|
|
1916
|
+
`What was being done: ${snapshot.whatWasBeingDone}`,
|
|
1917
|
+
`Resume location: ${snapshot.resumeLocation}`,
|
|
1918
|
+
`How to resume: ${snapshot.howToResume}`,
|
|
1919
|
+
].join("\n");
|
|
1920
|
+
paused = {
|
|
1921
|
+
...task,
|
|
1922
|
+
status: "paused",
|
|
1923
|
+
pauseSnapshot: snapshot,
|
|
1924
|
+
pauseHistory: [...task.pauseHistory, snapshot],
|
|
1925
|
+
statusLog: [...task.statusLog, {
|
|
1926
|
+
id: createStatusLogEntryId(),
|
|
1927
|
+
date: snapshot.pausedAt,
|
|
1928
|
+
fromStatus: "in-progress",
|
|
1929
|
+
toStatus: "paused",
|
|
1930
|
+
title: "in-progress → paused",
|
|
1931
|
+
description,
|
|
1932
|
+
}],
|
|
1933
|
+
updatedAt: snapshot.pausedAt,
|
|
1934
|
+
};
|
|
1935
|
+
phase.tasks = phase.tasks.map((candidate) => candidate.id === taskId ? paused : candidate);
|
|
1936
|
+
return phase;
|
|
1937
|
+
});
|
|
1938
|
+
return paused;
|
|
1939
|
+
}
|
|
1940
|
+
/** Resume a paused task without resetting its original startedAt. */
|
|
1941
|
+
async resumeTask(phaseId, taskId, timestamp = nowISO()) {
|
|
1942
|
+
let resumed;
|
|
1943
|
+
await this.updatePhase(phaseId, (phase) => {
|
|
1944
|
+
const task = phase.tasks.find((candidate) => candidate.id === taskId);
|
|
1945
|
+
if (!task)
|
|
1946
|
+
throw new PlanStoreError(`Task ${taskId} does not belong to phase ${phaseId}.`);
|
|
1947
|
+
if (task.status !== "paused") {
|
|
1948
|
+
throw new PlanStoreError(`Task ${taskId} cannot be resumed from ${task.status}; only paused work can be resumed.`);
|
|
1949
|
+
}
|
|
1950
|
+
const snapshot = task.pauseSnapshot;
|
|
1951
|
+
resumed = {
|
|
1952
|
+
...task,
|
|
1953
|
+
status: "in-progress",
|
|
1954
|
+
pauseSnapshot: null,
|
|
1955
|
+
startedAt: task.startedAt || timestamp,
|
|
1956
|
+
statusLog: [...task.statusLog, {
|
|
1957
|
+
id: createStatusLogEntryId(),
|
|
1958
|
+
date: timestamp,
|
|
1959
|
+
fromStatus: "paused",
|
|
1960
|
+
toStatus: "in-progress",
|
|
1961
|
+
title: "paused → in-progress",
|
|
1962
|
+
description: snapshot
|
|
1963
|
+
? `Resumed from ${snapshot.resumeLocation}. ${snapshot.howToResume}`
|
|
1964
|
+
: "Paused task resumed.",
|
|
1965
|
+
}],
|
|
1966
|
+
updatedAt: timestamp,
|
|
1967
|
+
};
|
|
1968
|
+
phase.tasks = phase.tasks.map((candidate) => candidate.id === taskId ? resumed : candidate);
|
|
1969
|
+
return phase;
|
|
1970
|
+
});
|
|
1971
|
+
return resumed;
|
|
1972
|
+
}
|
|
1844
1973
|
// ── Phase-scoped handoff (entity field, harness-agnostic) ────────────
|
|
1845
1974
|
/** Get the handoff text for a phase ("" if none). Throws if phase missing. */
|
|
1846
1975
|
async getPhaseHandoff(phaseId) {
|
package/dist/recap.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recap.d.ts","sourceRoot":"","sources":["../src/recap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAGjD;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAED,yEAAyE;AACzE,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,KAAK,CAAC;AAExC,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,YAAY,CAAC;CACxB;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,GAAE,YAAiB,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"recap.d.ts","sourceRoot":"","sources":["../src/recap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAGjD;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAED,yEAAyE;AACzE,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,KAAK,CAAC;AAExC,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,YAAY,CAAC;CACxB;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,GAAE,YAAiB,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAmOhH"}
|
package/dist/recap.js
CHANGED
|
@@ -33,6 +33,20 @@ export async function buildRecap(st, web = {}, opts = {}) {
|
|
|
33
33
|
const totalT = allTasks.length;
|
|
34
34
|
const doneT = allTasks.filter(({ task }) => task.status === "done").length;
|
|
35
35
|
const activeT = allTasks.filter(({ task }) => task.status === "in-progress").length;
|
|
36
|
+
const pausedTasks = allTasks.filter(({ task }) => task.status === "paused");
|
|
37
|
+
const pausedT = pausedTasks.length;
|
|
38
|
+
const pendingDeviation = [...plan.project.workDeviations]
|
|
39
|
+
.filter((deviation) => deviation.state === "resume-required" || deviation.state === "resolved")
|
|
40
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))
|
|
41
|
+
.find((deviation) => allTasks.some(({ task }) => task.id === deviation.resumeTaskId
|
|
42
|
+
&& (task.status === "paused" || task.status === "planned" || task.status === "waiting")));
|
|
43
|
+
const pendingResume = pendingDeviation
|
|
44
|
+
? allTasks.find(({ task }) => task.id === pendingDeviation.resumeTaskId)
|
|
45
|
+
: undefined;
|
|
46
|
+
const standalonePaused = pausedTasks
|
|
47
|
+
.filter(({ task }) => task.id !== pendingResume?.task.id)
|
|
48
|
+
.sort((left, right) => (right.task.pauseSnapshot?.pausedAt ?? "").localeCompare(left.task.pauseSnapshot?.pausedAt ?? ""));
|
|
49
|
+
const latestStandalonePaused = standalonePaused[0];
|
|
36
50
|
// Plan is fully complete: there is work and all of it is done, nothing active.
|
|
37
51
|
// (totalT > 0 guards the empty/unstarted case from looking "complete".)
|
|
38
52
|
const planComplete = totalT > 0 && doneT === totalT && doneP === totalP && doneF === totalF;
|
|
@@ -54,14 +68,24 @@ export async function buildRecap(st, web = {}, opts = {}) {
|
|
|
54
68
|
const name = plan.project.name || "(unnamed project)";
|
|
55
69
|
lines.push(`${italian ? "Progetto" : "Project"}: ${name}${plan.project.goal ? " — " + plan.project.goal : ""}`);
|
|
56
70
|
lines.push(italian
|
|
57
|
-
? `Avanzamento: feature ${doneF}/${totalF} completate (${activeF} attive) · fasi ${doneP}/${totalP} completate (${activeP} attive) · task ${doneT}/${totalT} completati (${activeT} attivi)`
|
|
58
|
-
: `Progress: Features ${doneF}/${totalF} done (${activeF} active) · Phases ${doneP}/${totalP} done (${activeP} active) · Tasks ${doneT}/${totalT} done (${activeT} active)`);
|
|
71
|
+
? `Avanzamento: feature ${doneF}/${totalF} completate (${activeF} attive) · fasi ${doneP}/${totalP} completate (${activeP} attive) · task ${doneT}/${totalT} completati (${activeT} attivi, ${pausedT} in pausa)`
|
|
72
|
+
: `Progress: Features ${doneF}/${totalF} done (${activeF} active) · Phases ${doneP}/${totalP} done (${activeP} active) · Tasks ${doneT}/${totalT} done (${activeT} active, ${pausedT} paused)`);
|
|
59
73
|
if (focusTask && focusPhase) {
|
|
60
74
|
const fr = focusFeature ? fref(focusFeature.number) : "?";
|
|
61
75
|
const pr = formatPhaseRef(focusPhase.number, focusFeature?.number);
|
|
62
76
|
const tr = tref(focusTask.task.number);
|
|
63
77
|
lines.push(`${italian ? "Focus corrente" : "Current focus"}: ${fr} — ${focusFeature?.name ?? "?"} / ${pr} — ${focusPhase.title} / ${tr} — ${focusTask.task.title} (in-progress)`);
|
|
64
78
|
}
|
|
79
|
+
else if (pendingResume) {
|
|
80
|
+
const feature = feats.find((entry) => entry.id === pendingResume.phase.featureId);
|
|
81
|
+
const ref = `${formatPhaseRef(pendingResume.phase.number, feature?.number)}/${tref(pendingResume.task.number)}`;
|
|
82
|
+
lines.push(`${italian ? "Focus corrente" : "Current focus"}: ${italian ? "ripresa obbligatoria" : "resume required"} — ${ref} — ${pendingResume.task.title} (${pendingResume.task.status})`);
|
|
83
|
+
}
|
|
84
|
+
else if (latestStandalonePaused) {
|
|
85
|
+
const feature = feats.find((entry) => entry.id === latestStandalonePaused.phase.featureId);
|
|
86
|
+
const ref = `${formatPhaseRef(latestStandalonePaused.phase.number, feature?.number)}/${tref(latestStandalonePaused.task.number)}`;
|
|
87
|
+
lines.push(`${italian ? "Focus corrente" : "Current focus"}: ${italian ? "task in pausa da riprendere" : "paused task to resume"} — ${ref} — ${latestStandalonePaused.task.title}`);
|
|
88
|
+
}
|
|
65
89
|
else if (handoffs.length > 0) {
|
|
66
90
|
const top = handoffs[0];
|
|
67
91
|
lines.push(`${italian ? "Focus corrente" : "Current focus"}: ${italian ? "nessun task attivo" : "no active task"} · ${italian ? "handoff pendente più recente" : "most recent pending handoff"}: ${top.compositeRef} — "${top.firstLine}"`);
|
|
@@ -79,7 +103,21 @@ export async function buildRecap(st, web = {}, opts = {}) {
|
|
|
79
103
|
// instead of surfacing potentially-stale resume.json nextSteps. When no
|
|
80
104
|
// handoff exists, fall back to resume.nextSteps but mark them as possibly
|
|
81
105
|
// stale (free-text that refreshResume never touches, so they can drift).
|
|
82
|
-
if (
|
|
106
|
+
if (pendingResume) {
|
|
107
|
+
const feature = feats.find((entry) => entry.id === pendingResume.phase.featureId);
|
|
108
|
+
const ref = `${formatPhaseRef(pendingResume.phase.number, feature?.number)}/${tref(pendingResume.task.number)}`;
|
|
109
|
+
lines.push(italian
|
|
110
|
+
? `Prossimo step: riprendi ${ref} con ${taskStartCmd}. Il checkpoint del task è prioritario rispetto a nuovo lavoro e handoff di fase.`
|
|
111
|
+
: `Next step: resume ${ref} with ${taskStartCmd}. Its task checkpoint takes precedence over new work and phase handoffs.`);
|
|
112
|
+
}
|
|
113
|
+
else if (latestStandalonePaused) {
|
|
114
|
+
const feature = feats.find((entry) => entry.id === latestStandalonePaused.phase.featureId);
|
|
115
|
+
const ref = `${formatPhaseRef(latestStandalonePaused.phase.number, feature?.number)}/${tref(latestStandalonePaused.task.number)}`;
|
|
116
|
+
lines.push(italian
|
|
117
|
+
? `Prossimo step: riprendi il checkpoint più recente, ${ref}, con ${taskStartCmd}, prima di scegliere nuovo lavoro.`
|
|
118
|
+
: `Next step: resume the newest checkpoint, ${ref}, with ${taskStartCmd} before selecting new work.`);
|
|
119
|
+
}
|
|
120
|
+
else if (handoffs.length > 0) {
|
|
83
121
|
lines.push(italian
|
|
84
122
|
? `Prossimo step: leggi l'handoff di fase pendente sotto (fonte autorevole, gestita attivamente). I nextSteps legacy di resume.json sono soppressi perché possono essere stale.`
|
|
85
123
|
: `Next step: read the pending phase handoff below (authoritative, actively maintained). Legacy resume.json nextSteps are suppressed because they may be stale.`);
|
|
@@ -95,11 +133,31 @@ export async function buildRecap(st, web = {}, opts = {}) {
|
|
|
95
133
|
: `⚠️ nextSteps are free-text from resume.json — may be stale; verify against current state before acting.`);
|
|
96
134
|
lines.push(staleNote);
|
|
97
135
|
}
|
|
136
|
+
if (pendingResume) {
|
|
137
|
+
const feature = feats.find((entry) => entry.id === pendingResume.phase.featureId);
|
|
138
|
+
const ref = `${formatPhaseRef(pendingResume.phase.number, feature?.number)}/${tref(pendingResume.task.number)}`;
|
|
139
|
+
const snapshot = pendingResume.task.pauseSnapshot ?? pendingDeviation?.snapshot;
|
|
140
|
+
lines.push("", italian ? "## Ripresa task obbligatoria" : "## Task resume required", `${ref} — ${pendingResume.task.title}`, snapshot ? `${italian ? "Perché è stato sospeso" : "Paused because"}: ${snapshot.reason}` : "", snapshot ? `${italian ? "Stato del lavoro" : "Work checkpoint"}: ${snapshot.whatWasBeingDone}` : "", snapshot ? `${italian ? "Riprendi da" : "Resume from"}: ${snapshot.resumeLocation}` : "", snapshot ? `${italian ? "Come riprendere" : "How to resume"}: ${snapshot.howToResume}` : "", `${italian ? "Azione" : "Action"}: ${taskStartCmd} ${ref}`);
|
|
141
|
+
}
|
|
142
|
+
if (standalonePaused.length > 0) {
|
|
143
|
+
lines.push("", italian ? `## Task in pausa (${standalonePaused.length})` : `## Paused tasks (${standalonePaused.length})`);
|
|
144
|
+
for (const entry of standalonePaused) {
|
|
145
|
+
const feature = feats.find((item) => item.id === entry.phase.featureId);
|
|
146
|
+
const ref = `${formatPhaseRef(entry.phase.number, feature?.number)}/${tref(entry.task.number)}`;
|
|
147
|
+
const snapshot = entry.task.pauseSnapshot;
|
|
148
|
+
lines.push(`${ref} — ${entry.task.title}`, snapshot ? ` ${italian ? "Perché" : "Why"}: ${snapshot.reason}` : "", snapshot ? ` ${italian ? "Stato" : "Checkpoint"}: ${snapshot.whatWasBeingDone}` : "", snapshot ? ` ${italian ? "Riprendi da" : "Resume from"}: ${snapshot.resumeLocation}` : "", snapshot ? ` ${italian ? "Come" : "How"}: ${snapshot.howToResume}` : "");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
98
151
|
if (handoffs.length > 0) {
|
|
99
152
|
const top = handoffs[0];
|
|
100
153
|
lines.push("", italian ? `## Handoff di fase pendenti (${handoffs.length})` : `## Pending phase handoffs (${handoffs.length})`);
|
|
101
154
|
handoffs.forEach((h, i) => lines.push(`[${i + 1}] ${h.compositeRef} — ${h.updatedAt} — "${h.firstLine}"`));
|
|
102
|
-
if (
|
|
155
|
+
if (pendingResume || latestStandalonePaused) {
|
|
156
|
+
lines.push("", italian
|
|
157
|
+
? "→ Questi handoff restano disponibili come contesto, ma prima risolvi il task da riprendere indicato sopra."
|
|
158
|
+
: "→ These handoffs remain available as context, but first resolve the task resume shown above.");
|
|
159
|
+
}
|
|
160
|
+
else if (handoffs.length === 1) {
|
|
103
161
|
lines.push("", italian
|
|
104
162
|
? `→ Vuoi riprendere da ${top.compositeRef}? Leggi l'handoff con ${handoffShowCmd} ${top.compositeRef} e avvia il primo task pertinente. L'handoff resta attivo finché tutti i task della fase non sono done/canceled, viene sostituito da un nuovo handoff, oppure viene cancellato esplicitamente.`
|
|
105
163
|
: `→ Do you want to resume from ${top.compositeRef}? Read the handoff with ${handoffShowCmd} ${top.compositeRef} and start the first relevant task. It stays active until every phase task is done/canceled, a new handoff replaces it, or it is explicitly cleared.`);
|
|
@@ -115,7 +173,7 @@ export async function buildRecap(st, web = {}, opts = {}) {
|
|
|
115
173
|
? `Piano completo — aggiungi una nuova feature (${featureAddCmd}) o fase (${phaseAddCmd}) per continuare.`
|
|
116
174
|
: `Plan complete — add a new feature (${featureAddCmd}) or phase (${phaseAddCmd}) to continue.`);
|
|
117
175
|
}
|
|
118
|
-
else if (activeT === 0) {
|
|
176
|
+
else if (activeT === 0 && !pendingResume && standalonePaused.length === 0) {
|
|
119
177
|
lines.push("", italian
|
|
120
178
|
? `Nessun handoff pendente e nessun task in-progress. Usa ${taskAddCmd} / ${taskStartCmd} per iniziare.`
|
|
121
179
|
: `No phase handoff pending and no task in-progress. Use ${taskAddCmd} / ${taskStartCmd} to begin work.`);
|