@mjasnikovs/pi-task 0.13.29 → 0.13.30
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/README.md +3 -2
- package/dist/config/config.d.ts +1 -0
- package/dist/config/config.js +2 -1
- package/dist/config/register.js +6 -1
- package/dist/task/auto-orchestrator.d.ts +8 -0
- package/dist/task/auto-orchestrator.js +57 -1
- package/dist/task/enforce-guidelines.d.ts +74 -0
- package/dist/task/enforce-guidelines.js +146 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ pi install npm:@mjasnikovs/pi-task
|
|
|
49
49
|
| `/task-auto <feature>` | Plan a feature into a task list and run each title through `/task` in order (resumable). |
|
|
50
50
|
| `/task-auto-resume` | Resume the active `/task-auto` run at the next unfinished task. |
|
|
51
51
|
| `/task-auto-cancel` | Stop the `/task-auto` loop after the current task (still resumable). |
|
|
52
|
-
| `/task-config` | Toggle pi-task settings in an editor dialog: remote server, compress reasoning, auto-commit, and
|
|
52
|
+
| `/task-config` | Toggle pi-task settings in an editor dialog: remote server, compress reasoning, auto-commit, orientation, and enforce guidelines. |
|
|
53
53
|
| `/remote` | Show the QR code & URLs for the web view (`/remote stop` to stop). Answer grill questions, start tasks, and watch progress from your phone. |
|
|
54
54
|
|
|
55
55
|
## The pipeline
|
|
@@ -140,7 +140,7 @@ Resolves an installed npm package, indexes its `.d.ts` files and README into a l
|
|
|
140
140
|
|
|
141
141
|
## Settings — `/task-config`
|
|
142
142
|
|
|
143
|
-
Run `/task-config` to toggle pi-task's behavior in an editor dialog. Settings persist to `~/.config/pi-task/config.json
|
|
143
|
+
Run `/task-config` to toggle pi-task's behavior in an editor dialog. Settings persist to `~/.config/pi-task/config.json`. All default to **on** except **enforce guidelines**:
|
|
144
144
|
|
|
145
145
|
| Setting | What it does |
|
|
146
146
|
| --- | --- |
|
|
@@ -148,6 +148,7 @@ Run `/task-config` to toggle pi-task's behavior in an editor dialog. Settings pe
|
|
|
148
148
|
| **compress reasoning** | After each message, compresses the model's `<think>` blocks down to the decisions/constraints/facts that matter later — keeping long local-model runs from drowning their own context in self-talk. |
|
|
149
149
|
| **auto-commit** | Snapshots the working tree into one git commit per `/task-auto` sub-task (see above). |
|
|
150
150
|
| **orientation** | Pre-reads the project's core files (manifest, config, domain types, schema, entrypoints, API surface) once and hands the contents to the read-heavy research workers, so they skip re-discovering the same files cold. Bounded by a hard byte budget; applied only where it helps (FILES/APIS workers). |
|
|
151
|
+
| **enforce guidelines** _(off by default)_ | Before each `/task-auto` commit, re-checks the sub-task's work against the project's `AGENTS.md` / `CLAUDE.md` (in the working directory). Local models drift and skip those rules, so a fresh pass of the same local model — with edit tools — reads the diff, fixes any violations in place, and returns a verdict. A violation it can't clear (or a pass that can't run) **blocks the commit and stops the run** so non-compliant work is never snapshotted; fix and `/task-auto-resume`. Adds one extra local-model pass per task, so it's opt-in. |
|
|
151
152
|
|
|
152
153
|
## Configuration
|
|
153
154
|
|
package/dist/config/config.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export interface PiTaskConfig {
|
|
|
3
3
|
compressReasoning: boolean;
|
|
4
4
|
autoCommit: boolean;
|
|
5
5
|
orientation: boolean;
|
|
6
|
+
enforceGuidelines: boolean;
|
|
6
7
|
}
|
|
7
8
|
export declare function getConfig(): PiTaskConfig;
|
|
8
9
|
export declare function saveConfig(config: PiTaskConfig): Promise<void>;
|
package/dist/config/config.js
CHANGED
|
@@ -6,7 +6,8 @@ const DEFAULTS = {
|
|
|
6
6
|
remote: true,
|
|
7
7
|
compressReasoning: true,
|
|
8
8
|
autoCommit: true,
|
|
9
|
-
orientation: true
|
|
9
|
+
orientation: true,
|
|
10
|
+
enforceGuidelines: false
|
|
10
11
|
};
|
|
11
12
|
const CONFIG_PATH = path.join(os.homedir(), '.config', 'pi-task', 'config.json');
|
|
12
13
|
const _g = globalThis;
|
package/dist/config/register.js
CHANGED
|
@@ -17,6 +17,11 @@ const ITEMS = [
|
|
|
17
17
|
id: 'orientation',
|
|
18
18
|
label: 'orientation',
|
|
19
19
|
description: 'Pre-supply the project core (manifest, types, schema…) to the read-heavy research workers'
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'enforceGuidelines',
|
|
23
|
+
label: 'enforce guidelines',
|
|
24
|
+
description: 'Before each /task-auto commit, re-check the work against AGENTS.md/CLAUDE.md and fix drift'
|
|
20
25
|
}
|
|
21
26
|
];
|
|
22
27
|
function makeTheme(theme) {
|
|
@@ -53,7 +58,7 @@ async function handleTaskConfig(_args, ctx) {
|
|
|
53
58
|
}
|
|
54
59
|
export function registerConfig(pi) {
|
|
55
60
|
registerBridgeCommand(pi, 'task-config', {
|
|
56
|
-
description: 'Configure pi-task settings (remote, compress reasoning, auto-commit, orientation).',
|
|
61
|
+
description: 'Configure pi-task settings (remote, compress reasoning, auto-commit, orientation, enforce guidelines).',
|
|
57
62
|
handler: handleTaskConfig
|
|
58
63
|
});
|
|
59
64
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import type { RunSingleTaskResult } from './orchestrator.js';
|
|
3
3
|
import { type CommitResult } from './auto-commit.js';
|
|
4
|
+
import { type EnforceOutcome } from './enforce-guidelines.js';
|
|
4
5
|
/**
|
|
5
6
|
* Injectable seams so the planner and loop are testable without spawning pi.
|
|
6
7
|
* `runChild` is used by planAuto; `runTask` is used by runAutoLoop.
|
|
@@ -15,6 +16,13 @@ export interface AutoDeps {
|
|
|
15
16
|
}) => Promise<RunSingleTaskResult>;
|
|
16
17
|
/** Snapshot the working tree into one commit after a task passes. */
|
|
17
18
|
commit: (cwd: string, message: string) => Promise<CommitResult>;
|
|
19
|
+
/**
|
|
20
|
+
* Verify the just-finished task's work against the project's AGENTS.md /
|
|
21
|
+
* CLAUDE.md guidelines (and auto-fix violations) BEFORE it is committed.
|
|
22
|
+
* Optional: when absent (tests, or the feature disabled), the loop treats it
|
|
23
|
+
* as a pass. ok === false blocks the commit and fails the task.
|
|
24
|
+
*/
|
|
25
|
+
enforce?: (cwd: string, taskTitle: string) => Promise<EnforceOutcome>;
|
|
18
26
|
}
|
|
19
27
|
/**
|
|
20
28
|
* Expand any @file references in the feature text by appending each referenced
|
|
@@ -15,6 +15,8 @@ import { isDuplicateQuestion, MAX_DUP_STRIKES, DUP_REPROMPT_HINT } from './quest
|
|
|
15
15
|
import { allocateAutoId, buildAutoBody, parseDecomposeList, parseTaskList, checkOffTask, stampTaskInProgress, findResumableAuto } from './auto-io.js';
|
|
16
16
|
import { writeTaskFile, readTaskFile, updateTaskFrontMatter, taskFilePath } from './task-io.js';
|
|
17
17
|
import { gitCommitAll } from './auto-commit.js';
|
|
18
|
+
import { runGuidelineEnforcement, ENFORCE_TIMEOUT_MS } from './enforce-guidelines.js';
|
|
19
|
+
import { runWorker } from '../workers/pi-worker-core.js';
|
|
18
20
|
import { runPhaseChild, prependHint, USER_CANCELLED } from './child-runner.js';
|
|
19
21
|
import { SessionUI, registerBridgeCommand } from '../remote/bridge.js';
|
|
20
22
|
import { pushNotify } from '../remote/push.js';
|
|
@@ -303,7 +305,43 @@ function defaultDeps(ctx, cwd, signal, title) {
|
|
|
303
305
|
}),
|
|
304
306
|
commit: (cwd2, message) => getConfig().autoCommit ?
|
|
305
307
|
gitCommitAll(cwd2, message, signal)
|
|
306
|
-
: Promise.resolve({ committed: false, reason: 'auto-commit disabled' })
|
|
308
|
+
: Promise.resolve({ committed: false, reason: 'auto-commit disabled' }),
|
|
309
|
+
enforce: (cwd2, _taskTitle) => {
|
|
310
|
+
if (!getConfig().enforceGuidelines) {
|
|
311
|
+
return Promise.resolve({ ok: true, reason: 'disabled' });
|
|
312
|
+
}
|
|
313
|
+
return runGuidelineEnforcement({
|
|
314
|
+
cwd: cwd2,
|
|
315
|
+
signal,
|
|
316
|
+
// Reuse the timeout- and loop-guarded worker child. It runs the
|
|
317
|
+
// same local model with edit tools so it can fix violations in
|
|
318
|
+
// place. Any non-clean exit (loop kill, timeout, leaked tool call,
|
|
319
|
+
// non-zero) throws so the pass becomes a blocking outcome rather
|
|
320
|
+
// than trusting a partial transcript.
|
|
321
|
+
runChild: async (tools, prompt, sig) => {
|
|
322
|
+
const r = await runWorker({
|
|
323
|
+
prompt,
|
|
324
|
+
cwd: cwd2,
|
|
325
|
+
signal: sig,
|
|
326
|
+
tools,
|
|
327
|
+
timeoutMs: ENFORCE_TIMEOUT_MS,
|
|
328
|
+
onLine: line => phaseDeps.logDebug?.(`enforce: ${line}`)
|
|
329
|
+
});
|
|
330
|
+
if (r.aborted && !r.timedOut)
|
|
331
|
+
throw new Error(USER_CANCELLED);
|
|
332
|
+
if (r.timedOut)
|
|
333
|
+
throw new Error('enforcement child timed out');
|
|
334
|
+
if (r.loopHit)
|
|
335
|
+
throw new Error('enforcement child looped');
|
|
336
|
+
if (r.leakedToolCall)
|
|
337
|
+
throw new Error('enforcement child leaked a tool call');
|
|
338
|
+
if (r.exitCode !== 0) {
|
|
339
|
+
throw new Error(`enforcement child exited ${r.exitCode}`);
|
|
340
|
+
}
|
|
341
|
+
return r.text;
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
307
345
|
};
|
|
308
346
|
}
|
|
309
347
|
// ─── Loop ────────────────────────────────────────────────────────────────────
|
|
@@ -402,6 +440,24 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
|
|
|
402
440
|
announceDone(active, `${id} stopped at "${next.title}"${why} — fix and run /task-auto-resume.`, 'error');
|
|
403
441
|
return;
|
|
404
442
|
}
|
|
443
|
+
// Before checking the task off or committing, hold the work to the
|
|
444
|
+
// project's AGENTS.md / CLAUDE.md rules. Local models drift and skip
|
|
445
|
+
// those guidelines, so the enforcement pass re-reads the diff with the
|
|
446
|
+
// same local model (edit tools enabled) and fixes violations in place.
|
|
447
|
+
// A non-clean verdict — or a pass that could not run — blocks the
|
|
448
|
+
// commit and fails the task: non-compliant work is never snapshotted,
|
|
449
|
+
// and the run pauses for /task-auto-resume. (No-op when the feature is
|
|
450
|
+
// off, when there are no guideline files, or in tests with no enforce
|
|
451
|
+
// dep.) Fixes it makes are folded into this task's snapshot below.
|
|
452
|
+
if (deps.enforce) {
|
|
453
|
+
active.ui.notify(`${id}: enforcing AGENTS.md/CLAUDE.md on "${next.title}"…`, 'info');
|
|
454
|
+
const verdict = await deps.enforce(cwd, next.title);
|
|
455
|
+
if (!verdict.ok) {
|
|
456
|
+
await updateTaskFrontMatter(cwd, id, { state: 'failed' });
|
|
457
|
+
announceDone(active, `${id} stopped at "${next.title}" — ${verdict.reason ?? 'guideline check failed'} — fix and run /task-auto-resume.`, 'error');
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
405
461
|
// res.ok === true means runner.run() completed, so res.taskId is the
|
|
406
462
|
// allocated TASK_NNNN id (never empty here). checkOffTask tolerates an
|
|
407
463
|
// empty id by writing a plain checked line, but that path is unreachable.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type SpawnFn } from '../shared/child-process.js';
|
|
2
|
+
/** Filenames discovered in the working directory (cwd only — no tree walk). */
|
|
3
|
+
export declare const GUIDELINE_FILENAMES: readonly ["AGENTS.md", "CLAUDE.md"];
|
|
4
|
+
/** Tools the fix pass is allowed: read/search to inspect, edit/write to fix. */
|
|
5
|
+
declare const ENFORCE_TOOLS = "read,grep,find,ls,edit,write";
|
|
6
|
+
/** Wall-clock ceiling for the enforcement child. Local models are slow and the
|
|
7
|
+
* fix pass reads several files and may rewrite a few; give it room but never
|
|
8
|
+
* let a wedged child hang the whole run. */
|
|
9
|
+
declare const ENFORCE_TIMEOUT_MS = 600000;
|
|
10
|
+
export interface GuidelineDoc {
|
|
11
|
+
/** Filenames found, in discovery order (e.g. ['AGENTS.md', 'CLAUDE.md']). */
|
|
12
|
+
files: string[];
|
|
13
|
+
/** The concatenated rules text, each file under a `## <name>` header. */
|
|
14
|
+
text: string;
|
|
15
|
+
}
|
|
16
|
+
export interface EnforceOutcome {
|
|
17
|
+
/** true → compliant (after any fixes), enforcement disabled, or no
|
|
18
|
+
* guideline files. false → a violation the pass could not clear, or the
|
|
19
|
+
* pass could not run / produced no verdict. */
|
|
20
|
+
ok: boolean;
|
|
21
|
+
/** Short, human-readable reason. Always set when ok === false; on the pass
|
|
22
|
+
* path set to the no-op cause ('disabled', 'no guideline files'). */
|
|
23
|
+
reason?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Read the guideline files that live directly in `cwd` (AGENTS.md, CLAUDE.md).
|
|
27
|
+
* Returns null when none exist or all are empty — the caller treats that as a
|
|
28
|
+
* pass. cwd-only by design: no walk up to the git root.
|
|
29
|
+
*/
|
|
30
|
+
export declare function discoverGuidelines(cwd: string, readFile?: (p: string) => Promise<string>): Promise<GuidelineDoc | null>;
|
|
31
|
+
/**
|
|
32
|
+
* Build the enforcement child's prompt: the rules, the diff of the work just
|
|
33
|
+
* done, and the contract for the final verdict line. Kept pure so the wording
|
|
34
|
+
* is unit-tested without spawning pi.
|
|
35
|
+
*/
|
|
36
|
+
export declare function buildEnforcePrompt(rulesText: string, diff: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Parse the child's final verdict. Scans for the LAST `ENFORCE: CLEAN` /
|
|
39
|
+
* `ENFORCE: VIOLATION` marker (the model may discuss before concluding).
|
|
40
|
+
*
|
|
41
|
+
* No marker at all is treated as NOT clean: a pass that cannot state a verdict
|
|
42
|
+
* is a gray area, and the contract is that uncertain work does not get
|
|
43
|
+
* committed.
|
|
44
|
+
*/
|
|
45
|
+
export declare function parseEnforceVerdict(text: string): {
|
|
46
|
+
clean: boolean;
|
|
47
|
+
detail: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Capture the work to verify as text: the tracked diff against HEAD plus the
|
|
51
|
+
* names of any new (untracked) files. Non-destructive — it does not touch the
|
|
52
|
+
* index, so the later `git add -A` in gitCommitAll still stages everything
|
|
53
|
+
* (including fixes the enforcement child makes).
|
|
54
|
+
*/
|
|
55
|
+
export declare function captureDiff(cwd: string, signal?: AbortSignal, spawnFn?: SpawnFn): Promise<string>;
|
|
56
|
+
export interface EnforcementDeps {
|
|
57
|
+
cwd: string;
|
|
58
|
+
signal?: AbortSignal;
|
|
59
|
+
/** Defaults to the real cwd-only file discovery. */
|
|
60
|
+
discover?: (cwd: string) => Promise<GuidelineDoc | null>;
|
|
61
|
+
/** Defaults to the real git diff capture. */
|
|
62
|
+
getDiff?: (cwd: string, signal?: AbortSignal) => Promise<string>;
|
|
63
|
+
/** Runs the enforcement child and returns its assistant text. Injected so
|
|
64
|
+
* the orchestrator wires the real child runner and tests use a fake. */
|
|
65
|
+
runChild: (tools: string, prompt: string, signal?: AbortSignal) => Promise<string>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Run the full enforcement pass for one task. Discovery is cwd-only; an empty
|
|
69
|
+
* result is a pass. Otherwise capture the diff, run the fix child, and turn its
|
|
70
|
+
* verdict into an ok/blocked outcome. Never throws — a child failure becomes a
|
|
71
|
+
* blocking outcome (ok: false) so an unverifiable task is not committed.
|
|
72
|
+
*/
|
|
73
|
+
export declare function runGuidelineEnforcement(deps: EnforcementDeps): Promise<EnforceOutcome>;
|
|
74
|
+
export { ENFORCE_TOOLS, ENFORCE_TIMEOUT_MS };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guideline enforcement for /task-auto.
|
|
3
|
+
*
|
|
4
|
+
* Local models drift: they skip the rules written in AGENTS.md / CLAUDE.md even
|
|
5
|
+
* when those files sit right next to the code. After a /task-auto sub-task's
|
|
6
|
+
* implementation turn settles — but BEFORE its per-task commit — this runs a
|
|
7
|
+
* fresh child pi (the same local model that did the work) that is handed the
|
|
8
|
+
* project's guideline files plus the task's diff, fixes any violations in place
|
|
9
|
+
* with its edit tools, and reports a CLEAN / VIOLATION verdict.
|
|
10
|
+
*
|
|
11
|
+
* A VIOLATION (or an enforcement pass that cannot confirm CLEAN) blocks the
|
|
12
|
+
* commit and fails the task, so non-compliant work is never snapshotted — the
|
|
13
|
+
* run pauses for /task-auto-resume. This is deliberately strict: the whole
|
|
14
|
+
* point is that "the model probably followed the rules" is not good enough.
|
|
15
|
+
*
|
|
16
|
+
* Gated by the `enforceGuidelines` config flag. With the flag off, or with no
|
|
17
|
+
* guideline files in the working directory, enforcement is a pass (ok: true).
|
|
18
|
+
*/
|
|
19
|
+
import * as fsp from 'node:fs/promises';
|
|
20
|
+
import * as path from 'node:path';
|
|
21
|
+
import { runChildDefault } from '../shared/child-process.js';
|
|
22
|
+
/** Filenames discovered in the working directory (cwd only — no tree walk). */
|
|
23
|
+
export const GUIDELINE_FILENAMES = ['AGENTS.md', 'CLAUDE.md'];
|
|
24
|
+
/** Tools the fix pass is allowed: read/search to inspect, edit/write to fix. */
|
|
25
|
+
const ENFORCE_TOOLS = 'read,grep,find,ls,edit,write';
|
|
26
|
+
/** Wall-clock ceiling for the enforcement child. Local models are slow and the
|
|
27
|
+
* fix pass reads several files and may rewrite a few; give it room but never
|
|
28
|
+
* let a wedged child hang the whole run. */
|
|
29
|
+
const ENFORCE_TIMEOUT_MS = 600_000;
|
|
30
|
+
/**
|
|
31
|
+
* Read the guideline files that live directly in `cwd` (AGENTS.md, CLAUDE.md).
|
|
32
|
+
* Returns null when none exist or all are empty — the caller treats that as a
|
|
33
|
+
* pass. cwd-only by design: no walk up to the git root.
|
|
34
|
+
*/
|
|
35
|
+
export async function discoverGuidelines(cwd, readFile = p => fsp.readFile(p, 'utf8')) {
|
|
36
|
+
const files = [];
|
|
37
|
+
const sections = [];
|
|
38
|
+
for (const name of GUIDELINE_FILENAMES) {
|
|
39
|
+
let raw;
|
|
40
|
+
try {
|
|
41
|
+
raw = await readFile(path.join(cwd, name));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
continue; // missing file — skip
|
|
45
|
+
}
|
|
46
|
+
if (raw.trim().length === 0)
|
|
47
|
+
continue; // present but empty — nothing to enforce
|
|
48
|
+
files.push(name);
|
|
49
|
+
sections.push(`## ${name}\n\n${raw.trim()}`);
|
|
50
|
+
}
|
|
51
|
+
if (files.length === 0)
|
|
52
|
+
return null;
|
|
53
|
+
return { files, text: sections.join('\n\n') };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build the enforcement child's prompt: the rules, the diff of the work just
|
|
57
|
+
* done, and the contract for the final verdict line. Kept pure so the wording
|
|
58
|
+
* is unit-tested without spawning pi.
|
|
59
|
+
*/
|
|
60
|
+
export function buildEnforcePrompt(rulesText, diff) {
|
|
61
|
+
return [
|
|
62
|
+
'You are a strict guideline-enforcement pass running after an AI coding agent',
|
|
63
|
+
'finished a task but before its work is committed. The agent is known to skip',
|
|
64
|
+
'project rules, so do not trust that it followed them — verify against the diff.',
|
|
65
|
+
'',
|
|
66
|
+
'PROJECT GUIDELINES (from this repository):',
|
|
67
|
+
rulesText,
|
|
68
|
+
'',
|
|
69
|
+
'CHANGES JUST MADE (verify these specifically; read any file you need in full):',
|
|
70
|
+
diff.trim().length > 0 ? diff : '(no textual diff captured — inspect the working tree)',
|
|
71
|
+
'',
|
|
72
|
+
'Your job:',
|
|
73
|
+
'1. Check the changed files against EVERY rule above.',
|
|
74
|
+
'2. For each violation you find, FIX it directly using your edit/write tools.',
|
|
75
|
+
' Make the minimal change that satisfies the rule; do not rewrite unrelated code.',
|
|
76
|
+
'3. Do not introduce new behavior or features — only bring the work into compliance.',
|
|
77
|
+
'',
|
|
78
|
+
'When you are done, output EXACTLY ONE of these as the final line:',
|
|
79
|
+
' ENFORCE: CLEAN (the changes comply with every rule, after your fixes)',
|
|
80
|
+
' ENFORCE: VIOLATION <text> (a rule is still violated and you could not fix it; say which)',
|
|
81
|
+
'Output the verdict line verbatim — it is parsed mechanically.'
|
|
82
|
+
].join('\n');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Parse the child's final verdict. Scans for the LAST `ENFORCE: CLEAN` /
|
|
86
|
+
* `ENFORCE: VIOLATION` marker (the model may discuss before concluding).
|
|
87
|
+
*
|
|
88
|
+
* No marker at all is treated as NOT clean: a pass that cannot state a verdict
|
|
89
|
+
* is a gray area, and the contract is that uncertain work does not get
|
|
90
|
+
* committed.
|
|
91
|
+
*/
|
|
92
|
+
export function parseEnforceVerdict(text) {
|
|
93
|
+
const re = /ENFORCE:\s*(CLEAN|VIOLATION)\b[ \t]*(.*)/gi;
|
|
94
|
+
let last = null;
|
|
95
|
+
for (let m = re.exec(text); m !== null; m = re.exec(text))
|
|
96
|
+
last = m;
|
|
97
|
+
if (!last)
|
|
98
|
+
return { clean: false, detail: 'no verdict emitted' };
|
|
99
|
+
const clean = last[1].toUpperCase() === 'CLEAN';
|
|
100
|
+
return { clean, detail: clean ? '' : last[2].trim() || 'unspecified violation' };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Capture the work to verify as text: the tracked diff against HEAD plus the
|
|
104
|
+
* names of any new (untracked) files. Non-destructive — it does not touch the
|
|
105
|
+
* index, so the later `git add -A` in gitCommitAll still stages everything
|
|
106
|
+
* (including fixes the enforcement child makes).
|
|
107
|
+
*/
|
|
108
|
+
export async function captureDiff(cwd, signal, spawnFn) {
|
|
109
|
+
const run = (args) => runChildDefault({ command: 'git', args }, cwd, signal, { mode: 'text' }, spawnFn);
|
|
110
|
+
const tracked = await run(['diff', 'HEAD']);
|
|
111
|
+
const untracked = await run(['ls-files', '--others', '--exclude-standard']);
|
|
112
|
+
const parts = [];
|
|
113
|
+
if (tracked.exitCode === 0 && tracked.stdout.trim().length > 0)
|
|
114
|
+
parts.push(tracked.stdout.trim());
|
|
115
|
+
const newFiles = untracked.exitCode === 0 ? untracked.stdout.trim() : '';
|
|
116
|
+
if (newFiles.length > 0)
|
|
117
|
+
parts.push(`New (untracked) files — read them in full:\n${newFiles}`);
|
|
118
|
+
return parts.join('\n\n');
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Run the full enforcement pass for one task. Discovery is cwd-only; an empty
|
|
122
|
+
* result is a pass. Otherwise capture the diff, run the fix child, and turn its
|
|
123
|
+
* verdict into an ok/blocked outcome. Never throws — a child failure becomes a
|
|
124
|
+
* blocking outcome (ok: false) so an unverifiable task is not committed.
|
|
125
|
+
*/
|
|
126
|
+
export async function runGuidelineEnforcement(deps) {
|
|
127
|
+
const discover = deps.discover ?? (cwd => discoverGuidelines(cwd));
|
|
128
|
+
const getDiff = deps.getDiff ?? ((cwd, signal) => captureDiff(cwd, signal));
|
|
129
|
+
const doc = await discover(deps.cwd);
|
|
130
|
+
if (!doc)
|
|
131
|
+
return { ok: true, reason: 'no guideline files' };
|
|
132
|
+
const diff = await getDiff(deps.cwd, deps.signal);
|
|
133
|
+
let text;
|
|
134
|
+
try {
|
|
135
|
+
text = await deps.runChild(ENFORCE_TOOLS, buildEnforcePrompt(doc.text, diff), deps.signal);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
139
|
+
return { ok: false, reason: `enforcement pass could not run: ${msg}` };
|
|
140
|
+
}
|
|
141
|
+
const verdict = parseEnforceVerdict(text);
|
|
142
|
+
if (verdict.clean)
|
|
143
|
+
return { ok: true };
|
|
144
|
+
return { ok: false, reason: `guideline violation: ${verdict.detail}` };
|
|
145
|
+
}
|
|
146
|
+
export { ENFORCE_TOOLS, ENFORCE_TIMEOUT_MS };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.30",
|
|
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",
|