@agent-plan/core 0.2.29 → 0.3.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/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 +118 -0
- package/dist/guard-decision.d.ts.map +1 -0
- package/dist/guard-decision.js +203 -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 +108 -0
- package/dist/handoff-reply.d.ts.map +1 -0
- package/dist/handoff-reply.js +279 -0
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -1
- 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/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 +395 -0
- package/dist/project-context.d.ts.map +1 -0
- package/dist/project-context.js +295 -0
- package/dist/recap.d.ts.map +1 -1
- package/dist/recap.js +27 -0
- package/dist/recommend-reply.d.ts +51 -0
- package/dist/recommend-reply.d.ts.map +1 -0
- package/dist/recommend-reply.js +84 -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 +31 -13
- 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.map +1 -1
- package/dist/task-selection.js +14 -12
- 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,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness-agnostic decision logic for the "no task in progress" edit guard.
|
|
3
|
+
* Any adapter (Claude Code PreToolUse hook, Pi's advisory guard, a future
|
|
4
|
+
* harness) can feed it a parsed tool-use event plus already-fetched planner
|
|
5
|
+
* state and get back the same decision. This never talks to the plan store
|
|
6
|
+
* or stdin/stdout — callers own that I/O. It does read the filesystem in one
|
|
7
|
+
* narrow way (see {@link canonicalize}): resolving symlinks so a path
|
|
8
|
+
* comparison isn't fooled by one, which is cheap (a handful of stat calls,
|
|
9
|
+
* not a plan load) and safe to call on paths that don't exist yet.
|
|
10
|
+
*/
|
|
11
|
+
export type GuardPermissionDecision = "allow" | "ask" | "deny";
|
|
12
|
+
/** The tool-use fields the decision needs, already pulled out of whatever
|
|
13
|
+
* shape the harness's raw event uses (Claude Code's `tool_input.file_path`,
|
|
14
|
+
* Pi's `event.input.path`, etc.). */
|
|
15
|
+
export interface GuardEventInput {
|
|
16
|
+
toolName: string;
|
|
17
|
+
/** Absolute (or resolvable) working directory used to resolve relative paths. */
|
|
18
|
+
cwd: string;
|
|
19
|
+
/** Absolute planner root, e.g. `join(cwd, ".planner")`. */
|
|
20
|
+
plannerRoot: string;
|
|
21
|
+
/** Edit / Write target path. */
|
|
22
|
+
filePath?: string;
|
|
23
|
+
/** NotebookEdit target path. */
|
|
24
|
+
notebookPath?: string;
|
|
25
|
+
/** Bash command line, when toolName is "Bash". */
|
|
26
|
+
command?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Planner state the caller already fetched (I/O happens once, upstream). */
|
|
29
|
+
export interface GuardStateInput {
|
|
30
|
+
hasPlannerDir: boolean;
|
|
31
|
+
totalTasks: number;
|
|
32
|
+
hasInProgressTask: boolean;
|
|
33
|
+
guardBypassed: boolean;
|
|
34
|
+
/** Human-readable "start this task" hint, or the generic fallback; empty allowed. */
|
|
35
|
+
startHint: string;
|
|
36
|
+
}
|
|
37
|
+
export interface GuardClassification {
|
|
38
|
+
/** Whether this tool call is one the guard evaluates at all. */
|
|
39
|
+
guarded: boolean;
|
|
40
|
+
/** Best-effort candidate paths the call would write to (may be empty when unknown). */
|
|
41
|
+
paths: string[];
|
|
42
|
+
}
|
|
43
|
+
export interface GuardDecision {
|
|
44
|
+
decision: GuardPermissionDecision;
|
|
45
|
+
reason?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Whether a Bash command line contains a write-shaped operation: output
|
|
49
|
+
* redirection, `tee`, in-place `sed`/`perl`, a file-creating `cp`/`mv`,
|
|
50
|
+
* `rm`, or a `git` operation that rewrites tracked files. Deliberately
|
|
51
|
+
* over-inclusive: `git`, build/test runners, `ls`, `cat`, `grep`, `find`
|
|
52
|
+
* and friends must stay unmatched, but when in doubt this returns true — the
|
|
53
|
+
* cost of a false positive here is one extra prompt, not a blocked
|
|
54
|
+
* operation, so under-matching (missing a real edit) is the worse failure.
|
|
55
|
+
*/
|
|
56
|
+
export declare function isWriteShapedBashCommand(command: string): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Best-effort extraction of the file path(s) a write-shaped Bash segment
|
|
59
|
+
* targets, so a command that only touches `.planner/` can still be
|
|
60
|
+
* recognized and allowed. This cannot see through a heredoc whose target is
|
|
61
|
+
* computed (a variable, command substitution, etc.) rather than written
|
|
62
|
+
* literally in the command line — that case is expected to fall through to
|
|
63
|
+
* the normal in-progress/bypass checks below, not to be silently allowed.
|
|
64
|
+
*
|
|
65
|
+
* A wider gap of the same kind: a command that writes through an
|
|
66
|
+
* interpreter (`python3 - <<PY` then `open(path, "w")`, `node -e`, a script
|
|
67
|
+
* invoked by name) is opaque here — nothing in the command line looks
|
|
68
|
+
* write-shaped. P105(F005)/T431 evaluated closing this and chose not to,
|
|
69
|
+
* for reasons that also rule out revisiting it the same way later:
|
|
70
|
+
* - Parsing the script body is unbounded (a path can be computed, read
|
|
71
|
+
* from argv, or built at runtime) and would trade an honest documented
|
|
72
|
+
* limit for false confidence.
|
|
73
|
+
* - A PostToolUse hook that notices tracked files changed while no task
|
|
74
|
+
* was in progress sounds like a fit (the guard's job is visibility, not
|
|
75
|
+
* prevention), but it collides with this project's own workflow: a
|
|
76
|
+
* task is left with its changes uncommitted for the orchestrator to
|
|
77
|
+
* review, so "no task in-progress, tracked files changed outside
|
|
78
|
+
* .planner/" is the routine state right after every task completes,
|
|
79
|
+
* not a signal of anything wrong. Telling that apart from a real
|
|
80
|
+
* interpreter write means diffing only the delta from one specific
|
|
81
|
+
* Bash call, which needs a session-scoped git-status baseline checked
|
|
82
|
+
* on every Bash invocation — read-only ones included, since which
|
|
83
|
+
* commands wrote can't be known in advance. That statefulness and
|
|
84
|
+
* per-call cost is worse than the gap it would close.
|
|
85
|
+
* The limit stands, documented rather than silently assumed away.
|
|
86
|
+
*/
|
|
87
|
+
export declare function extractBashWriteTargets(command: string): string[];
|
|
88
|
+
/** Whether `candidate` resolves to a path inside `plannerRoot` (or is it exactly). */
|
|
89
|
+
export declare function isPathInsidePlannerRoot(candidate: string, plannerRoot: string, cwd: string): boolean;
|
|
90
|
+
/** True only when every candidate path is inside the planner root, and there
|
|
91
|
+
* is at least one candidate — an empty/unknown path list must never count as
|
|
92
|
+
* "inside", or an unresolvable target would be silently allowed. Exported so
|
|
93
|
+
* a hook harness can use the exact same check for its cheap early-allow path
|
|
94
|
+
* without duplicating the rule. */
|
|
95
|
+
export declare function isEntirelyInsidePlannerRoot(paths: string[], plannerRoot: string, cwd: string): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Which tool calls the guard covers, and what path(s) they would write to.
|
|
98
|
+
* Everything else (Read, Grep, WebFetch, MCP tools, read-only Bash, ...) is
|
|
99
|
+
* unguarded and returns `{ guarded: false }` without touching the planner
|
|
100
|
+
* store.
|
|
101
|
+
*/
|
|
102
|
+
export declare function classifyGuardedTool(event: GuardEventInput): GuardClassification;
|
|
103
|
+
/**
|
|
104
|
+
* The full "no task in progress" guard decision, as a pure function: given a
|
|
105
|
+
* parsed tool-use event and planner state the caller already fetched, decide
|
|
106
|
+
* allow or ask (deny is part of the type for a future genuinely-deny case,
|
|
107
|
+
* but nothing here returns it today). No stdin/stdout and no plan-store
|
|
108
|
+
* calls — the hook harness (or a Pi equivalent) does the reading and
|
|
109
|
+
* printing around this; the only filesystem access is the cheap symlink
|
|
110
|
+
* canonicalization in {@link isPathInsidePlannerRoot}.
|
|
111
|
+
*
|
|
112
|
+
* Order matters and mirrors the cost of finding out: anything the guard
|
|
113
|
+
* doesn't cover, and anything that resolves entirely inside the planner
|
|
114
|
+
* root, is decided from the event alone (no planner state needed). Only a
|
|
115
|
+
* real candidate for blocking reaches the state-based checks.
|
|
116
|
+
*/
|
|
117
|
+
export declare function decideGuardPreToolUse(event: GuardEventInput, state: GuardStateInput): GuardDecision;
|
|
118
|
+
//# 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":"AAGA;;;;;;;;;GASG;AAEH,MAAM,MAAM,uBAAuB,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AAE/D;;qCAEqC;AACrC,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,GAAG,EAAE,MAAM,CAAC;IACZ,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB,gCAAgC;IAChC,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,qFAAqF;IACrF,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,OAAO,EAAE,OAAO,CAAC;IACjB,uFAAuF;IACvF,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,uBAAuB,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAsCD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAGjE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CA2BjE;AAsBD,sFAAsF;AACtF,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAKpG;AAED;;;;mCAImC;AACnC,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAEtG;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,eAAe,GAAG,mBAAmB,CAa/E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,GAAG,aAAa,CAkBnG"}
|