@agent-plan/core 0.2.29 → 0.3.1
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/accepted-decision-guard.d.ts +94 -0
- package/dist/accepted-decision-guard.d.ts.map +1 -0
- package/dist/accepted-decision-guard.js +110 -0
- package/dist/checklist.d.ts +28 -0
- package/dist/checklist.d.ts.map +1 -1
- package/dist/checklist.js +59 -0
- package/dist/feature-delete.d.ts +62 -0
- package/dist/feature-delete.d.ts.map +1 -0
- package/dist/feature-delete.js +49 -0
- package/dist/guard-decision.d.ts +128 -0
- package/dist/guard-decision.d.ts.map +1 -0
- package/dist/guard-decision.js +269 -0
- package/dist/guard-state.d.ts +14 -0
- package/dist/guard-state.d.ts.map +1 -0
- package/dist/guard-state.js +35 -0
- package/dist/handoff-context.d.ts +130 -1
- package/dist/handoff-context.d.ts.map +1 -1
- package/dist/handoff-context.js +167 -9
- package/dist/handoff-reply.d.ts +113 -0
- package/dist/handoff-reply.d.ts.map +1 -0
- package/dist/handoff-reply.js +279 -0
- package/dist/index.d.ts +18 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -1
- package/dist/load-reply.d.ts +63 -0
- package/dist/load-reply.d.ts.map +1 -0
- package/dist/load-reply.js +64 -0
- package/dist/mutation-reply.d.ts +105 -0
- package/dist/mutation-reply.d.ts.map +1 -0
- package/dist/mutation-reply.js +203 -0
- package/dist/naming.d.ts +1 -1
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +1 -1
- package/dist/on-hold.d.ts +55 -0
- package/dist/on-hold.d.ts.map +1 -0
- package/dist/on-hold.js +123 -0
- package/dist/package-version.d.ts +13 -0
- package/dist/package-version.d.ts.map +1 -1
- package/dist/package-version.js +32 -0
- package/dist/pagination.d.ts +19 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +15 -0
- package/dist/payload-fallback.js +1 -1
- package/dist/plan-store.d.ts +141 -8
- package/dist/plan-store.d.ts.map +1 -1
- package/dist/plan-store.js +459 -87
- package/dist/project-context-migration.d.ts +5 -0
- package/dist/project-context-migration.d.ts.map +1 -1
- package/dist/project-context-migration.js +13 -0
- package/dist/project-context.d.ts +397 -0
- package/dist/project-context.d.ts.map +1 -0
- package/dist/project-context.js +294 -0
- package/dist/recap.d.ts.map +1 -1
- package/dist/recap.js +33 -0
- package/dist/recommend-reply.d.ts +51 -0
- package/dist/recommend-reply.d.ts.map +1 -0
- package/dist/recommend-reply.js +93 -0
- package/dist/refs.d.ts +16 -0
- package/dist/refs.d.ts.map +1 -1
- package/dist/refs.js +38 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/schema.d.ts +7 -2
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +8 -3
- package/dist/stranded-phase.d.ts +73 -0
- package/dist/stranded-phase.d.ts.map +1 -0
- package/dist/stranded-phase.js +93 -0
- package/dist/task-completion-gate.d.ts +84 -0
- package/dist/task-completion-gate.d.ts.map +1 -0
- package/dist/task-completion-gate.js +72 -0
- package/dist/task-context.d.ts +1 -0
- package/dist/task-context.d.ts.map +1 -1
- package/dist/task-context.js +42 -14
- package/dist/task-lifecycle.d.ts +12 -0
- package/dist/task-lifecycle.d.ts.map +1 -0
- package/dist/task-lifecycle.js +23 -0
- package/dist/task-order-context.d.ts +72 -0
- package/dist/task-order-context.d.ts.map +1 -0
- package/dist/task-order-context.js +82 -0
- package/dist/task-selection.d.ts +4 -1
- package/dist/task-selection.d.ts.map +1 -1
- package/dist/task-selection.js +36 -14
- package/dist/task-start-outcome.d.ts +26 -0
- package/dist/task-start-outcome.d.ts.map +1 -1
- package/dist/task-start-outcome.js +23 -0
- package/dist/text-bounds.d.ts +32 -0
- package/dist/text-bounds.d.ts.map +1 -0
- package/dist/text-bounds.js +43 -0
- package/dist/write-coordination.d.ts +2 -2
- package/dist/write-coordination.js +2 -2
- package/package.json +1 -1
- package/planner-skill.md +30 -12
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accepted Decision ownership rule and the compatibility guards that enforce
|
|
3
|
+
* it, shared by every adapter (MCP, Pi, HTTP, and any future harness).
|
|
4
|
+
*
|
|
5
|
+
* The rule: a decision has exactly one owner. Project-wide decisions belong
|
|
6
|
+
* only to project.acceptedDecisions; feature-wide decisions only to that
|
|
7
|
+
* feature; phase-specific decisions only to that phase; genuinely
|
|
8
|
+
* task-local decisions only to that task. Nothing duplicates a decision
|
|
9
|
+
* across two owners by default — see the semantic createAcceptedDecision /
|
|
10
|
+
* updateAcceptedDecision / deleteAcceptedDecision operations in
|
|
11
|
+
* plan-store.ts, which already write to exactly one target.
|
|
12
|
+
*
|
|
13
|
+
* Two things previously worked against that rule, both fixed here once so
|
|
14
|
+
* no adapter has to restate the wording (AGENTS.md rule 4):
|
|
15
|
+
*
|
|
16
|
+
* 1. `decision_record` (Pi) wrote the same decision onto both a feature and
|
|
17
|
+
* a phase. It no longer writes anything; see
|
|
18
|
+
* buildDecisionRecordRedirectReply.
|
|
19
|
+
* 2. The legacy free-form `decisions: string[]` field on project, phase,
|
|
20
|
+
* and task let an agent keep "recording" decisions in a place nothing
|
|
21
|
+
* treats as authoritative (buildBoundedAcceptedDecisionContext only ever
|
|
22
|
+
* reads acceptedDecisions). Every update tool that exposes this field
|
|
23
|
+
* now rejects new writes to it with LEGACY_DECISIONS_ARRAY_READ_ONLY_MESSAGE,
|
|
24
|
+
* the same way raw acceptedDecisions replacement was already rejected.
|
|
25
|
+
* Existing legacy content is left exactly as persisted — read-only, not
|
|
26
|
+
* deleted or migrated; only project-level legacy decisions get folded
|
|
27
|
+
* into acceptedDecisions, and only through the existing explicit
|
|
28
|
+
* project-context migration, never automatically here.
|
|
29
|
+
*/
|
|
30
|
+
import { type PlanStore } from "./plan-store.js";
|
|
31
|
+
import type { MutationReply } from "./mutation-reply.js";
|
|
32
|
+
export declare const ACCEPTED_DECISION_OWNERSHIP_RULE = "A decision has exactly one owner: the project when it is project-wide, a feature when it is feature-wide, a phase when it is phase-specific, or a task when the decision is genuinely local to that task's work and will not outlive it. Never record the same decision on more than one owner.";
|
|
33
|
+
export declare const ACCEPTED_DECISION_RAW_REPLACEMENT_DISABLED_MESSAGE = "Raw acceptedDecisions replacement is disabled. Use the accepted-decision create, update, or delete operations (accepted_decision_create/update/delete, planner-accepted-decision-create/update/delete, or the /accepted-decisions HTTP endpoints) so IDs and acceptedAt are preserved.";
|
|
34
|
+
export declare const LEGACY_DECISIONS_ARRAY_READ_ONLY_MESSAGE = "The legacy free-form decisions list is retained read-only for history; it is not decision authority. A decision has exactly one owner: the project when it is project-wide, a feature when it is feature-wide, a phase when it is phase-specific, or a task when the decision is genuinely local to that task's work and will not outlive it. Never record the same decision on more than one owner. Record new durable decisions with the accepted-decision create operation on the owner that actually holds this one.";
|
|
35
|
+
export declare const ACCEPTED_DECISION_SEMANTIC_MUTATION_REQUIRED_ERROR_CODE = "ACCEPTED_DECISION_SEMANTIC_MUTATION_REQUIRED";
|
|
36
|
+
export declare const LEGACY_DECISIONS_ARRAY_READ_ONLY_ERROR_CODE = "LEGACY_DECISIONS_ARRAY_READ_ONLY";
|
|
37
|
+
/**
|
|
38
|
+
* decision_record's compatibility reply: a typed redirect, never a write.
|
|
39
|
+
* It cannot dual-write because it performs no write at all — the caller
|
|
40
|
+
* must pick exactly one owner and call the semantic create operation
|
|
41
|
+
* itself. Kept here so every current or future surface offering a
|
|
42
|
+
* decision_record-shaped compatibility tool renders identical guidance
|
|
43
|
+
* instead of drifting.
|
|
44
|
+
*/
|
|
45
|
+
export interface DecisionRecordRedirectInput {
|
|
46
|
+
/** Resolved, human-readable feature ref (e.g. "F005"), for the feature suggestion. */
|
|
47
|
+
featureRef: string;
|
|
48
|
+
/** Resolved, human-readable phase ref (e.g. "P102(F005)"), for the phase suggestion. */
|
|
49
|
+
phaseRef: string;
|
|
50
|
+
}
|
|
51
|
+
export interface DecisionRecordRedirectReply {
|
|
52
|
+
text: string;
|
|
53
|
+
structured: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
export declare function buildDecisionRecordRedirectReply(input: DecisionRecordRedirectInput): DecisionRecordRedirectReply;
|
|
56
|
+
/**
|
|
57
|
+
* Resolve an Accepted Decision target (project/feature/phase/task) to its
|
|
58
|
+
* owning entity id and a display ref. Both adapters carried an identical
|
|
59
|
+
* copy for accepted_decision_create/update/delete; kept here so target
|
|
60
|
+
* resolution can never drift from `resolveFeatureRefStrict`/
|
|
61
|
+
* `findPhaseByRef`/`findTaskByRef` — the same ref grammar this project uses
|
|
62
|
+
* everywhere else.
|
|
63
|
+
*/
|
|
64
|
+
export type AcceptedDecisionTargetType = "project" | "feature" | "phase" | "task";
|
|
65
|
+
export type AcceptedDecisionTargetResolution = {
|
|
66
|
+
ok: true;
|
|
67
|
+
owner: {
|
|
68
|
+
kind: "project";
|
|
69
|
+
} | {
|
|
70
|
+
kind: "feature";
|
|
71
|
+
featureId: string;
|
|
72
|
+
} | {
|
|
73
|
+
kind: "phase";
|
|
74
|
+
phaseId: string;
|
|
75
|
+
} | {
|
|
76
|
+
kind: "task";
|
|
77
|
+
phaseId: string;
|
|
78
|
+
taskId: string;
|
|
79
|
+
};
|
|
80
|
+
ref: string;
|
|
81
|
+
} | {
|
|
82
|
+
ok: false;
|
|
83
|
+
error: string;
|
|
84
|
+
};
|
|
85
|
+
export declare function resolveAcceptedDecisionTarget(st: PlanStore, targetType: AcceptedDecisionTargetType, targetRef: string | undefined): Promise<AcceptedDecisionTargetResolution>;
|
|
86
|
+
/**
|
|
87
|
+
* Render a PlanStoreError raised by an Accepted Decision create/update/delete
|
|
88
|
+
* as a text/structured pair, or rethrow when it isn't one of those (a
|
|
89
|
+
* different failure is the caller's to handle). Both adapters carried an
|
|
90
|
+
* identical copy of this decision; kept here so the type check and the
|
|
91
|
+
* message can never drift. Each adapter still wraps its own envelope.
|
|
92
|
+
*/
|
|
93
|
+
export declare function acceptedDecisionMutationFailureReply(error: unknown, outcome: "created" | "updated" | "deleted"): MutationReply;
|
|
94
|
+
//# sourceMappingURL=accepted-decision-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accepted-decision-guard.d.ts","sourceRoot":"","sources":["../src/accepted-decision-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAGjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,eAAO,MAAM,gCAAgC,oSACsP,CAAC;AAEpS,eAAO,MAAM,kDAAkD,2RAC2N,CAAC;AAE3R,eAAO,MAAM,wCAAwC,6fAC2M,CAAC;AAEjQ,eAAO,MAAM,uDAAuD,iDAAiD,CAAC;AACtH,eAAO,MAAM,2CAA2C,qCAAqC,CAAC;AAE9F;;;;;;;GAOG;AACH,MAAM,WAAW,2BAA2B;IAC1C,sFAAsF;IACtF,UAAU,EAAE,MAAM,CAAC;IACnB,wFAAwF;IACxF,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AAED,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,2BAA2B,GAAG,2BAA2B,CA0BhH;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAC;AAClF,MAAM,MAAM,gCAAgC,GACxC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE;QAAE,IAAI,EAAE,SAAS,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACvL;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjC,wBAAsB,6BAA6B,CACjD,EAAE,EAAE,SAAS,EACb,UAAU,EAAE,0BAA0B,EACtC,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,OAAO,CAAC,gCAAgC,CAAC,CA0B3C;AAED;;;;;;GAMG;AACH,wBAAgB,oCAAoC,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,aAAa,CAM9H"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accepted Decision ownership rule and the compatibility guards that enforce
|
|
3
|
+
* it, shared by every adapter (MCP, Pi, HTTP, and any future harness).
|
|
4
|
+
*
|
|
5
|
+
* The rule: a decision has exactly one owner. Project-wide decisions belong
|
|
6
|
+
* only to project.acceptedDecisions; feature-wide decisions only to that
|
|
7
|
+
* feature; phase-specific decisions only to that phase; genuinely
|
|
8
|
+
* task-local decisions only to that task. Nothing duplicates a decision
|
|
9
|
+
* across two owners by default — see the semantic createAcceptedDecision /
|
|
10
|
+
* updateAcceptedDecision / deleteAcceptedDecision operations in
|
|
11
|
+
* plan-store.ts, which already write to exactly one target.
|
|
12
|
+
*
|
|
13
|
+
* Two things previously worked against that rule, both fixed here once so
|
|
14
|
+
* no adapter has to restate the wording (AGENTS.md rule 4):
|
|
15
|
+
*
|
|
16
|
+
* 1. `decision_record` (Pi) wrote the same decision onto both a feature and
|
|
17
|
+
* a phase. It no longer writes anything; see
|
|
18
|
+
* buildDecisionRecordRedirectReply.
|
|
19
|
+
* 2. The legacy free-form `decisions: string[]` field on project, phase,
|
|
20
|
+
* and task let an agent keep "recording" decisions in a place nothing
|
|
21
|
+
* treats as authoritative (buildBoundedAcceptedDecisionContext only ever
|
|
22
|
+
* reads acceptedDecisions). Every update tool that exposes this field
|
|
23
|
+
* now rejects new writes to it with LEGACY_DECISIONS_ARRAY_READ_ONLY_MESSAGE,
|
|
24
|
+
* the same way raw acceptedDecisions replacement was already rejected.
|
|
25
|
+
* Existing legacy content is left exactly as persisted — read-only, not
|
|
26
|
+
* deleted or migrated; only project-level legacy decisions get folded
|
|
27
|
+
* into acceptedDecisions, and only through the existing explicit
|
|
28
|
+
* project-context migration, never automatically here.
|
|
29
|
+
*/
|
|
30
|
+
import { PlanStoreError } from "./plan-store.js";
|
|
31
|
+
import { formatFeatureRef, formatPhaseRef, featureNumberOfPhase } from "./naming.js";
|
|
32
|
+
import { findPhaseByRef, findTaskByRef, resolveFeatureRefStrict } from "./refs.js";
|
|
33
|
+
export const ACCEPTED_DECISION_OWNERSHIP_RULE = "A decision has exactly one owner: the project when it is project-wide, a feature when it is feature-wide, a phase when it is phase-specific, or a task when the decision is genuinely local to that task's work and will not outlive it. Never record the same decision on more than one owner.";
|
|
34
|
+
export const ACCEPTED_DECISION_RAW_REPLACEMENT_DISABLED_MESSAGE = "Raw acceptedDecisions replacement is disabled. Use the accepted-decision create, update, or delete operations (accepted_decision_create/update/delete, planner-accepted-decision-create/update/delete, or the /accepted-decisions HTTP endpoints) so IDs and acceptedAt are preserved.";
|
|
35
|
+
export const LEGACY_DECISIONS_ARRAY_READ_ONLY_MESSAGE = `The legacy free-form decisions list is retained read-only for history; it is not decision authority. ${ACCEPTED_DECISION_OWNERSHIP_RULE} Record new durable decisions with the accepted-decision create operation on the owner that actually holds this one.`;
|
|
36
|
+
export const ACCEPTED_DECISION_SEMANTIC_MUTATION_REQUIRED_ERROR_CODE = "ACCEPTED_DECISION_SEMANTIC_MUTATION_REQUIRED";
|
|
37
|
+
export const LEGACY_DECISIONS_ARRAY_READ_ONLY_ERROR_CODE = "LEGACY_DECISIONS_ARRAY_READ_ONLY";
|
|
38
|
+
export function buildDecisionRecordRedirectReply(input) {
|
|
39
|
+
const { featureRef, phaseRef } = input;
|
|
40
|
+
return {
|
|
41
|
+
text: [
|
|
42
|
+
`decision_record no longer writes a decision on both ${featureRef} and ${phaseRef}.`,
|
|
43
|
+
ACCEPTED_DECISION_OWNERSHIP_RULE,
|
|
44
|
+
"Call accepted_decision_create with the single owner this decision actually belongs to:",
|
|
45
|
+
`- Project-wide: targetType "project" (omit targetRef).`,
|
|
46
|
+
`- Feature-wide: targetType "feature", targetRef "${featureRef}".`,
|
|
47
|
+
`- Phase-specific: targetType "phase", targetRef "${phaseRef}".`,
|
|
48
|
+
`- Task-local: targetType "task", targetRef the exact task composite ref.`,
|
|
49
|
+
].join("\n"),
|
|
50
|
+
structured: {
|
|
51
|
+
recorded: false,
|
|
52
|
+
written: false,
|
|
53
|
+
redirected: true,
|
|
54
|
+
redirectTool: "accepted_decision_create",
|
|
55
|
+
ownershipRule: ACCEPTED_DECISION_OWNERSHIP_RULE,
|
|
56
|
+
suggestions: {
|
|
57
|
+
project: { targetType: "project" },
|
|
58
|
+
feature: { targetType: "feature", targetRef: featureRef },
|
|
59
|
+
phase: { targetType: "phase", targetRef: phaseRef },
|
|
60
|
+
task: { targetType: "task", targetRef: null },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export async function resolveAcceptedDecisionTarget(st, targetType, targetRef) {
|
|
66
|
+
if (targetType === "project") {
|
|
67
|
+
const project = await st.loadProject();
|
|
68
|
+
return { ok: true, owner: { kind: "project" }, ref: project.name };
|
|
69
|
+
}
|
|
70
|
+
const ref = targetRef?.trim();
|
|
71
|
+
if (!ref)
|
|
72
|
+
return { ok: false, error: `targetRef is required for ${targetType} accepted decisions.` };
|
|
73
|
+
const [featuresDoc, phases] = await Promise.all([st.loadFeatures(), st.loadAllPhases()]);
|
|
74
|
+
const features = featuresDoc.features;
|
|
75
|
+
if (targetType === "feature") {
|
|
76
|
+
const resolved = resolveFeatureRefStrict(features, ref);
|
|
77
|
+
if (!resolved.ok)
|
|
78
|
+
return { ok: false, error: resolved.error };
|
|
79
|
+
return { ok: true, owner: { kind: "feature", featureId: resolved.feature.id }, ref: formatFeatureRef(resolved.feature.number) };
|
|
80
|
+
}
|
|
81
|
+
if (targetType === "phase") {
|
|
82
|
+
const phase = findPhaseByRef(phases, features, ref);
|
|
83
|
+
if (!phase)
|
|
84
|
+
return { ok: false, error: `Phase not found: ${ref}` };
|
|
85
|
+
return { ok: true, owner: { kind: "phase", phaseId: phase.id }, ref: formatPhaseRef(phase.number, featureNumberOfPhase(phase, features)) };
|
|
86
|
+
}
|
|
87
|
+
const found = findTaskByRef(phases, features, ref);
|
|
88
|
+
if (!found)
|
|
89
|
+
return { ok: false, error: `Task not found: ${ref}` };
|
|
90
|
+
return {
|
|
91
|
+
ok: true,
|
|
92
|
+
owner: { kind: "task", phaseId: found.phase.id, taskId: found.task.id },
|
|
93
|
+
ref: `${formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features))}/T${String(found.task.number).padStart(3, "0")}`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Render a PlanStoreError raised by an Accepted Decision create/update/delete
|
|
98
|
+
* as a text/structured pair, or rethrow when it isn't one of those (a
|
|
99
|
+
* different failure is the caller's to handle). Both adapters carried an
|
|
100
|
+
* identical copy of this decision; kept here so the type check and the
|
|
101
|
+
* message can never drift. Each adapter still wraps its own envelope.
|
|
102
|
+
*/
|
|
103
|
+
export function acceptedDecisionMutationFailureReply(error, outcome) {
|
|
104
|
+
if (!(error instanceof PlanStoreError) || !String(error.details?.errorCode ?? "").startsWith("ACCEPTED_DECISION_"))
|
|
105
|
+
throw error;
|
|
106
|
+
return {
|
|
107
|
+
text: `❌ Accepted Decision mutation failed [${error.details?.errorCode}]: ${error.message}`,
|
|
108
|
+
structured: { [outcome]: false, ...error.details },
|
|
109
|
+
};
|
|
110
|
+
}
|
package/dist/checklist.d.ts
CHANGED
|
@@ -13,4 +13,32 @@ export declare function addChecklistItem(items: ChecklistItem[], taskId: string,
|
|
|
13
13
|
export declare function removeChecklistItem(items: ChecklistItem[], selector: string): ChecklistItem | undefined;
|
|
14
14
|
/** Tick/untick the matched item in place. checked omitted → toggle. Returns the item, or undefined. */
|
|
15
15
|
export declare function toggleChecklistItem(items: ChecklistItem[], selector: string, checked?: boolean): ChecklistItem | undefined;
|
|
16
|
+
export interface ChecklistReplacement {
|
|
17
|
+
items: ChecklistItem[];
|
|
18
|
+
/** Titles of previously-ticked items whose tick could not be carried over.
|
|
19
|
+
* Empty on the common paths. Callers MUST surface a non-empty list: a
|
|
20
|
+
* success result that drops ticks without saying so is the defect this
|
|
21
|
+
* function exists to prevent. */
|
|
22
|
+
lostTicks: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build the replacement list for a whole-checklist update, carrying tick state
|
|
26
|
+
* across from the items being replaced.
|
|
27
|
+
*
|
|
28
|
+
* The input is plain titles, so item identity has to be reconstructed. Two
|
|
29
|
+
* passes, in this order:
|
|
30
|
+
*
|
|
31
|
+
* 1. Exact trimmed title. Order-independent, so reordering a list keeps every
|
|
32
|
+
* tick. Each surviving item is consumed at most once, so duplicate titles
|
|
33
|
+
* carry over one tick each rather than multiplying.
|
|
34
|
+
* 2. Position, but ONLY when the list length is unchanged. This is what makes
|
|
35
|
+
* renaming an item keep its tick — the reported failure case. Restricting
|
|
36
|
+
* it to equal-length lists means an insertion or deletion never shifts a
|
|
37
|
+
* tick onto a neighbour.
|
|
38
|
+
*
|
|
39
|
+
* Anything still unmatched is a new item and starts unchecked; if it displaced
|
|
40
|
+
* a ticked item, that title is reported in `lostTicks` rather than dropped in
|
|
41
|
+
* silence.
|
|
42
|
+
*/
|
|
43
|
+
export declare function replaceChecklist(existing: ChecklistItem[], titles: string[], taskId: string): ChecklistReplacement;
|
|
16
44
|
//# sourceMappingURL=checklist.d.ts.map
|
package/dist/checklist.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checklist.d.ts","sourceRoot":"","sources":["../src/checklist.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD;;;;;;GAMG;AAEH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAerG;AAED,iHAAiH;AACjH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,CAKrG;AAED,oHAAoH;AACpH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CASvG;AAED,uGAAuG;AACvG,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,aAAa,GAAG,SAAS,CAK1H"}
|
|
1
|
+
{"version":3,"file":"checklist.d.ts","sourceRoot":"","sources":["../src/checklist.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD;;;;;;GAMG;AAEH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAerG;AAED,iHAAiH;AACjH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,CAKrG;AAED,oHAAoH;AACpH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CASvG;AAED,uGAAuG;AACvG,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,aAAa,GAAG,SAAS,CAK1H;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB;;;sCAGkC;IAClC,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,oBAAoB,CA0ClH"}
|
package/dist/checklist.js
CHANGED
|
@@ -50,3 +50,62 @@ export function toggleChecklistItem(items, selector, checked) {
|
|
|
50
50
|
found.checked = checked ?? !found.checked;
|
|
51
51
|
return found;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the replacement list for a whole-checklist update, carrying tick state
|
|
55
|
+
* across from the items being replaced.
|
|
56
|
+
*
|
|
57
|
+
* The input is plain titles, so item identity has to be reconstructed. Two
|
|
58
|
+
* passes, in this order:
|
|
59
|
+
*
|
|
60
|
+
* 1. Exact trimmed title. Order-independent, so reordering a list keeps every
|
|
61
|
+
* tick. Each surviving item is consumed at most once, so duplicate titles
|
|
62
|
+
* carry over one tick each rather than multiplying.
|
|
63
|
+
* 2. Position, but ONLY when the list length is unchanged. This is what makes
|
|
64
|
+
* renaming an item keep its tick — the reported failure case. Restricting
|
|
65
|
+
* it to equal-length lists means an insertion or deletion never shifts a
|
|
66
|
+
* tick onto a neighbour.
|
|
67
|
+
*
|
|
68
|
+
* Anything still unmatched is a new item and starts unchecked; if it displaced
|
|
69
|
+
* a ticked item, that title is reported in `lostTicks` rather than dropped in
|
|
70
|
+
* silence.
|
|
71
|
+
*/
|
|
72
|
+
export function replaceChecklist(existing, titles, taskId) {
|
|
73
|
+
const cleanTitles = titles.map((title) => title.trim()).filter((title) => title.length > 0);
|
|
74
|
+
const consumed = new Set();
|
|
75
|
+
const carried = new Array(cleanTitles.length).fill(false);
|
|
76
|
+
// Tracks whether pass 1 found an exact-title match, independent of that
|
|
77
|
+
// match's checked value. `carried[index]` alone cannot stand in for this:
|
|
78
|
+
// an exact match to an unticked item legitimately produces carried=false,
|
|
79
|
+
// and pass 2 must not treat that as "unmatched" and overwrite it with a
|
|
80
|
+
// stray tick from whatever unrelated item previously sat at this position.
|
|
81
|
+
const matchedByTitle = new Array(cleanTitles.length).fill(false);
|
|
82
|
+
cleanTitles.forEach((title, index) => {
|
|
83
|
+
const match = existing.findIndex((item, i) => !consumed.has(i) && item.title.trim() === title);
|
|
84
|
+
if (match >= 0) {
|
|
85
|
+
consumed.add(match);
|
|
86
|
+
matchedByTitle[index] = true;
|
|
87
|
+
carried[index] = existing[match].checked;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
if (cleanTitles.length === existing.length) {
|
|
91
|
+
cleanTitles.forEach((_title, index) => {
|
|
92
|
+
if (matchedByTitle[index])
|
|
93
|
+
return;
|
|
94
|
+
const candidate = existing[index];
|
|
95
|
+
if (!candidate || consumed.has(index))
|
|
96
|
+
return;
|
|
97
|
+
consumed.add(index);
|
|
98
|
+
carried[index] = candidate.checked;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const items = cleanTitles.map((title, index) => ({
|
|
102
|
+
id: createChecklistItemId(taskId, index + 1, title),
|
|
103
|
+
number: index + 1,
|
|
104
|
+
title,
|
|
105
|
+
checked: carried[index] ?? false,
|
|
106
|
+
}));
|
|
107
|
+
const lostTicks = existing
|
|
108
|
+
.filter((item, i) => item.checked && !consumed.has(i))
|
|
109
|
+
.map((item) => item.title);
|
|
110
|
+
return { items, lostTicks };
|
|
111
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delete a feature the same way from every surface — MCP, the pi adapter's
|
|
3
|
+
* tool and interactive paths, and the REST server. Before this task each of
|
|
4
|
+
* those four wrote its own delete, and two of them cleared the feature
|
|
5
|
+
* record and never touched its phases: the phases were left pointing at a
|
|
6
|
+
* featureId that no longer existed anywhere — an orphan, not a link. Per
|
|
7
|
+
* AGENTS.md rule 4, "also unlink the phases" is not a rule a caller should
|
|
8
|
+
* have to remember; it lives here once and every caller gets it whether it
|
|
9
|
+
* asked for it or not.
|
|
10
|
+
*
|
|
11
|
+
* Cascade semantics (must match exactly — do not "simplify" to one
|
|
12
|
+
* behaviour): `cascade: true` deletes the feature's phases outright (phase
|
|
13
|
+
* deletion already archives their handoffs; this does not reimplement
|
|
14
|
+
* that). `cascade: false` unlinks each phase (clears `featureId`) and
|
|
15
|
+
* leaves it alive — the phase still exists with its tasks, it just no
|
|
16
|
+
* longer belongs to a feature.
|
|
17
|
+
*
|
|
18
|
+
* The delete is verified, not assumed: after writing, the feature list is
|
|
19
|
+
* read back from disk and the id must actually be gone. Reporting success
|
|
20
|
+
* without checking is the same defect class as T413.
|
|
21
|
+
*/
|
|
22
|
+
import type { PlanStore } from "./plan-store.js";
|
|
23
|
+
export interface DeleteFeatureCascadeOptions {
|
|
24
|
+
/** true deletes the feature's phases; false unlinks them and leaves them alive. */
|
|
25
|
+
cascade: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** One phase's fate, for callers (the REST server's WS broadcast, in
|
|
28
|
+
* particular) that need to say what happened to each affected phase. */
|
|
29
|
+
export interface DeleteFeatureCascadePhase {
|
|
30
|
+
id: string;
|
|
31
|
+
action: "deleted" | "unlinked";
|
|
32
|
+
}
|
|
33
|
+
export interface DeleteFeatureCascadeResult {
|
|
34
|
+
/** Canonical F00x ref — never the caller's raw input. */
|
|
35
|
+
ref: string;
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
cascade: boolean;
|
|
39
|
+
phaseCount: number;
|
|
40
|
+
phases: DeleteFeatureCascadePhase[];
|
|
41
|
+
}
|
|
42
|
+
export type DeleteFeatureCascadeErrorCode = "FEATURE_NOT_FOUND" | "FEATURE_DELETE_NOT_PERSISTED";
|
|
43
|
+
export type DeleteFeatureCascadeOutcome = {
|
|
44
|
+
ok: true;
|
|
45
|
+
result: DeleteFeatureCascadeResult;
|
|
46
|
+
} | {
|
|
47
|
+
ok: false;
|
|
48
|
+
errorCode: DeleteFeatureCascadeErrorCode;
|
|
49
|
+
error: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Delete the feature identified by the already-resolved `featureId` (a
|
|
53
|
+
* canonical UUID — turning a caller's fuzzy ref like "checkout" or "F5"
|
|
54
|
+
* into that id is the caller's job, same as every other mutation in this
|
|
55
|
+
* codebase; resolveFeatureRefStrict does that today). The feature is
|
|
56
|
+
* re-read fresh, inside the write lock, immediately before the mutation —
|
|
57
|
+
* not trusted from whatever the caller resolved earlier — so a feature
|
|
58
|
+
* that another process deleted between the caller's resolve and this call
|
|
59
|
+
* fails loudly (FEATURE_NOT_FOUND) instead of reporting a phantom success.
|
|
60
|
+
*/
|
|
61
|
+
export declare function deleteFeatureCascade(store: PlanStore, featureId: string, options: DeleteFeatureCascadeOptions): Promise<DeleteFeatureCascadeOutcome>;
|
|
62
|
+
//# sourceMappingURL=feature-delete.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feature-delete.d.ts","sourceRoot":"","sources":["../src/feature-delete.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAGjD,MAAM,WAAW,2BAA2B;IAC1C,mFAAmF;IACnF,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;wEACwE;AACxE,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,SAAS,GAAG,UAAU,CAAC;CAChC;AAED,MAAM,WAAW,0BAA0B;IACzC,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,yBAAyB,EAAE,CAAC;CACrC;AAED,MAAM,MAAM,6BAA6B,GAAG,mBAAmB,GAAG,8BAA8B,CAAC;AAEjG,MAAM,MAAM,2BAA2B,GACnC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,0BAA0B,CAAA;CAAE,GAChD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,6BAA6B,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3E;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,SAAS,EAChB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,2BAA2B,CAAC,CAyCtC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { formatFeatureRef } from "./naming.js";
|
|
2
|
+
/**
|
|
3
|
+
* Delete the feature identified by the already-resolved `featureId` (a
|
|
4
|
+
* canonical UUID — turning a caller's fuzzy ref like "checkout" or "F5"
|
|
5
|
+
* into that id is the caller's job, same as every other mutation in this
|
|
6
|
+
* codebase; resolveFeatureRefStrict does that today). The feature is
|
|
7
|
+
* re-read fresh, inside the write lock, immediately before the mutation —
|
|
8
|
+
* not trusted from whatever the caller resolved earlier — so a feature
|
|
9
|
+
* that another process deleted between the caller's resolve and this call
|
|
10
|
+
* fails loudly (FEATURE_NOT_FOUND) instead of reporting a phantom success.
|
|
11
|
+
*/
|
|
12
|
+
export async function deleteFeatureCascade(store, featureId, options) {
|
|
13
|
+
return store.runBatch(async () => {
|
|
14
|
+
const features = (await store.loadFeatures()).features;
|
|
15
|
+
const feature = features.find((entry) => entry.id === featureId);
|
|
16
|
+
if (!feature) {
|
|
17
|
+
return { ok: false, errorCode: "FEATURE_NOT_FOUND", error: `Feature not found: ${featureId}` };
|
|
18
|
+
}
|
|
19
|
+
const ref = formatFeatureRef(feature.number);
|
|
20
|
+
const phases = (await store.loadAllPhases()).filter((phase) => phase.featureId === feature.id);
|
|
21
|
+
await store.updateFeatures((doc) => {
|
|
22
|
+
doc.features = doc.features.filter((entry) => entry.id !== feature.id);
|
|
23
|
+
return doc;
|
|
24
|
+
});
|
|
25
|
+
const phaseResults = [];
|
|
26
|
+
for (const phase of phases) {
|
|
27
|
+
if (options.cascade) {
|
|
28
|
+
await store.deletePhase(phase.id);
|
|
29
|
+
phaseResults.push({ id: phase.id, action: "deleted" });
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
await store.updatePhase(phase.id, (current) => ({ ...current, featureId: undefined, updatedAt: new Date().toISOString() }));
|
|
33
|
+
phaseResults.push({ id: phase.id, action: "unlinked" });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
await store.writeGenerated();
|
|
37
|
+
// Prove it happened: the feature must actually be gone from a fresh
|
|
38
|
+
// disk read, not merely filtered out of an in-memory copy that a
|
|
39
|
+
// subsequent write silently dropped.
|
|
40
|
+
const stillPresent = (await store.loadFeatures()).features.some((entry) => entry.id === feature.id);
|
|
41
|
+
if (stillPresent) {
|
|
42
|
+
return { ok: false, errorCode: "FEATURE_DELETE_NOT_PERSISTED", error: `Feature delete did not persist: ${ref}` };
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
result: { ref, id: feature.id, name: feature.name, cascade: options.cascade, phaseCount: phaseResults.length, phases: phaseResults },
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness-agnostic logic for the "no task in progress" warning. The guard
|
|
3
|
+
* never blocks and never asks: every tool call proceeds, and a call that
|
|
4
|
+
* changes project code while no task is in progress only earns the agent a
|
|
5
|
+
* warning. Planner operations never need a task. Any adapter (Claude Code
|
|
6
|
+
* PreToolUse hook, Pi's tool_call handler, a future harness) feeds it a
|
|
7
|
+
* parsed tool-use event plus already-fetched planner state and gets back the
|
|
8
|
+
* same warning, or none. This never talks to the plan store
|
|
9
|
+
* or stdin/stdout — callers own that I/O. It does read the filesystem in one
|
|
10
|
+
* narrow way (see {@link canonicalize}): resolving symlinks so a path
|
|
11
|
+
* comparison isn't fooled by one, which is cheap (a handful of stat calls,
|
|
12
|
+
* not a plan load) and safe to call on paths that don't exist yet.
|
|
13
|
+
*/
|
|
14
|
+
/** The tool-use fields the decision needs, already pulled out of whatever
|
|
15
|
+
* shape the harness's raw event uses (Claude Code's `tool_input.file_path`,
|
|
16
|
+
* Pi's `event.input.path`, etc.). */
|
|
17
|
+
export interface GuardEventInput {
|
|
18
|
+
/** Tool name in any case: Claude Code's `Edit`/`Bash` and Pi's `edit`/`bash` are the same tool. */
|
|
19
|
+
toolName: string;
|
|
20
|
+
/** Absolute (or resolvable) working directory used to resolve relative paths. */
|
|
21
|
+
cwd: string;
|
|
22
|
+
/** Absolute planner root, e.g. `join(cwd, ".planner")`. */
|
|
23
|
+
plannerRoot: string;
|
|
24
|
+
/** Edit / Write target path (Pi: `path`). */
|
|
25
|
+
filePath?: string;
|
|
26
|
+
/** NotebookEdit target path. */
|
|
27
|
+
notebookPath?: string;
|
|
28
|
+
/** Bash command line, when toolName is "Bash". */
|
|
29
|
+
command?: string;
|
|
30
|
+
}
|
|
31
|
+
/** Planner state the caller already fetched (I/O happens once, upstream). */
|
|
32
|
+
export interface GuardStateInput {
|
|
33
|
+
hasPlannerDir: boolean;
|
|
34
|
+
totalTasks: number;
|
|
35
|
+
hasInProgressTask: boolean;
|
|
36
|
+
guardBypassed: boolean;
|
|
37
|
+
/** The task the warning suggests starting, when there is an obvious one. */
|
|
38
|
+
focusTask?: {
|
|
39
|
+
id: string;
|
|
40
|
+
title: string;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export interface GuardClassification {
|
|
44
|
+
/** Whether this tool call may write project code, so the guard must look at task state. */
|
|
45
|
+
guarded: boolean;
|
|
46
|
+
/** The project paths outside `.planner/` the call would write to; empty when the target is unknown. */
|
|
47
|
+
paths: string[];
|
|
48
|
+
}
|
|
49
|
+
export interface NoTaskWarning {
|
|
50
|
+
/** Text for the agent, present only when it should be warned. */
|
|
51
|
+
warning?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Whether a Bash command line contains a write-shaped operation: output
|
|
55
|
+
* redirection, `tee`, in-place `sed`/`perl`, a file-creating `cp`/`mv`,
|
|
56
|
+
* `rm`, or a `git` operation that rewrites tracked files. Deliberately
|
|
57
|
+
* over-inclusive: `git`, build/test runners, `ls`, `cat`, `grep`, `find`
|
|
58
|
+
* and friends must stay unmatched, but when in doubt this returns true — a
|
|
59
|
+
* false positive costs one warning to the agent, never a blocked call or a
|
|
60
|
+
* prompt, so under-matching (missing a real edit) is the worse failure.
|
|
61
|
+
*/
|
|
62
|
+
export declare function isWriteShapedBashCommand(command: string): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Best-effort extraction of the file path(s) a write-shaped Bash segment
|
|
65
|
+
* targets, so a command that only touches `.planner/` or files outside the
|
|
66
|
+
* project earns no warning. This cannot see through a heredoc whose target
|
|
67
|
+
* is computed (a variable, command substitution, etc.) rather than written
|
|
68
|
+
* literally in the command line — that case still reaches the in-progress /
|
|
69
|
+
* bypass checks and can warn, rather than being silently treated as safe.
|
|
70
|
+
*
|
|
71
|
+
* A wider gap of the same kind: a command that writes through an
|
|
72
|
+
* interpreter (`python3 - <<PY` then `open(path, "w")`, `node -e`, a script
|
|
73
|
+
* invoked by name) is opaque here — nothing in the command line looks
|
|
74
|
+
* write-shaped. P105(F005)/T431 evaluated closing this and chose not to,
|
|
75
|
+
* for reasons that also rule out revisiting it the same way later:
|
|
76
|
+
* - Parsing the script body is unbounded (a path can be computed, read
|
|
77
|
+
* from argv, or built at runtime) and would trade an honest documented
|
|
78
|
+
* limit for false confidence.
|
|
79
|
+
* - A PostToolUse hook that notices tracked files changed while no task
|
|
80
|
+
* was in progress sounds like a fit (the guard's job is visibility, not
|
|
81
|
+
* prevention), but it collides with this project's own workflow: a
|
|
82
|
+
* task is left with its changes uncommitted for the orchestrator to
|
|
83
|
+
* review, so "no task in-progress, tracked files changed outside
|
|
84
|
+
* .planner/" is the routine state right after every task completes,
|
|
85
|
+
* not a signal of anything wrong. Telling that apart from a real
|
|
86
|
+
* interpreter write means diffing only the delta from one specific
|
|
87
|
+
* Bash call, which needs a session-scoped git-status baseline checked
|
|
88
|
+
* on every Bash invocation — read-only ones included, since which
|
|
89
|
+
* commands wrote can't be known in advance. That statefulness and
|
|
90
|
+
* per-call cost is worse than the gap it would close.
|
|
91
|
+
* The limit stands, documented rather than silently assumed away.
|
|
92
|
+
*/
|
|
93
|
+
export declare function extractBashWriteTargets(command: string): string[];
|
|
94
|
+
/** Whether `candidate` resolves to a path inside `plannerRoot` (or is it exactly). */
|
|
95
|
+
export declare function isPathInsidePlannerRoot(candidate: string, plannerRoot: string, cwd: string): boolean;
|
|
96
|
+
/** Whether writing `candidate` changes the project's code: it is inside the
|
|
97
|
+
* project (the directory that owns `.planner/`) and not inside `.planner/`.
|
|
98
|
+
* `/dev/null`, `~/.claude/...`, temp dirs and other repositories are not. */
|
|
99
|
+
export declare function isProjectCodePath(candidate: string, plannerRoot: string, cwd: string): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Whether a tool call may change the project's code, and so needs the guard
|
|
102
|
+
* to look at task state. This is the one place that rule lives; the hook's
|
|
103
|
+
* cheap early exit and {@link noTaskWarning} both read `guarded`.
|
|
104
|
+
*
|
|
105
|
+
* - Tools that never write (Read, Grep, MCP tools, read-only Bash, ...) are
|
|
106
|
+
* not guarded.
|
|
107
|
+
* - A write whose targets are all known and none is project code (inside
|
|
108
|
+
* `.planner/`, `/dev/null`, `~/.claude/...`, a temp dir, another repo) is
|
|
109
|
+
* not guarded: updating the planner or the user's own config never needs a
|
|
110
|
+
* task.
|
|
111
|
+
* - A write with at least one project-code target is guarded, with `paths`
|
|
112
|
+
* narrowed to those targets.
|
|
113
|
+
* - A write whose target is unknown (a computed heredoc, an Edit with no
|
|
114
|
+
* path) is guarded with empty `paths` — never silently allowed.
|
|
115
|
+
*/
|
|
116
|
+
export declare function classifyGuardedTool(event: GuardEventInput): GuardClassification;
|
|
117
|
+
/**
|
|
118
|
+
* The warning for a tool call that changes project code while no task is in
|
|
119
|
+
* progress, or none. Never a block or a prompt: the adapter lets the call
|
|
120
|
+
* proceed either way and only passes the text to the agent. Pure apart from
|
|
121
|
+
* the cheap symlink canonicalization in {@link isPathInsidePlannerRoot}.
|
|
122
|
+
*
|
|
123
|
+
* Order mirrors the cost of finding out: anything that cannot change project
|
|
124
|
+
* code (see {@link classifyGuardedTool}) is settled from the event alone, so
|
|
125
|
+
* adapters can skip loading the plan for it.
|
|
126
|
+
*/
|
|
127
|
+
export declare function noTaskWarning(event: GuardEventInput, state: GuardStateInput): NoTaskWarning;
|
|
128
|
+
//# sourceMappingURL=guard-decision.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard-decision.d.ts","sourceRoot":"","sources":["../src/guard-decision.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH;;qCAEqC;AACrC,MAAM,WAAW,eAAe;IAC9B,mGAAmG;IACnG,QAAQ,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,GAAG,EAAE,MAAM,CAAC;IACZ,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gCAAgC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,OAAO,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,aAAa,EAAE,OAAO,CAAC;IACvB,4EAA4E;IAC5E,SAAS,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAED,MAAM,WAAW,mBAAmB;IAClC,2FAA2F;IAC3F,OAAO,EAAE,OAAO,CAAC;IACjB,uGAAuG;IACvG,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAiFD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAGjE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CA2BjE;AAuCD,sFAAsF;AACtF,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAEpG;AAED;;6EAE6E;AAC7E,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAE9F;AAoBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,eAAe,GAAG,mBAAmB,CAM/E;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,GAAG,aAAa,CAa3F"}
|