@agent-plan/core 0.2.22 → 0.2.23-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/display-status.d.ts +3 -5
- package/dist/display-status.d.ts.map +1 -1
- package/dist/display-status.js +5 -14
- package/dist/handoff-context.d.ts +60 -0
- package/dist/handoff-context.d.ts.map +1 -0
- package/dist/handoff-context.js +150 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/plan-store.d.ts +49 -9
- package/dist/plan-store.d.ts.map +1 -1
- package/dist/plan-store.js +376 -71
- package/dist/planner-rules.d.ts +21 -0
- package/dist/planner-rules.d.ts.map +1 -0
- package/dist/planner-rules.js +58 -0
- package/dist/read-tracking.d.ts +86 -0
- package/dist/read-tracking.d.ts.map +1 -0
- package/dist/read-tracking.js +192 -0
- package/dist/recap.d.ts.map +1 -1
- package/dist/recap.js +30 -24
- package/dist/schema.d.ts +551 -227
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +18 -6
- package/dist/task-selection.d.ts +38 -1
- package/dist/task-selection.d.ts.map +1 -1
- package/dist/task-selection.js +49 -11
- package/dist/task-start-outcome.d.ts +23 -0
- package/dist/task-start-outcome.d.ts.map +1 -0
- package/dist/task-start-outcome.js +23 -0
- package/package.json +1 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical planner extension rules — the agent-behavior contract that applies
|
|
3
|
+
* to EVERY project using the Agent Plan extension (Pi, MCP / Claude Code / Codex,
|
|
4
|
+
* future harnesses). These are STATIC: no timestamps, no dynamic content, so the
|
|
5
|
+
* same text seeds every .planner/ and never diverges across worktrees or
|
|
6
|
+
* branches (no conflict from date/timestamp changes).
|
|
7
|
+
*
|
|
8
|
+
* AGENTS.md governs ONLY the development of the agent-plan extension and must
|
|
9
|
+
* not duplicate these rules.
|
|
10
|
+
*/
|
|
11
|
+
export declare const PLANNER_EXTENSION_RULES: string[];
|
|
12
|
+
export interface ExtensionRulesFile {
|
|
13
|
+
extensionRules: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Load the effective extension rules for a planner root. Returns the project's
|
|
17
|
+
* own .planner/rules.json (static, user-overridable) when present and non-empty,
|
|
18
|
+
* otherwise the canonical code set. Never returns timestamps or dynamic data.
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadExtensionRules(plannerRoot: string): Promise<string[]>;
|
|
21
|
+
//# sourceMappingURL=planner-rules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planner-rules.d.ts","sourceRoot":"","sources":["../src/planner-rules.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAM,EA2B3C,CAAC;AAEF,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAW/E"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Canonical planner extension rules — the agent-behavior contract that applies
|
|
5
|
+
* to EVERY project using the Agent Plan extension (Pi, MCP / Claude Code / Codex,
|
|
6
|
+
* future harnesses). These are STATIC: no timestamps, no dynamic content, so the
|
|
7
|
+
* same text seeds every .planner/ and never diverges across worktrees or
|
|
8
|
+
* branches (no conflict from date/timestamp changes).
|
|
9
|
+
*
|
|
10
|
+
* AGENTS.md governs ONLY the development of the agent-plan extension and must
|
|
11
|
+
* not duplicate these rules.
|
|
12
|
+
*/
|
|
13
|
+
export const PLANNER_EXTENSION_RULES = [
|
|
14
|
+
// §1 — source of truth
|
|
15
|
+
"Keep the planner as the single operational source of truth while working: read the relevant planner state before starting; update it when an activity starts, changes state, blocks, or concludes; and record next steps, blockers, and decisions in the relevant planner entities. Never leave work only in the conversation.",
|
|
16
|
+
// §2 — task lifecycle
|
|
17
|
+
"Respect the task lifecycle strictly. Always call task_start before touching code, and task_complete with durable evidence of shipped work, verification (including partial verification), remaining/unverified work, files, and decisions when a deliverable is done. Never enter in-progress or done through task_update. Sync state changes (start/complete/block) to the planner at the exact moment they happen — never batch updates at session end. If task_start is denied, the task remains planned: satisfy the stated read prerequisites and retry, and never claim work started without an explicit successful start result. A task marked in-progress means you are actually working on it; if you stop, close or block it with a motivation in statusLog. Derived feature/phase status is computed from tasks, not stored in JSON.",
|
|
18
|
+
// §3 — markdown not source of truth
|
|
19
|
+
"Do not treat markdown as the source of truth for the plan. The plan's primary source is structured data in .planner/; markdown is a generated, human/agent-readable view.",
|
|
20
|
+
// §5 — plan location
|
|
21
|
+
"The plan lives in .planner/ within the target project. Whether .planner/ is git-tracked is at the project's discretion.",
|
|
22
|
+
// §6 — discuss per phase
|
|
23
|
+
"Discuss the plan per phase: clarify objective, scope, non-scope, dependencies, risks, and outcomes before working a phase; detail implementation when the phase is actually worked, not up front.",
|
|
24
|
+
// §7 — naming
|
|
25
|
+
"Naming: phases and tasks use global project-wide numbering (P001, T001, …) with a slug derived from the title. Numbers are assigned once at creation from a monotonic global counter and never reused (deletes leave gaps).",
|
|
26
|
+
// §8 — status changes & motivation
|
|
27
|
+
"Every task status change is recorded in an incremental statusLog. Motivation is mandatory for blocked/canceled/rejected/deferred/waiting and for returning to planned from a non-planned status; not required for done or for normal in-progress-from-planned. Use task_update (not task_start/task_complete) for non-lifecycle status changes, with an exhaustive motivation.",
|
|
28
|
+
// §10 — references
|
|
29
|
+
"Reference entities with human, unique, composite IDs — Feature 'F001 - Name', Phase 'P001(F001) - Title', Task 'T003 - Number'. Short forms P003/T007 and the 5-char global shortId (e.g. UUXD1) are also valid. Never reference raw UUIDs. To locate an entity, use the compact list tools (feature_list/phase_list/task_list), not by reading .planner/*.json files.",
|
|
30
|
+
// §12 — handoff
|
|
31
|
+
"Handoff is per-phase (phase.handoff), not a file. Write it only on explicit user request and only after the exact feature+phase target is confirmed. Run handoff_prepare for that phase, reconcile all still-relevant existing content into one active handoff, and synchronize durable task/phase/feature context in the same handoff_write operation. A pending handoff never blocks task_start and is archived only when the phase completes or the user explicitly clears it; refreshing it must not create superseded copies.",
|
|
32
|
+
// §12 — operational hygiene
|
|
33
|
+
"Operational hygiene: start the task (task_start) before thinking about implementation; complete it (task_complete) as part of delivering the deliverable, not after; motivate every block so a third party can understand the impediment.",
|
|
34
|
+
// Avvio del planner
|
|
35
|
+
"The planner and Web UI never start automatically. Do not start the Web UI or show its URL unless the user runs load/recap/web-status. The Web UI URL appears only in the recap after load, or on explicit web status.",
|
|
36
|
+
// Regola dettagli
|
|
37
|
+
"Write relevant points (decisions, constraints, current state, file:line refs, edge cases) into the task/phase/feature description or notes as soon as they emerge. Before starting, resuming, or switching to a task, read task_get(full=true), then its parent phase_get(full=true), then its parent feature_get(full=true), in that exact order; read linked requirements explicitly when present. Cite entities with composite IDs, not bare UUIDs.",
|
|
38
|
+
// Expected operational behavior
|
|
39
|
+
"When you begin work, task_start and task_switch enforce the required ordered full reads. Read any relevant phase handoff as additional context, then update the planner before and after significant changes. If you change an architectural decision, document it explicitly.",
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* Load the effective extension rules for a planner root. Returns the project's
|
|
43
|
+
* own .planner/rules.json (static, user-overridable) when present and non-empty,
|
|
44
|
+
* otherwise the canonical code set. Never returns timestamps or dynamic data.
|
|
45
|
+
*/
|
|
46
|
+
export async function loadExtensionRules(plannerRoot) {
|
|
47
|
+
try {
|
|
48
|
+
const raw = await readFile(join(plannerRoot, "rules.json"), "utf8");
|
|
49
|
+
const parsed = JSON.parse(raw);
|
|
50
|
+
if (Array.isArray(parsed.extensionRules) && parsed.extensionRules.length > 0) {
|
|
51
|
+
return parsed.extensionRules.filter((r) => typeof r === "string" && r.length > 0);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Missing or malformed rules.json → fall back to the canonical code set.
|
|
56
|
+
}
|
|
57
|
+
return PLANNER_EXTENSION_RULES;
|
|
58
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped, ordered context-read enforcement for agent lifecycle operations.
|
|
3
|
+
*
|
|
4
|
+
* The first complete read for a task must be task(full) → phase(full) →
|
|
5
|
+
* feature(full), with linked requirements read independently. A persisted
|
|
6
|
+
* sessionInfo attestation may satisfy later checks in the same session while
|
|
7
|
+
* every entity's updatedAt remains at or before its attestation timestamp.
|
|
8
|
+
*/
|
|
9
|
+
type SessionInfoEntry = {
|
|
10
|
+
sessionId: string;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
};
|
|
13
|
+
type ReadTrackedEntity = {
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
sessionInfo?: SessionInfoEntry[];
|
|
16
|
+
};
|
|
17
|
+
export type ContextReadEligibility = {
|
|
18
|
+
eligible: boolean;
|
|
19
|
+
reason: string;
|
|
20
|
+
};
|
|
21
|
+
export interface SessionContextReadInput {
|
|
22
|
+
sessionId: string;
|
|
23
|
+
taskId: string;
|
|
24
|
+
phaseId: string;
|
|
25
|
+
featureId?: string;
|
|
26
|
+
task?: ReadTrackedEntity;
|
|
27
|
+
phase?: ReadTrackedEntity;
|
|
28
|
+
feature?: ReadTrackedEntity;
|
|
29
|
+
requirements?: Array<ReadTrackedEntity & {
|
|
30
|
+
id: string;
|
|
31
|
+
}>;
|
|
32
|
+
requirementIds?: string[];
|
|
33
|
+
}
|
|
34
|
+
/** Record a full feature read in the default compatibility session. */
|
|
35
|
+
export declare function markFeatureRead(featureId: string): void;
|
|
36
|
+
/** Record a full feature read for an explicit harness session. */
|
|
37
|
+
export declare function markFeatureReadForSessionId(sessionId: string, featureId: string): void;
|
|
38
|
+
/** Record a full phase read in the default compatibility session. */
|
|
39
|
+
export declare function markPhaseRead(phaseId: string, _featureId?: string): void;
|
|
40
|
+
/** Record a full phase read for an explicit harness session. */
|
|
41
|
+
export declare function markPhaseReadForSessionId(sessionId: string, phaseId: string): void;
|
|
42
|
+
/** Record a full task read in the default compatibility session. */
|
|
43
|
+
export declare function markTaskRead(taskId: string, _phaseId?: string, _featureId?: string): void;
|
|
44
|
+
/** Record a full task read for an explicit harness session. */
|
|
45
|
+
export declare function markTaskReadForSessionId(sessionId: string, taskId: string): void;
|
|
46
|
+
/** Record that a requirement was explicitly read in the default session. */
|
|
47
|
+
export declare function markRequirementRead(requirementId: string): void;
|
|
48
|
+
/** Record that a requirement was explicitly read for an explicit session. */
|
|
49
|
+
export declare function markRequirementReadForSessionId(sessionId: string, requirementId: string): void;
|
|
50
|
+
/**
|
|
51
|
+
* Evaluate exact in-memory read ordering first, then a persisted attestation
|
|
52
|
+
* for the same session and current entity revisions. A previously persisted
|
|
53
|
+
* but now stale attestation always wins over stale in-memory ordering.
|
|
54
|
+
*/
|
|
55
|
+
export declare function contextReadEligibilityForSession(input: SessionContextReadInput): ContextReadEligibility;
|
|
56
|
+
/** Return true only when the persisted attestation covers the current revisions. */
|
|
57
|
+
export declare function hasValidSessionAttestation(input: SessionContextReadInput): boolean;
|
|
58
|
+
/** Compatibility check for the default session and in-memory reads only. */
|
|
59
|
+
export declare function contextReadEligibility(taskId: string, phaseId: string, featureId?: string): ContextReadEligibility;
|
|
60
|
+
/** Legacy parent-read check retained for callers that only need independent parent state. */
|
|
61
|
+
export declare function hasReadParents(featureId: string | undefined, phaseId: string): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Whether linked requirements are read in memory or have valid persisted
|
|
64
|
+
* attestations for the current session and entity revisions.
|
|
65
|
+
*/
|
|
66
|
+
export declare function hasReadRequirementsForSession(sessionId: string, requirementIds: string[], requirements?: Array<ReadTrackedEntity & {
|
|
67
|
+
id: string;
|
|
68
|
+
}>): boolean;
|
|
69
|
+
/** Legacy requirement check for the default compatibility session. */
|
|
70
|
+
export declare function hasReadRequirements(requirementIds: string[]): boolean;
|
|
71
|
+
/** Clear one session's read state, or all state for legacy callers. */
|
|
72
|
+
export declare function invalidateReads(sessionId?: string): void;
|
|
73
|
+
/** Initialize an explicit session without clearing other harness sessions. */
|
|
74
|
+
export declare function startReadSession(sessionId: string): void;
|
|
75
|
+
/** Compatibility advisory for non-lifecycle callers. */
|
|
76
|
+
export declare function parentReadAdvisory(featureId: string | undefined, phaseId: string): string;
|
|
77
|
+
/** Advisory text for the separate linked-requirements gate. */
|
|
78
|
+
export declare function requirementReadAdvisory(requirementIds: string[]): string;
|
|
79
|
+
/** Snapshot for diagnostics. */
|
|
80
|
+
export declare function readTrackingSnapshot(sessionId?: string): {
|
|
81
|
+
features: string[];
|
|
82
|
+
phases: string[];
|
|
83
|
+
requirements: string[];
|
|
84
|
+
};
|
|
85
|
+
export {};
|
|
86
|
+
//# sourceMappingURL=read-tracking.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-tracking.d.ts","sourceRoot":"","sources":["../src/read-tracking.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,KAAK,gBAAgB,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjE,KAAK,iBAAiB,GAAG;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAClC,CAAC;AAUF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,YAAY,CAAC,EAAE,KAAK,CAAC,iBAAiB,GAAG;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACzD,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AA8CD,uEAAuE;AACvE,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAEvD;AAED,kEAAkE;AAClE,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAEtF;AAED,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAExE;AAED,gEAAgE;AAChE,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAElF;AAED,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAEzF;AAED,+DAA+D;AAC/D,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAEhF;AAED,4EAA4E;AAC5E,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAE/D;AAED,6EAA6E;AAC7E,wBAAgB,+BAA+B,CAAC,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI,CAE9F;AAyCD;;;;GAIG;AACH,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,uBAAuB,GAAG,sBAAsB,CAQvG;AAED,oFAAoF;AACpF,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAElF;AAED,4EAA4E;AAC5E,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,sBAAsB,CAElH;AAED,6FAA6F;AAC7F,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAGtF;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAC3C,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EAAE,EACxB,YAAY,GAAE,KAAK,CAAC,iBAAiB,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAM,GAC3D,OAAO,CAGT;AAED,sEAAsE;AACtE,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,OAAO,CAErE;AAED,uEAAuE;AACvE,wBAAgB,eAAe,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAQxD;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAExD;AAED,wDAAwD;AACxD,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAIzF;AAED,+DAA+D;AAC/D,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,MAAM,CAMxE;AAED,gCAAgC;AAChC,wBAAgB,oBAAoB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CAAE,CAOzH"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-scoped, ordered context-read enforcement for agent lifecycle operations.
|
|
3
|
+
*
|
|
4
|
+
* The first complete read for a task must be task(full) → phase(full) →
|
|
5
|
+
* feature(full), with linked requirements read independently. A persisted
|
|
6
|
+
* sessionInfo attestation may satisfy later checks in the same session while
|
|
7
|
+
* every entity's updatedAt remains at or before its attestation timestamp.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_SESSION_ID = "__default__";
|
|
10
|
+
const newState = () => ({
|
|
11
|
+
tasks: new Map(),
|
|
12
|
+
phases: new Map(),
|
|
13
|
+
features: new Map(),
|
|
14
|
+
requirements: new Set(),
|
|
15
|
+
nextSequence: 0,
|
|
16
|
+
});
|
|
17
|
+
const states = new Map([[DEFAULT_SESSION_ID, newState()]]);
|
|
18
|
+
function normalizeSessionId(sessionId) {
|
|
19
|
+
return sessionId?.trim() || DEFAULT_SESSION_ID;
|
|
20
|
+
}
|
|
21
|
+
function stateFor(sessionId) {
|
|
22
|
+
const key = normalizeSessionId(sessionId);
|
|
23
|
+
let state = states.get(key);
|
|
24
|
+
if (!state) {
|
|
25
|
+
state = newState();
|
|
26
|
+
states.set(key, state);
|
|
27
|
+
}
|
|
28
|
+
return state;
|
|
29
|
+
}
|
|
30
|
+
function record(map, id, state) {
|
|
31
|
+
state.nextSequence += 1;
|
|
32
|
+
map.set(id, state.nextSequence);
|
|
33
|
+
}
|
|
34
|
+
function markTaskReadForSession(sessionId, taskId) {
|
|
35
|
+
const state = stateFor(sessionId);
|
|
36
|
+
record(state.tasks, taskId, state);
|
|
37
|
+
}
|
|
38
|
+
function markPhaseReadForSession(sessionId, phaseId) {
|
|
39
|
+
const state = stateFor(sessionId);
|
|
40
|
+
record(state.phases, phaseId, state);
|
|
41
|
+
}
|
|
42
|
+
function markFeatureReadForSession(sessionId, featureId) {
|
|
43
|
+
const state = stateFor(sessionId);
|
|
44
|
+
record(state.features, featureId, state);
|
|
45
|
+
}
|
|
46
|
+
/** Record a full feature read in the default compatibility session. */
|
|
47
|
+
export function markFeatureRead(featureId) {
|
|
48
|
+
markFeatureReadForSession(DEFAULT_SESSION_ID, featureId);
|
|
49
|
+
}
|
|
50
|
+
/** Record a full feature read for an explicit harness session. */
|
|
51
|
+
export function markFeatureReadForSessionId(sessionId, featureId) {
|
|
52
|
+
markFeatureReadForSession(sessionId, featureId);
|
|
53
|
+
}
|
|
54
|
+
/** Record a full phase read in the default compatibility session. */
|
|
55
|
+
export function markPhaseRead(phaseId, _featureId) {
|
|
56
|
+
markPhaseReadForSession(DEFAULT_SESSION_ID, phaseId);
|
|
57
|
+
}
|
|
58
|
+
/** Record a full phase read for an explicit harness session. */
|
|
59
|
+
export function markPhaseReadForSessionId(sessionId, phaseId) {
|
|
60
|
+
markPhaseReadForSession(sessionId, phaseId);
|
|
61
|
+
}
|
|
62
|
+
/** Record a full task read in the default compatibility session. */
|
|
63
|
+
export function markTaskRead(taskId, _phaseId, _featureId) {
|
|
64
|
+
markTaskReadForSession(DEFAULT_SESSION_ID, taskId);
|
|
65
|
+
}
|
|
66
|
+
/** Record a full task read for an explicit harness session. */
|
|
67
|
+
export function markTaskReadForSessionId(sessionId, taskId) {
|
|
68
|
+
markTaskReadForSession(sessionId, taskId);
|
|
69
|
+
}
|
|
70
|
+
/** Record that a requirement was explicitly read in the default session. */
|
|
71
|
+
export function markRequirementRead(requirementId) {
|
|
72
|
+
stateFor(DEFAULT_SESSION_ID).requirements.add(requirementId);
|
|
73
|
+
}
|
|
74
|
+
/** Record that a requirement was explicitly read for an explicit session. */
|
|
75
|
+
export function markRequirementReadForSessionId(sessionId, requirementId) {
|
|
76
|
+
stateFor(sessionId).requirements.add(requirementId);
|
|
77
|
+
}
|
|
78
|
+
function orderedEligibility(sessionId, taskId, phaseId, featureId) {
|
|
79
|
+
const state = stateFor(sessionId);
|
|
80
|
+
const taskSequence = state.tasks.get(taskId);
|
|
81
|
+
if (taskSequence === undefined) {
|
|
82
|
+
return { eligible: false, reason: "Read this exact task with full=true first." };
|
|
83
|
+
}
|
|
84
|
+
const phaseSequence = state.phases.get(phaseId);
|
|
85
|
+
if (phaseSequence === undefined || phaseSequence <= taskSequence) {
|
|
86
|
+
return { eligible: false, reason: "After reading the task, read its parent phase with full=true." };
|
|
87
|
+
}
|
|
88
|
+
if (!featureId)
|
|
89
|
+
return { eligible: true, reason: "" };
|
|
90
|
+
const featureSequence = state.features.get(featureId);
|
|
91
|
+
if (featureSequence === undefined || featureSequence <= phaseSequence) {
|
|
92
|
+
return { eligible: false, reason: "After reading the phase, read its parent feature with full=true." };
|
|
93
|
+
}
|
|
94
|
+
return { eligible: true, reason: "" };
|
|
95
|
+
}
|
|
96
|
+
function validSessionInfo(entity, sessionId) {
|
|
97
|
+
const entry = entity?.sessionInfo?.find((candidate) => candidate.sessionId === sessionId);
|
|
98
|
+
return Boolean(entity && entry && entity.updatedAt <= entry.createdAt);
|
|
99
|
+
}
|
|
100
|
+
function persistedEligibility(input) {
|
|
101
|
+
if (!validSessionInfo(input.task, input.sessionId) || !validSessionInfo(input.phase, input.sessionId))
|
|
102
|
+
return false;
|
|
103
|
+
if (input.featureId && !validSessionInfo(input.feature, input.sessionId))
|
|
104
|
+
return false;
|
|
105
|
+
const requirementIds = input.requirementIds ?? [];
|
|
106
|
+
return requirementIds.every((id) => validSessionInfo(input.requirements?.find((requirement) => requirement.id === id), input.sessionId));
|
|
107
|
+
}
|
|
108
|
+
function hasStoredSessionAttestation(input) {
|
|
109
|
+
const entities = [input.task, input.phase, input.feature, ...(input.requirements ?? [])];
|
|
110
|
+
return entities.some((entity) => entity?.sessionInfo?.some((entry) => entry.sessionId === input.sessionId));
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Evaluate exact in-memory read ordering first, then a persisted attestation
|
|
114
|
+
* for the same session and current entity revisions. A previously persisted
|
|
115
|
+
* but now stale attestation always wins over stale in-memory ordering.
|
|
116
|
+
*/
|
|
117
|
+
export function contextReadEligibilityForSession(input) {
|
|
118
|
+
const ordered = orderedEligibility(input.sessionId, input.taskId, input.phaseId, input.featureId);
|
|
119
|
+
if (ordered.eligible)
|
|
120
|
+
return ordered;
|
|
121
|
+
if (input.task && input.phase && persistedEligibility(input))
|
|
122
|
+
return { eligible: true, reason: "" };
|
|
123
|
+
if (input.task && input.phase && hasStoredSessionAttestation(input)) {
|
|
124
|
+
return { eligible: false, reason: "Context changed since the last session read; reread the exact task, phase, feature, and linked requirements." };
|
|
125
|
+
}
|
|
126
|
+
return ordered;
|
|
127
|
+
}
|
|
128
|
+
/** Return true only when the persisted attestation covers the current revisions. */
|
|
129
|
+
export function hasValidSessionAttestation(input) {
|
|
130
|
+
return persistedEligibility(input);
|
|
131
|
+
}
|
|
132
|
+
/** Compatibility check for the default session and in-memory reads only. */
|
|
133
|
+
export function contextReadEligibility(taskId, phaseId, featureId) {
|
|
134
|
+
return orderedEligibility(DEFAULT_SESSION_ID, taskId, phaseId, featureId);
|
|
135
|
+
}
|
|
136
|
+
/** Legacy parent-read check retained for callers that only need independent parent state. */
|
|
137
|
+
export function hasReadParents(featureId, phaseId) {
|
|
138
|
+
const state = stateFor(DEFAULT_SESSION_ID);
|
|
139
|
+
return state.phases.has(phaseId) && (!featureId || state.features.has(featureId));
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Whether linked requirements are read in memory or have valid persisted
|
|
143
|
+
* attestations for the current session and entity revisions.
|
|
144
|
+
*/
|
|
145
|
+
export function hasReadRequirementsForSession(sessionId, requirementIds, requirements = []) {
|
|
146
|
+
const state = stateFor(sessionId);
|
|
147
|
+
return requirementIds.every((id) => state.requirements.has(id) || validSessionInfo(requirements.find((requirement) => requirement.id === id), sessionId));
|
|
148
|
+
}
|
|
149
|
+
/** Legacy requirement check for the default compatibility session. */
|
|
150
|
+
export function hasReadRequirements(requirementIds) {
|
|
151
|
+
return requirementIds.every((id) => stateFor(DEFAULT_SESSION_ID).requirements.has(id));
|
|
152
|
+
}
|
|
153
|
+
/** Clear one session's read state, or all state for legacy callers. */
|
|
154
|
+
export function invalidateReads(sessionId) {
|
|
155
|
+
if (sessionId) {
|
|
156
|
+
states.delete(normalizeSessionId(sessionId));
|
|
157
|
+
stateFor(sessionId);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
states.clear();
|
|
161
|
+
states.set(DEFAULT_SESSION_ID, newState());
|
|
162
|
+
}
|
|
163
|
+
/** Initialize an explicit session without clearing other harness sessions. */
|
|
164
|
+
export function startReadSession(sessionId) {
|
|
165
|
+
stateFor(sessionId);
|
|
166
|
+
}
|
|
167
|
+
/** Compatibility advisory for non-lifecycle callers. */
|
|
168
|
+
export function parentReadAdvisory(featureId, phaseId) {
|
|
169
|
+
const state = stateFor(DEFAULT_SESSION_ID);
|
|
170
|
+
if (state.phases.has(phaseId) && (!featureId || state.features.has(featureId)))
|
|
171
|
+
return "";
|
|
172
|
+
return "\n\n⚠️ READ REQUIRED before proceeding: read the parent phase and feature with full=true.";
|
|
173
|
+
}
|
|
174
|
+
/** Advisory text for the separate linked-requirements gate. */
|
|
175
|
+
export function requirementReadAdvisory(requirementIds) {
|
|
176
|
+
const state = stateFor(DEFAULT_SESSION_ID);
|
|
177
|
+
if (requirementIds.length === 0)
|
|
178
|
+
return "";
|
|
179
|
+
const unread = requirementIds.filter((id) => !state.requirements.has(id));
|
|
180
|
+
if (unread.length === 0)
|
|
181
|
+
return "";
|
|
182
|
+
return "\n\n⚠️ REQUIREMENTS READ REQUIRED before proceeding: read the requirements linked to this phase and feature.";
|
|
183
|
+
}
|
|
184
|
+
/** Snapshot for diagnostics. */
|
|
185
|
+
export function readTrackingSnapshot(sessionId) {
|
|
186
|
+
const state = stateFor(sessionId);
|
|
187
|
+
return {
|
|
188
|
+
features: [...state.features.keys()],
|
|
189
|
+
phases: [...state.phases.keys()],
|
|
190
|
+
requirements: [...state.requirements],
|
|
191
|
+
};
|
|
192
|
+
}
|
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,CA4OhH"}
|
package/dist/recap.js
CHANGED
|
@@ -33,20 +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
|
|
37
|
-
const
|
|
36
|
+
const checkpointedTasks = allTasks.filter(({ task }) => !["done", "canceled", "rejected"].includes(task.status) && task.pauseSnapshot);
|
|
37
|
+
const checkpointedT = checkpointedTasks.length;
|
|
38
38
|
const pendingDeviation = [...plan.project.workDeviations]
|
|
39
39
|
.filter((deviation) => deviation.state === "resume-required" || deviation.state === "resolved")
|
|
40
40
|
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))
|
|
41
41
|
.find((deviation) => allTasks.some(({ task }) => task.id === deviation.resumeTaskId
|
|
42
|
-
&& (task.status === "
|
|
42
|
+
&& (task.status === "planned" || task.status === "waiting" || task.status === "in-progress")));
|
|
43
43
|
const pendingResume = pendingDeviation
|
|
44
44
|
? allTasks.find(({ task }) => task.id === pendingDeviation.resumeTaskId)
|
|
45
45
|
: undefined;
|
|
46
|
-
const
|
|
46
|
+
const standaloneCheckpoints = checkpointedTasks
|
|
47
47
|
.filter(({ task }) => task.id !== pendingResume?.task.id)
|
|
48
48
|
.sort((left, right) => (right.task.pauseSnapshot?.pausedAt ?? "").localeCompare(left.task.pauseSnapshot?.pausedAt ?? ""));
|
|
49
|
-
const
|
|
49
|
+
const latestStandaloneCheckpoint = standaloneCheckpoints[0];
|
|
50
50
|
// Plan is fully complete: there is work and all of it is done, nothing active.
|
|
51
51
|
// (totalT > 0 guards the empty/unstarted case from looking "complete".)
|
|
52
52
|
const planComplete = totalT > 0 && doneT === totalT && doneP === totalP && doneF === totalF;
|
|
@@ -63,28 +63,34 @@ export async function buildRecap(st, web = {}, opts = {}) {
|
|
|
63
63
|
const featureAddCmd = cmd("/planner feature add", "planner-feature-add");
|
|
64
64
|
const phaseAddCmd = cmd("/planner phase add", "planner-phase-add");
|
|
65
65
|
const handoffShowCmd = cmd("/planner handoff show", "planner-handoff-show");
|
|
66
|
+
const featureShowCmd = cmd("/planner feature show", "planner-feature-show");
|
|
67
|
+
const phaseShowCmd = cmd("/planner phase show", "planner-phase-show");
|
|
68
|
+
const taskShowCmd = cmd("/planner task show", "planner-task-show");
|
|
66
69
|
const lines = [];
|
|
67
70
|
lines.push(italian ? "## Ripresa planner" : "## Planner recap");
|
|
68
71
|
const name = plan.project.name || "(unnamed project)";
|
|
69
72
|
lines.push(`${italian ? "Progetto" : "Project"}: ${name}${plan.project.goal ? " — " + plan.project.goal : ""}`);
|
|
70
73
|
lines.push(italian
|
|
71
|
-
? `Avanzamento: feature ${doneF}/${totalF} completate (${activeF} attive) · fasi ${doneP}/${totalP} completate (${activeP} attive) · task ${doneT}/${totalT} completati (${activeT} attivi, ${
|
|
72
|
-
: `Progress: Features ${doneF}/${totalF} done (${activeF} active) · Phases ${doneP}/${totalP} done (${activeP} active) · Tasks ${doneT}/${totalT} done (${activeT} active, ${
|
|
74
|
+
? `Avanzamento: feature ${doneF}/${totalF} completate (${activeF} attive) · fasi ${doneP}/${totalP} completate (${activeP} attive) · task ${doneT}/${totalT} completati (${activeT} attivi, ${checkpointedT} con checkpoint)`
|
|
75
|
+
: `Progress: Features ${doneF}/${totalF} done (${activeF} active) · Phases ${doneP}/${totalP} done (${activeP} active) · Tasks ${doneT}/${totalT} done (${activeT} active, ${checkpointedT} with checkpoints)`);
|
|
73
76
|
if (focusTask && focusPhase) {
|
|
74
77
|
const fr = focusFeature ? fref(focusFeature.number) : "?";
|
|
75
78
|
const pr = formatPhaseRef(focusPhase.number, focusFeature?.number);
|
|
76
79
|
const tr = tref(focusTask.task.number);
|
|
77
80
|
lines.push(`${italian ? "Focus corrente" : "Current focus"}: ${fr} — ${focusFeature?.name ?? "?"} / ${pr} — ${focusPhase.title} / ${tr} — ${focusTask.task.title} (in-progress)`);
|
|
81
|
+
lines.push("", italian
|
|
82
|
+
? `Questo task è in-progress (lavoro iniziato in una sessione precedente). Prima di continuare, rileggi il contesto completo: ${taskShowCmd} ${tr} (full=true), ${phaseShowCmd} ${pr} (full=true), ${featureShowCmd} ${fr} (full=true).`
|
|
83
|
+
: `⚠️ This task is in-progress (work started in a previous session). Before continuing, re-read the full context in this order: ${taskShowCmd} ${tr} (full=true), ${phaseShowCmd} ${pr} (full=true), ${featureShowCmd} ${fr} (full=true).`);
|
|
78
84
|
}
|
|
79
85
|
else if (pendingResume) {
|
|
80
86
|
const feature = feats.find((entry) => entry.id === pendingResume.phase.featureId);
|
|
81
87
|
const ref = `${formatPhaseRef(pendingResume.phase.number, feature?.number)}/${tref(pendingResume.task.number)}`;
|
|
82
88
|
lines.push(`${italian ? "Focus corrente" : "Current focus"}: ${italian ? "ripresa obbligatoria" : "resume required"} — ${ref} — ${pendingResume.task.title} (${pendingResume.task.status})`);
|
|
83
89
|
}
|
|
84
|
-
else if (
|
|
85
|
-
const feature = feats.find((entry) => entry.id ===
|
|
86
|
-
const ref = `${formatPhaseRef(
|
|
87
|
-
lines.push(`${italian ? "Focus corrente" : "Current focus"}: ${italian ? "
|
|
90
|
+
else if (latestStandaloneCheckpoint) {
|
|
91
|
+
const feature = feats.find((entry) => entry.id === latestStandaloneCheckpoint.phase.featureId);
|
|
92
|
+
const ref = `${formatPhaseRef(latestStandaloneCheckpoint.phase.number, feature?.number)}/${tref(latestStandaloneCheckpoint.task.number)}`;
|
|
93
|
+
lines.push(`${italian ? "Focus corrente" : "Current focus"}: ${italian ? "checkpoint da valutare" : "checkpoint to evaluate"} — ${ref} — ${latestStandaloneCheckpoint.task.title}`);
|
|
88
94
|
}
|
|
89
95
|
else if (handoffs.length > 0) {
|
|
90
96
|
const top = handoffs[0];
|
|
@@ -107,15 +113,15 @@ export async function buildRecap(st, web = {}, opts = {}) {
|
|
|
107
113
|
const feature = feats.find((entry) => entry.id === pendingResume.phase.featureId);
|
|
108
114
|
const ref = `${formatPhaseRef(pendingResume.phase.number, feature?.number)}/${tref(pendingResume.task.number)}`;
|
|
109
115
|
lines.push(italian
|
|
110
|
-
? `
|
|
111
|
-
: `
|
|
116
|
+
? `Avviso ripresa: valuta se riprendere ${ref} con ${taskStartCmd}. Il checkpoint del task va letto prima di decidere se tornare su questo lavoro o proseguire altrove.`
|
|
117
|
+
: `Resume advisory: evaluate whether to resume ${ref} with ${taskStartCmd}. Read its checkpoint before deciding whether to return to this work or continue elsewhere.`);
|
|
112
118
|
}
|
|
113
|
-
else if (
|
|
114
|
-
const feature = feats.find((entry) => entry.id ===
|
|
115
|
-
const ref = `${formatPhaseRef(
|
|
119
|
+
else if (latestStandaloneCheckpoint) {
|
|
120
|
+
const feature = feats.find((entry) => entry.id === latestStandaloneCheckpoint.phase.featureId);
|
|
121
|
+
const ref = `${formatPhaseRef(latestStandaloneCheckpoint.phase.number, feature?.number)}/${tref(latestStandaloneCheckpoint.task.number)}`;
|
|
116
122
|
lines.push(italian
|
|
117
|
-
? `
|
|
118
|
-
: `
|
|
123
|
+
? `Avviso ripresa: valuta il checkpoint più recente, ${ref}, con ${taskStartCmd}, prima di scegliere se tornare su quel lavoro o avviarne altro.`
|
|
124
|
+
: `Resume advisory: evaluate the newest checkpoint, ${ref}, with ${taskStartCmd} before deciding whether to return to it or start other work.`);
|
|
119
125
|
}
|
|
120
126
|
else if (handoffs.length > 0) {
|
|
121
127
|
lines.push(italian
|
|
@@ -137,11 +143,11 @@ export async function buildRecap(st, web = {}, opts = {}) {
|
|
|
137
143
|
const feature = feats.find((entry) => entry.id === pendingResume.phase.featureId);
|
|
138
144
|
const ref = `${formatPhaseRef(pendingResume.phase.number, feature?.number)}/${tref(pendingResume.task.number)}`;
|
|
139
145
|
const snapshot = pendingResume.task.pauseSnapshot ?? pendingDeviation?.snapshot;
|
|
140
|
-
lines.push("", italian ? "##
|
|
146
|
+
lines.push("", italian ? "## Avviso ripresa task" : "## Task resume advisory", `${ref} — ${pendingResume.task.title}`, snapshot ? `${italian ? "Perché è stato salvato il checkpoint" : "Checkpoint reason"}: ${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 suggerita: valuta ${taskStartCmd} ${ref}` : `Suggested action: evaluate ${taskStartCmd} ${ref}`);
|
|
141
147
|
}
|
|
142
|
-
if (
|
|
143
|
-
lines.push("", italian ? `##
|
|
144
|
-
for (const entry of
|
|
148
|
+
if (standaloneCheckpoints.length > 0) {
|
|
149
|
+
lines.push("", italian ? `## Checkpoint salvati (${standaloneCheckpoints.length})` : `## Saved checkpoints (${standaloneCheckpoints.length})`);
|
|
150
|
+
for (const entry of standaloneCheckpoints) {
|
|
145
151
|
const feature = feats.find((item) => item.id === entry.phase.featureId);
|
|
146
152
|
const ref = `${formatPhaseRef(entry.phase.number, feature?.number)}/${tref(entry.task.number)}`;
|
|
147
153
|
const snapshot = entry.task.pauseSnapshot;
|
|
@@ -152,7 +158,7 @@ export async function buildRecap(st, web = {}, opts = {}) {
|
|
|
152
158
|
const top = handoffs[0];
|
|
153
159
|
lines.push("", italian ? `## Handoff di fase pendenti (${handoffs.length})` : `## Pending phase handoffs (${handoffs.length})`);
|
|
154
160
|
handoffs.forEach((h, i) => lines.push(`[${i + 1}] ${h.compositeRef} — ${h.updatedAt} — "${h.firstLine}"`));
|
|
155
|
-
if (pendingResume ||
|
|
161
|
+
if (pendingResume || latestStandaloneCheckpoint) {
|
|
156
162
|
lines.push("", italian
|
|
157
163
|
? "→ Questi handoff restano disponibili come contesto, ma prima risolvi il task da riprendere indicato sopra."
|
|
158
164
|
: "→ These handoffs remain available as context, but first resolve the task resume shown above.");
|
|
@@ -173,7 +179,7 @@ export async function buildRecap(st, web = {}, opts = {}) {
|
|
|
173
179
|
? `Piano completo — aggiungi una nuova feature (${featureAddCmd}) o fase (${phaseAddCmd}) per continuare.`
|
|
174
180
|
: `Plan complete — add a new feature (${featureAddCmd}) or phase (${phaseAddCmd}) to continue.`);
|
|
175
181
|
}
|
|
176
|
-
else if (activeT === 0 && !pendingResume &&
|
|
182
|
+
else if (activeT === 0 && !pendingResume && standaloneCheckpoints.length === 0) {
|
|
177
183
|
lines.push("", italian
|
|
178
184
|
? `Nessun handoff pendente e nessun task in-progress. Usa ${taskAddCmd} / ${taskStartCmd} per iniziare.`
|
|
179
185
|
: `No phase handoff pending and no task in-progress. Use ${taskAddCmd} / ${taskStartCmd} to begin work.`);
|