@onefl666/dsh-great-plan 0.0.0-stage → 0.6.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.
@@ -0,0 +1,135 @@
1
+ import { EXIT_PLAN_MODE_TOOL, selectPlanCompactionRange } from './plan-range.js';
2
+
3
+ // The canonical name lives in plan-range.js; re-exported for existing importers.
4
+ export { EXIT_PLAN_MODE_TOOL };
5
+
6
+ /** The command accepts no arguments; this is its user-visible usage line. */
7
+ export const USAGE = 'Usage: /great-plan-compact (no arguments)';
8
+
9
+ /**
10
+ * Stable manual-compaction error codes mapped to user text.
11
+ *
12
+ * Every value is user-visible; the Client mirrors each string in
13
+ * `COMPACT_MESSAGE_KEYS` so it can be localized (cross-side test).
14
+ */
15
+ export const FAILURES = {
16
+ busy: 'Compaction is unavailable because another compaction is running.',
17
+ changed: 'The history selected for compaction changed during the attempt.',
18
+ summary: 'Compaction could not produce a useful summary.',
19
+ commit: 'Compaction did not finish cleanly.',
20
+ persistence: 'Compaction finished, but the session could not be saved.',
21
+ cancelled: 'Compaction was cancelled.',
22
+ };
23
+
24
+ /**
25
+ * Static command failure messages, also mirrored by the Client's
26
+ * `COMPACT_MESSAGE_KEYS`. Dynamic messages (`...: ${messageOf(error)}`) are
27
+ * intentionally not listed and fall back to raw English when unmapped.
28
+ */
29
+ export const MESSAGES = {
30
+ planStateUnavailable: 'Plan state is unavailable in this composition.',
31
+ planInactive: 'Plan mode is not active; nothing to compact.',
32
+ backendUnavailable: 'Compaction is unavailable because no compaction backend is composed.',
33
+ noReview: 'There is no pending plan review to compact.',
34
+ empty: 'No compactable history exists before the pending plan.',
35
+ };
36
+
37
+ function messageOf(error) {
38
+ return error instanceof Error ? error.message : String(error);
39
+ }
40
+
41
+ /**
42
+ * Resolve the composed compaction service for one agent.
43
+ *
44
+ * Presets isolate their mounted services, so the agent's preset group owns its
45
+ * `compaction` instance while the host-plane read only sees globally mounted
46
+ * compositions. The agent-scoped context is the middle step; the preset
47
+ * registry is the authoritative source when it exposes the service.
48
+ *
49
+ * @param {object | undefined} agent the agent executing the command.
50
+ * @param {object} [accessors] service readers supplied by the composition.
51
+ * @param {() => object | undefined} [accessors.getAgentPresets] host-plane preset registry reader.
52
+ * @param {(agent: object | undefined) => object | undefined} [accessors.getCompaction] host-plane compaction reader.
53
+ * @returns {object | undefined} the first service that exposes `compactRegion`.
54
+ */
55
+ export function resolveCompactionService(agent, { getAgentPresets, getCompaction } = {}) {
56
+ if (agent !== undefined && agent !== null) {
57
+ // 1. The service mounted in the agent's preset group.
58
+ try {
59
+ const scoped = typeof getAgentPresets === 'function'
60
+ ? getAgentPresets()?.serviceFor?.(agent, 'compaction')
61
+ : undefined;
62
+ if (scoped?.compactRegion !== undefined) return scoped;
63
+ } catch {
64
+ // An absent or unreadable preset registry falls through.
65
+ }
66
+ // 2. The service mounted in the agent's scoped context.
67
+ try {
68
+ const scoped = agent?.ctx?.get?.('compaction');
69
+ if (scoped?.compactRegion !== undefined) return scoped;
70
+ } catch {
71
+ // An unreadable scoped context falls through.
72
+ }
73
+ }
74
+ // 3. The host-plane service; a throwing read stays visible to the caller.
75
+ const fallback = typeof getCompaction === 'function' ? getCompaction(agent) : undefined;
76
+ return fallback?.compactRegion !== undefined ? fallback : undefined;
77
+ }
78
+
79
+ export function createCompactCommand({ getProjections, getCompaction, getBalancedAfter, getAgentPresets }) {
80
+ return {
81
+ name: 'great-plan-compact',
82
+ description: 'Compact older plan history before executing (used by Great Plan).',
83
+ async handler(invocation) {
84
+ const args = invocation ?? {};
85
+ const rawInput = typeof args.rawInput === 'string' ? args.rawInput.trim() : '';
86
+ if (rawInput !== '') return { kind: 'error', text: USAGE };
87
+ const { agent, signal } = args;
88
+
89
+ let plan;
90
+ try {
91
+ const projections = getProjections();
92
+ if (typeof projections?.snapshot !== 'function') return { kind: 'error', text: MESSAGES.planStateUnavailable };
93
+ plan = projections.snapshot(agent.session, ['plan'])?.values?.plan;
94
+ } catch (error) {
95
+ return { kind: 'error', text: `Plan state is unavailable: ${messageOf(error)}` };
96
+ }
97
+ if (plan?.active !== true) return { kind: 'error', text: MESSAGES.planInactive };
98
+
99
+ let compaction;
100
+ try {
101
+ compaction = resolveCompactionService(agent, { getAgentPresets, getCompaction });
102
+ } catch (error) {
103
+ return { kind: 'error', text: `Compaction is unavailable: ${messageOf(error)}` };
104
+ }
105
+ if (compaction?.compactRegion === undefined) return { kind: 'error', text: MESSAGES.backendUnavailable };
106
+
107
+ let balancedAfter;
108
+ try {
109
+ balancedAfter = await getBalancedAfter();
110
+ } catch (error) {
111
+ return { kind: 'error', text: `Compaction is unavailable because the compaction package could not be loaded: ${messageOf(error)}` };
112
+ }
113
+
114
+ let range;
115
+ try {
116
+ range = selectPlanCompactionRange(agent.session, balancedAfter, { planReviewTool: EXIT_PLAN_MODE_TOOL });
117
+ } catch (error) {
118
+ return { kind: 'error', text: `Unable to inspect plan history: ${messageOf(error)}` };
119
+ }
120
+ if (range.error === 'no-review') return { kind: 'error', text: MESSAGES.noReview };
121
+ if (range.error === 'empty') return { kind: 'error', text: MESSAGES.empty };
122
+
123
+ try {
124
+ const result = await compaction.compactRegion(range.start, range.end, agent, signal);
125
+ const items = Array.isArray(result?.shadowedSeqs) ? result.shadowedSeqs.length : 0;
126
+ const tokens = Number.isFinite(result?.shadowedTokenCount) ? result.shadowedTokenCount : 0;
127
+ return { kind: 'success', text: `Compacted ${items} history items (~${tokens} tokens).` };
128
+ } catch (error) {
129
+ if (signal?.aborted) return { kind: 'error', text: FAILURES.cancelled };
130
+ const code = typeof error?.code === 'string' ? error.code : undefined;
131
+ return { kind: 'error', text: Object.hasOwn(FAILURES, code) ? FAILURES[code] : messageOf(error) };
132
+ }
133
+ },
134
+ };
135
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Evidence-first plan handoff prompt section.
3
+ *
4
+ * Plan mode already renders the deployment's native `plan:policy` guidance.
5
+ * This plugin adds one independent section immediately after it, asking the
6
+ * planner to hand over its investigation together with the plan. It also carries
7
+ * an optional unlimited-token-budget reminder and a brainstorming paragraph,
8
+ * each independently switchable from the Settings page. The native policy text
9
+ * is never replaced or duplicated.
10
+ */
11
+
12
+ /** Prompt section name; unique in the global prompt layer. */
13
+ export const PLAN_EVIDENCE_SECTION = 'great-plan:plan-evidence';
14
+
15
+ /** Used when the instance cannot resolve the centrally owned PLAN_POLICY order. */
16
+ export const PLAN_EVIDENCE_ORDER_FALLBACK = 501;
17
+
18
+ /** Model-facing collaborative-discovery and Team-Lead guidance; the Settings page can switch it off. */
19
+ export const PLAN_BRAINSTORMING_GUIDANCE = `Discovery and delegation:
20
+ - Assess the goal, success criteria, constraints, and current state; do not quiz the user with a checklist.
21
+ - If an answer could materially change scope or approach, ask one concise question per turn and wait. If the request is clear or the user delegates the choice, proceed.
22
+ - You are the Team Lead. Use \`spawn_teammate\` to start at least 2 exploration teammates in parallel, each with one distinct, concrete scope, and require concise findings cited as path:line-range. Do not investigate their scopes yourself; collect all results and resolve conflicts.
23
+ - Then spawn 1 separate planning teammate to draft a candidate plan from the consolidated evidence. Verify the evidence and the draft yourself before submitting.
24
+ - If \`spawn_teammate\` is unavailable or fails, say so in the plan and continue alone; never invent teammate work.
25
+ - Show alternatives and tradeoffs only when they help the user decide; state the key reason for your recommendation.
26
+ - Separate user requirements from your recommendations, assumptions, and unverified points. Give a brief rationale, not step-by-step chain-of-thought.`;
27
+
28
+ /** Model-facing unlimited-token-budget reminder; the Settings page can switch it off. */
29
+ export const PLAN_UNLIMITED_BUDGET_GUIDANCE = 'Token budget: unlimited. Never cut investigation, tool calls, or needed questions to save tokens; keep the final plan concise.';
30
+
31
+ /** Model-facing evidence and executor handoff guidance; always rendered in plan mode. */
32
+ export const PLAN_HANDOFF_GUIDANCE = `Plan as handoff (the executor may get only this plan or a compacted history, so make it self-contained):
33
+ - Background: request, current behavior, constraints, assumptions.
34
+ - Evidence: files/symbols with line ranges (e.g. path/file.js:12-30), searches and commands with key results, docs/skills/URLs used, and how conflicts were resolved. Cite evidence for each design claim; mark anything unverified as an assumption or open question.
35
+ - Handoff: what is settled and must not be re-investigated; which unknowns to verify first.
36
+ Never present a guess as fact or invent paths, APIs, command output, or test results. Re-check citations before submitting. Keep evidence compact: precise pointers, not raw logs or whole files.`;
37
+
38
+ /** Default guidance (every switch on) appended while plan mode is active. */
39
+ export const PLAN_EVIDENCE_GUIDANCE = `${PLAN_UNLIMITED_BUDGET_GUIDANCE}\n\n${PLAN_BRAINSTORMING_GUIDANCE}\n\n${PLAN_HANDOFF_GUIDANCE}`;
40
+
41
+ /**
42
+ * Compose the plan-mode guidance from its independently switchable parts.
43
+ *
44
+ * The budget reminder comes first as a general instruction, then the optional
45
+ * brainstorming guidance, and the evidence/handoff guidance always closes the
46
+ * section. Disabled parts are omitted entirely, never replaced by placeholders.
47
+ * With every switch on the precomputed `PLAN_EVIDENCE_GUIDANCE` constant is
48
+ * returned so the common path allocates nothing per prompt assembly.
49
+ */
50
+ export function composePlanGuidance({ brainstorming = true, unlimitedBudget = true } = {}) {
51
+ if (brainstorming !== false && unlimitedBudget !== false) return PLAN_EVIDENCE_GUIDANCE;
52
+ const parts = [];
53
+ if (unlimitedBudget !== false) parts.push(PLAN_UNLIMITED_BUDGET_GUIDANCE);
54
+ if (brainstorming !== false) parts.push(PLAN_BRAINSTORMING_GUIDANCE);
55
+ parts.push(PLAN_HANDOFF_GUIDANCE);
56
+ return parts.join('\n\n');
57
+ }
58
+
59
+ /**
60
+ * Read one switch from the plugin config.
61
+ *
62
+ * A volatile field arrives as a reference object; plain booleans are accepted
63
+ * for tests and hand-written rows. Every missing, malformed, or unreadable
64
+ * value keeps the fallback so prompt assembly never fails.
65
+ */
66
+ function readConfigSwitch(config, field, fallback) {
67
+ if (config === undefined || config === null || typeof config !== 'object') return fallback;
68
+ let value;
69
+ try {
70
+ value = config[field];
71
+ } catch {
72
+ return fallback;
73
+ }
74
+ if (value === undefined || value === null) return fallback;
75
+ if (typeof value === 'boolean') return value;
76
+ if (typeof value.get === 'function') {
77
+ try {
78
+ const live = value.get();
79
+ return typeof live === 'boolean' ? live : fallback;
80
+ } catch {
81
+ return fallback;
82
+ }
83
+ }
84
+ return fallback;
85
+ }
86
+
87
+ /** Read the brainstorming switch; defaults to enabled. */
88
+ export function readBrainstormingEnabled(config) {
89
+ return readConfigSwitch(config, 'injectBrainstormingPrompt', true);
90
+ }
91
+
92
+ /** Read the unlimited-token-budget switch; defaults to enabled. */
93
+ export function readUnlimitedBudgetEnabled(config) {
94
+ return readConfigSwitch(config, 'injectUnlimitedBudgetPrompt', true);
95
+ }
96
+
97
+ /**
98
+ * Resolve one boolean-validated plan-mode state from a planMode-like service.
99
+ *
100
+ * A usable state exposes a boolean `active`; a pending selection overrides the
101
+ * logged state. Missing, malformed, or throwing reads return `undefined` so the
102
+ * caller can try its next source.
103
+ */
104
+ function readPlanModeState(readService, agent) {
105
+ try {
106
+ const service = readService();
107
+ if (service === undefined || service === null || typeof service.get !== 'function') return undefined;
108
+ const state = service.get(agent);
109
+ if (state !== undefined && state !== null && typeof state === 'object' && typeof state.active === 'boolean') {
110
+ return state.pending === undefined ? state.active : state.pending === true;
111
+ }
112
+ } catch {
113
+ return undefined;
114
+ }
115
+ return undefined;
116
+ }
117
+
118
+ /**
119
+ * Decide whether plan mode is active for the step being assembled.
120
+ *
121
+ * The Host plane cannot rely on its own `planMode` instance: agent presets
122
+ * commonly mount the plan-mode plugin in an isolated group, so the host-plane
123
+ * instance does not see the state that the preset drives. The agent's own
124
+ * scoped context (`agent.ctx`) carries the instance that owns the session's
125
+ * plan state and the pending selection's direction — the same data source the
126
+ * native `plan:policy` section reads. The host-plane instance is the fallback
127
+ * for compositions where plan mode is host-owned, and the shared `plan`
128
+ * session projection (the unit the compaction command also reads) is the final
129
+ * synchronous fallback. Every source degrades to inactive instead of throwing.
130
+ */
131
+ export function readPlanModeActive(agent, getPlanMode, getSessionProjections) {
132
+ if (agent === undefined || agent === null) return false;
133
+ // 1. The agent-scoped planMode instance: `get()` returns the logged state
134
+ // plus a pending selection whose boolean is the selected direction.
135
+ const scoped = readPlanModeState(() => agent.ctx?.get?.('planMode'), agent);
136
+ if (scoped !== undefined) return scoped;
137
+ // 2. The host-plane planMode instance.
138
+ const host = readPlanModeState(() => (typeof getPlanMode === 'function' ? getPlanMode() : undefined), agent);
139
+ if (host !== undefined) return host;
140
+ // 3. The shared `plan` projection's cropped view { active, pending }. The
141
+ // pending direction is not exposed here, so a pending selection counts as
142
+ // active; by assembly time the pre-step has already applied pending
143
+ // selections, which makes this equivalent to the native section's gate.
144
+ try {
145
+ const projections = typeof getSessionProjections === 'function' ? getSessionProjections() : undefined;
146
+ const session = agent?.session;
147
+ if (projections !== undefined && projections !== null && session !== undefined && typeof projections.snapshot === 'function') {
148
+ const state = projections.snapshot(session, ['plan'])?.values?.plan;
149
+ if (state !== undefined && state !== null && typeof state === 'object' && typeof state.active === 'boolean') {
150
+ return state.active === true || state.pending === true;
151
+ }
152
+ }
153
+ } catch {
154
+ // No usable plan state anywhere; treat the step as outside plan mode.
155
+ }
156
+ return false;
157
+ }
158
+
159
+ /**
160
+ * Register the section right after the native plan policy.
161
+ *
162
+ * @param {object} options
163
+ * @param {object} options.systemPrompt the Host `systemPrompt` service.
164
+ * @param {() => object | undefined} options.getPlanMode optional host-plane `planMode` reader.
165
+ * @param {() => object | undefined} [options.getSessionProjections] optional projection service reader.
166
+ * @param {() => object | undefined} options.getConfig optional live plugin config reader.
167
+ */
168
+ export function registerPlanEvidenceSection({ systemPrompt, getPlanMode, getSessionProjections, getConfig }) {
169
+ let order = PLAN_EVIDENCE_ORDER_FALLBACK;
170
+ try {
171
+ const resolved = typeof systemPrompt.getSectionOrder === 'function'
172
+ ? systemPrompt.getSectionOrder('PLAN_POLICY')
173
+ : undefined;
174
+ if (Number.isFinite(resolved)) order = resolved + 1;
175
+ } catch {
176
+ // Keep the fallback order; an unavailable central table must not break the plugin.
177
+ }
178
+ return systemPrompt.section({
179
+ name: PLAN_EVIDENCE_SECTION,
180
+ order,
181
+ text: (context) => {
182
+ let config;
183
+ try {
184
+ config = typeof getConfig === 'function' ? getConfig() : undefined;
185
+ } catch {
186
+ config = undefined;
187
+ }
188
+ if (!readPlanModeActive(context?.agent, getPlanMode, getSessionProjections)) return '';
189
+ return composePlanGuidance({
190
+ brainstorming: readBrainstormingEnabled(config),
191
+ unlimitedBudget: readUnlimitedBudgetEnabled(config),
192
+ });
193
+ },
194
+ });
195
+ }
@@ -0,0 +1,35 @@
1
+ /** The plan-review tool call that marks the pending plan review surface node. */
2
+ export const EXIT_PLAN_MODE_TOOL = 'exit_plan_mode';
3
+
4
+ /**
5
+ * Pure plan-compaction range selection.
6
+ *
7
+ * The pending plan review is the last surface node: an assistant message
8
+ * carrying the plan-review tool call (`exit_plan_mode`). The compactable range
9
+ * starts right after the leading system messages and ends at the last balanced
10
+ * node before that review, snapping backwards until the tool pairing holds.
11
+ */
12
+ export function selectPlanCompactionRange(session, balancedAfter, options = {}) {
13
+ const planReviewTool = typeof options.planReviewTool === 'string'
14
+ ? options.planReviewTool
15
+ : EXIT_PLAN_MODE_TOOL;
16
+ const nodes = session.surface.nodes;
17
+ if (nodes.length === 0) return { error: 'empty' };
18
+ const last = session.eventAt(nodes[nodes.length - 1]);
19
+ if (last?.type !== 'assistant/message') return { error: 'no-review' };
20
+ const content = last.data?.message?.content;
21
+ if (!Array.isArray(content) || !content.some((block) => block?.type === 'tool-call' && block.name === planReviewTool)) {
22
+ return { error: 'no-review' };
23
+ }
24
+
25
+ let startIndex = 0;
26
+ while (startIndex < nodes.length && session.eventAt(nodes[startIndex])?.type === 'system/message') startIndex += 1;
27
+
28
+ let endIndex = nodes.length - 2;
29
+ while (endIndex >= startIndex) {
30
+ if (balancedAfter(session, nodes[endIndex]) === true) break;
31
+ endIndex -= 1;
32
+ }
33
+ if (endIndex < startIndex) return { error: 'empty' };
34
+ return { start: nodes[startIndex], end: nodes[endIndex] };
35
+ }
package/lib/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { Config } from './host/config.js';
2
+ export { apply } from './host/apply.js';
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Great Plan",
4
+ "description": "Choose the model and reasoning effort, then execute a reviewed plan in place, after compaction, or in a fresh session with the original instructions and runtime context."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "卓越计划",
4
+ "description": "在执行计划前选择模型与思考强度,并支持直接执行、压缩上下文后执行,或在新会话中携带原始指令与运行环境执行。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,71 @@
1
1
  {
2
2
  "name": "@onefl666/dsh-great-plan",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.6.0",
4
+ "license": "MIT",
5
+ "author": "onefl666",
6
+ "keywords": [
7
+ "deepseek-harness",
8
+ "dsh",
9
+ "dsh-plugin",
10
+ "dsh-bundle",
11
+ "plan-mode",
12
+ "agent",
13
+ "web-client"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "homepage": "https://github.com/onefl666/dsh-great-plan#readme",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/onefl666/dsh-great-plan.git"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/onefl666/dsh-great-plan/issues"
25
+ },
26
+ "type": "module",
27
+ "main": "lib/index.js",
28
+ "description": "Choose the model and reasoning effort, then execute, compact-and-execute, or start fresh with a reviewed plan.",
29
+ "icon": "./icon.svg",
30
+ "exports": {
31
+ ".": "./lib/index.js",
32
+ "./client": "./lib/client.js",
33
+ "./locale/*.json": "./locale/*.json",
34
+ "./icon": "./icon.svg",
35
+ "./package.json": "./package.json"
36
+ },
37
+ "files": [
38
+ "lib/index.js",
39
+ "lib/host/*.js",
40
+ "lib/client.js",
41
+ "lib/client.*.js",
42
+ "cordis.patch.yml",
43
+ "locale/*.json",
44
+ "icon.svg",
45
+ "README.md",
46
+ "README.zh-CN.md",
47
+ "LICENSE"
48
+ ],
49
+ "peerDependencies": {
50
+ "@deepseek-ai/dsh-compaction": "^0.2.1-rc.3",
51
+ "@deepseek-ai/schemastery": "~3.18.5"
52
+ },
53
+ "dsh": {
54
+ "bundle": {
55
+ "patch": "./cordis.patch.yml"
56
+ },
57
+ "client": {
58
+ "platform": "web",
59
+ "immediately": true,
60
+ "inject": [
61
+ "@deepseek-ai/dsh-client-ui-conversation",
62
+ "@deepseek-ai/dsh-client-ui-user-questions",
63
+ "@deepseek-ai/dsh-client-ui-model-selection",
64
+ "@deepseek-ai/dsh-client-ui-workspace",
65
+ "@deepseek-ai/dsh-client-ui-settings",
66
+ "@deepseek-ai/dsh-client-ui-settings-general",
67
+ "@deepseek-ai/dsh-client-locale"
68
+ ]
69
+ }
70
+ }
71
+ }