@mjasnikovs/pi-task 0.16.5 → 0.17.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.
@@ -11,7 +11,6 @@ import { runSingleTask } from './orchestrator.js';
11
11
  import { parseClarifyList, deriveTitle } from './parsers.js';
12
12
  import { renderInlineMarkdown, stripInlineMarkdown } from './inline-markdown.js';
13
13
  import { AUTO_CLARIFY_PROMPT, AUTO_DECOMPOSE_PROMPT } from './auto-prompts.js';
14
- import { getProjectSnapshot } from './file-inventory.js';
15
14
  import { isDuplicateQuestion, MAX_DUP_STRIKES, DUP_REPROMPT_HINT } from './question-dedup.js';
16
15
  import { allocateAutoId, buildAutoBody, parseDecomposeList, parseTaskList, checkOffTask, stampTaskInProgress, findResumableAuto } from './auto-io.js';
17
16
  import { writeTaskFile, readTaskFile, updateTaskFrontMatter, taskFilePath, tasksDir } from './task-io.js';
@@ -213,13 +212,6 @@ export async function planAuto(ctx, cwd, feature, deps) {
213
212
  if (planPhantoms.length > 0) {
214
213
  logPlanDebug(cwd, `phantom specifiers rewritten in plan spec: ${planPhantoms.map(x => x.spec).join(', ')}`);
215
214
  }
216
- // Factual snapshot of the target directory (root files + top-level dirs).
217
- // Clarify only "may" read the repo; a model that skips the read assumes a
218
- // greenfield project and recommends bootstrapping config/manifest files that
219
- // already exist (or invents a path like /workspace). Handing it the facts
220
- // up front removes the guess. '' for a non-git/empty tree ⇒ prompt omits the
221
- // block and the model correctly treats the target as greenfield.
222
- const projectState = await getProjectSnapshot(cwd);
223
215
  const answers = [];
224
216
  // Plain text of every question already shown, for the duplicate backstop.
225
217
  const askedQuestions = [];
@@ -232,7 +224,7 @@ export async function planAuto(ctx, cwd, feature, deps) {
232
224
  // Open-ended: keep asking until the model emits NONE or the user dismisses —
233
225
  // but never past MAX_CLARIFY_QUESTIONS distinct questions.
234
226
  while (askedQuestions.length < MAX_CLARIFY_QUESTIONS) {
235
- const qRaw = await deps.runChild('auto-clarify', 'read', prependHint(dupHint, AUTO_CLARIFY_PROMPT(featureForModel, answers.join('\n'), projectState)));
227
+ const qRaw = await deps.runChild('auto-clarify', 'read', prependHint(dupHint, AUTO_CLARIFY_PROMPT(featureForModel, answers.join('\n'))));
236
228
  const parsed = parseClarifyList(qRaw);
237
229
  if (parsed.length === 0)
238
230
  break; // NONE / nothing left to ask
@@ -9,7 +9,7 @@
9
9
  * when no clarification remains. priorQA carries the questions already answered so
10
10
  * each next question adapts to them.
11
11
  */
12
- export declare const AUTO_CLARIFY_PROMPT: (feature: string, priorQA: string, projectState?: string) => string;
12
+ export declare const AUTO_CLARIFY_PROMPT: (feature: string, priorQA: string) => string;
13
13
  /**
14
14
  * Decompose: output a markdown checkbox list of task titles (one line each).
15
15
  */
@@ -9,19 +9,16 @@
9
9
  * when no clarification remains. priorQA carries the questions already answered so
10
10
  * each next question adapts to them.
11
11
  */
12
- export const AUTO_CLARIFY_PROMPT = (feature, priorQA, projectState = '') => `You are planning how to split a feature into separate implementation tasks, one clarifying question at a time.
12
+ export const AUTO_CLARIFY_PROMPT = (feature, priorQA) => `You are planning how to split a feature into separate implementation tasks, one clarifying question at a time.
13
13
 
14
14
  FEATURE REQUEST:
15
15
  ${feature.trim()}
16
16
 
17
17
  ANSWERS SO FAR:
18
18
  ${priorQA.trim() || '(none yet)'}
19
- ${projectState.trim() ? `\n${projectState.trim()}\n` : ''}
19
+
20
20
  You may use the read tool to inspect the repo and any referenced docs so your
21
- question and recommendation are grounded in what already exists. When a PROJECT
22
- STATE snapshot is given above, trust it: never assume an empty or greenfield
23
- project, never invent directory paths, and never recommend creating files it
24
- already lists — at most propose updating them.
21
+ question and recommendation are grounded in what already exists.
25
22
 
26
23
  Output the SINGLE most important clarifying question that REMAINS — the one whose
27
24
  answer would most change HOW this feature is split into tasks (scope boundaries,
@@ -17,19 +17,3 @@ export declare function stripTasksDir(raw: string): string;
17
17
  /** Cap output to maxLines real (non-blank) paths; tag truncation when cut. */
18
18
  export declare function capInventory(raw: string, maxLines?: number): string;
19
19
  export declare function getFileInventory(cwd: string, signal?: AbortSignal, maxLines?: number): Promise<string>;
20
- /**
21
- * Derive a compact PROJECT STATE block (root-level files + top-level directories)
22
- * from a raw `git ls-files` listing. Pure/testable; `getProjectSnapshot` wires it
23
- * to the live repo. Returns '' when there is nothing tracked so the caller omits
24
- * the block entirely (and the model is then correctly free to treat the target as
25
- * an empty/greenfield project).
26
- */
27
- export declare function formatProjectSnapshot(raw: string): string;
28
- /**
29
- * Live PROJECT STATE snapshot for the planning phase. Clarify otherwise only
30
- * "may" read the repo; a weak model that skips the read then assumes a greenfield
31
- * project and recommends creating package.json/tsconfig from scratch — or invents
32
- * a path like /workspace — even when those configs already exist on disk. Handing
33
- * it the facts up front removes the guess. Empty (non-git / nothing tracked) ⇒ ''.
34
- */
35
- export declare function getProjectSnapshot(cwd: string, signal?: AbortSignal): Promise<string>;
@@ -59,40 +59,3 @@ export async function getFileInventory(cwd, signal, maxLines = DEFAULT_MAX_LINES
59
59
  return '';
60
60
  return capInventory(stripTasksDir(raw), maxLines);
61
61
  }
62
- /**
63
- * Derive a compact PROJECT STATE block (root-level files + top-level directories)
64
- * from a raw `git ls-files` listing. Pure/testable; `getProjectSnapshot` wires it
65
- * to the live repo. Returns '' when there is nothing tracked so the caller omits
66
- * the block entirely (and the model is then correctly free to treat the target as
67
- * an empty/greenfield project).
68
- */
69
- export function formatProjectSnapshot(raw) {
70
- const lines = stripTasksDir(raw)
71
- .split('\n')
72
- .map(l => l.trim())
73
- .filter(l => l.length > 0);
74
- const rootFiles = [...new Set(lines.filter(l => !l.includes('/')))].sort();
75
- const dirs = [...new Set(lines.filter(l => l.includes('/')).map(l => l.split('/')[0]))].sort();
76
- if (rootFiles.length === 0 && dirs.length === 0)
77
- return '';
78
- return ('EXISTING PROJECT STATE (a factual snapshot of the target directory — trust it over '
79
- + 'assumptions: do NOT assume an empty or greenfield project, do NOT invent directory '
80
- + 'paths, and do NOT propose creating files that already exist. Treat the config/manifest '
81
- + "files listed here as the project's established setup unless the request explicitly says "
82
- + 'to replace them):\n'
83
- + `Root files: ${rootFiles.join(', ') || '(none)'}\n`
84
- + `Top-level directories: ${dirs.join(', ') || '(none)'}`);
85
- }
86
- /**
87
- * Live PROJECT STATE snapshot for the planning phase. Clarify otherwise only
88
- * "may" read the repo; a weak model that skips the read then assumes a greenfield
89
- * project and recommends creating package.json/tsconfig from scratch — or invents
90
- * a path like /workspace — even when those configs already exist on disk. Handing
91
- * it the facts up front removes the guess. Empty (non-git / nothing tracked) ⇒ ''.
92
- */
93
- export async function getProjectSnapshot(cwd, signal) {
94
- const raw = await runGitLsFiles(cwd, signal);
95
- if (!raw)
96
- return '';
97
- return formatProjectSnapshot(raw);
98
- }
@@ -42,6 +42,14 @@ export interface PhaseConfig {
42
42
  export declare function extractToolingCommands(research: string): string[] | null;
43
43
  /** Replace the TOOLING section in a research string with a VERIFIED-TOOLING section. */
44
44
  export declare function replaceToolingWithVerified(research: string, verifiedCommands: string[]): string;
45
+ /**
46
+ * Manifest + config content (orientation tiers 0–1) for refine, with the preserve
47
+ * directive. Reuses the SAME orientation machinery research feeds its workers — not
48
+ * a parallel reader — scoped to the "accumulative" files a from-scratch rewrite
49
+ * silently destroys. '' (non-git/empty/orientation-off) leaves refine unchanged, so
50
+ * a genuinely greenfield task is still free to create.
51
+ */
52
+ export declare function refineExistingFilesBlock(deps: PhaseDeps): Promise<string>;
45
53
  export declare const phaseRefine: (deps: PhaseDeps, raw: string, planContext?: string) => Promise<string>;
46
54
  export declare function phaseVerifyTooling(deps: PhaseDeps, research: string): Promise<string>;
47
55
  export interface PhaseResearchDeps extends ExternalContextDeps {
@@ -11,7 +11,7 @@ import { search as defaultSearch } from '../workers/search-core.js';
11
11
  import { extractEnrichTargets } from './enrichment.js';
12
12
  import { isIntegrationUnknown } from './unknown-routing.js';
13
13
  import { getFileInventory } from './file-inventory.js';
14
- import { buildOrientation } from './orientation.js';
14
+ import { buildOrientation, orientationTier } from './orientation.js';
15
15
  import { getConfig } from '../config/config.js';
16
16
  import { readFile } from 'node:fs/promises';
17
17
  import { resolve } from 'node:path';
@@ -63,15 +63,67 @@ export function replaceToolingWithVerified(research, verifiedCommands) {
63
63
  return replaced;
64
64
  }
65
65
  // ─── Phase functions ─────────────────────────────────────────────────────────
66
- export const phaseRefine = (deps, raw, planContext) => runPhaseWithLoopGuard(deps, 'refine', 'read', hint => prependHint(hint, appendNoThink(REFINE_PROMPT(raw, planContext))),
67
- // refine's deliverable is a 4-section text rewrite that never strictly
68
- // needs a successful read on a test-writing task against a large
69
- // existing codebase the model over-explores (re-reads source hunting for
70
- // the impl) and burns the loop budget. Degrade to a no-tools final
71
- // attempt instead of hard-failing the whole run. See TASK_0016 (mx5):
72
- // refine looped 3×/resume forever; the deliverable was always producible
73
- // from the title + design doc alone.
74
- { degradeOnExhaustion: true });
66
+ // Authoritative directive that travels with the refine orientation block. Refine
67
+ // runs BEFORE research, so on a "Scaffold project with package.json…" title it has
68
+ // no signal the file already exists and authors greenfield strip-constraints
69
+ // ("bun-plugin-tailwind the only dependency", "exactly N scripts") that compose
70
+ // then obeys over the research facts the implementer executes a wholesale
71
+ // rewrite that drops every existing dependency and script (mx5 TASK_0001 emptied
72
+ // package.json; the REAL pipeline reproduced it 3/3 even with research surfacing
73
+ // the deps). Handing refine the manifest/config CONTENT up front, with this
74
+ // reframe, fixes it at the origin (A/B: 1/5 → 5/5 preserve, 5/5 end-to-end).
75
+ const REFINE_PRESERVE_DIRECTIVE = 'EXISTING FILES ON DISK — AUTHORITATIVE (overrides any "scaffold / create / set '
76
+ + 'up / initialize / from scratch / only / exactly / minimal" wording in the task '
77
+ + 'below): the files shown in the PROJECT ORIENTATION block ALREADY EXIST. When '
78
+ + 'the task says to scaffold, create, set up, initialize, or configure a file that '
79
+ + 'already exists, your GOAL and CONSTRAINTS MUST frame it as an in-place UPDATE '
80
+ + 'that PRESERVES every existing dependency, devDependency, script, field, and '
81
+ + "compiler option — adding or changing only the task's explicit delta. Do NOT "
82
+ + 'author any constraint that empties, reduces to a fixed/minimal set, renames, '
83
+ + 'recreates, or drops existing entries (no "X is the only dependency", no '
84
+ + '"exactly N scripts", no "produce exactly N files from scratch"). An existing '
85
+ + 'entry is NEVER scope drift and is never removed because it "belongs to a later step".';
86
+ /**
87
+ * Manifest + config content (orientation tiers 0–1) for refine, with the preserve
88
+ * directive. Reuses the SAME orientation machinery research feeds its workers — not
89
+ * a parallel reader — scoped to the "accumulative" files a from-scratch rewrite
90
+ * silently destroys. '' (non-git/empty/orientation-off) leaves refine unchanged, so
91
+ * a genuinely greenfield task is still free to create.
92
+ */
93
+ export async function refineExistingFilesBlock(deps) {
94
+ if (!getConfig().orientation)
95
+ return '';
96
+ const inventoryRaw = await getFileInventory(deps.cwd, deps.signal).catch(() => '');
97
+ if (inventoryRaw.length === 0)
98
+ return '';
99
+ const paths = inventoryRaw
100
+ .split('\n')
101
+ .map(l => l.trim())
102
+ .filter(l => l.length > 0 && (orientationTier(l) === 0 || orientationTier(l) === 1));
103
+ if (paths.length === 0)
104
+ return '';
105
+ const { block } = await buildOrientation(paths, async (p) => {
106
+ try {
107
+ return await readFile(resolve(deps.cwd, p), 'utf8');
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ });
113
+ return block.trim().length === 0 ? '' : `${REFINE_PRESERVE_DIRECTIVE}\n\n${block.trim()}`;
114
+ }
115
+ export const phaseRefine = async (deps, raw, planContext) => {
116
+ const existingFiles = await refineExistingFilesBlock(deps).catch(() => '');
117
+ return runPhaseWithLoopGuard(deps, 'refine', 'read', hint => prependHint(hint, appendNoThink(REFINE_PROMPT(raw, planContext, existingFiles))),
118
+ // refine's deliverable is a 4-section text rewrite that never strictly
119
+ // needs a successful read — on a test-writing task against a large
120
+ // existing codebase the model over-explores (re-reads source hunting for
121
+ // the impl) and burns the loop budget. Degrade to a no-tools final
122
+ // attempt instead of hard-failing the whole run. See TASK_0016 (mx5):
123
+ // refine looped 3×/resume forever; the deliverable was always producible
124
+ // from the title + design doc alone.
125
+ { degradeOnExhaustion: true });
126
+ };
75
127
  export async function phaseVerifyTooling(deps, research) {
76
128
  const commands = extractToolingCommands(research);
77
129
  if (!commands || commands.length === 0) {
@@ -47,7 +47,7 @@ export declare const COMPRESS_LABEL_PROMPT: (title: string, maxChars: number) =>
47
47
  * "Scaffold …" title re-expands the entire design into one task (validated: a real
48
48
  * /task-auto run implemented all 24 steps under step 1).
49
49
  */
50
- declare const REFINE_PROMPT: (raw: string, planContext?: string) => string;
50
+ declare const REFINE_PROMPT: (raw: string, planContext?: string, existingFiles?: string) => string;
51
51
  declare const RESEARCH_READ_ONLY_CONSTRAINT = "IMPORTANT: You are ONLY allowed to READ. Do NOT create, modify, or delete any files. Use the read, grep, find, and ls tools to inspect the repo.";
52
52
  declare const RESEARCH_FILES_PROMPT: (refined: string) => string;
53
53
  declare const RESEARCH_APIS_PROMPT: (refined: string) => string;
@@ -61,7 +61,7 @@ ${title}`;
61
61
  * "Scaffold …" title re-expands the entire design into one task (validated: a real
62
62
  * /task-auto run implemented all 24 steps under step 1).
63
63
  */
64
- const REFINE_PROMPT = (raw, planContext) => `${planContext ? planContext + '\n\n---\n\n' : ''}You receive a user's task description for an AI coding agent. Rewrite it to be unambiguous and actionable.
64
+ const REFINE_PROMPT = (raw, planContext, existingFiles) => `${planContext ? planContext + '\n\n---\n\n' : ''}You receive a user's task description for an AI coding agent. Rewrite it to be unambiguous and actionable.
65
65
 
66
66
  Output structure (four sections, exact headings, in this order):
67
67
 
@@ -87,7 +87,7 @@ Rules:
87
87
  - Do not invent requirements not implied by the input.
88
88
  - If the task references a design/spec document (an @-path or a named spec file), READ it and treat it as authoritative. Carry its concrete schema verbatim into GOAL/CONSTRAINTS — table and column names, types, endpoint methods and paths, enum values. The task title is only a pointer into that spec: where the title and the spec disagree, follow the spec, and never introduce a table, column, endpoint, or dependency the spec does not define.
89
89
  - Do not output any preamble, commentary, or markdown headings beyond the four sections above.
90
-
90
+ ${existingFiles && existingFiles.trim() ? `\n${existingFiles.trim()}\n` : ''}
91
91
  Task: ${raw}`;
92
92
  // ─── Research fan-out prompts ─────────────────────────────────────────────────
93
93
  const RESEARCH_READ_ONLY_CONSTRAINT = `IMPORTANT: You are ONLY allowed to READ. Do NOT create, modify, or delete any files. Use the read, grep, find, and ls tools to inspect the repo.`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mjasnikovs/pi-task",
3
- "version": "0.16.5",
3
+ "version": "0.17.0",
4
4
  "description": "Deterministic spec-orchestration for local models, with a bundled real-time remote web view and web/docs/fetch/worker subagent tools.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",