@mjasnikovs/pi-task 0.18.24 → 0.18.26
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/config/config.d.ts +29 -0
- package/dist/config/config.js +25 -1
- package/dist/config/register.js +23 -2
- package/dist/index.js +2 -0
- package/dist/task/artifact-closure.d.ts +107 -0
- package/dist/task/artifact-closure.js +826 -0
- package/dist/task/auto-orchestrator.js +42 -3
- package/dist/task/command-watchdog.d.ts +67 -0
- package/dist/task/command-watchdog.js +114 -0
- package/dist/task/final-gate.js +22 -0
- package/dist/task/requirements.d.ts +6 -1
- package/dist/task/requirements.js +15 -2
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* This module currently holds the planning half (AutoDeps + planAuto). The run
|
|
6
6
|
* loop, command handlers, and defaultDeps are added by the next task.
|
|
7
7
|
*/
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
8
9
|
import * as fsp from 'node:fs/promises';
|
|
9
10
|
import * as path from 'node:path';
|
|
10
11
|
import { gateRunTask, markResumable } from './orchestrator.js';
|
|
@@ -35,6 +36,7 @@ import { CONTRACT_EXTRACT_PROMPT, parseContractLines, keepGroundedContracts, app
|
|
|
35
36
|
import { reconcileTitleSources } from './decompose-fidelity.js';
|
|
36
37
|
import { REQUIREMENT_EXTRACT_PROMPT, COVERAGE_MAP_PROMPT, parseRequirementLines, keepGroundedRequirements, capRequirements, enumerateObligationPassages, uncoveredPassages, extractionRetryHint, parseCoverageMap, accountCoverage, isCrossCuttingRequirement, appendCarriedRequirements, buildRequirementsLedger } from './requirements.js';
|
|
37
38
|
import { decideAdoption, groundedCoverage } from './coverage-loop.js';
|
|
39
|
+
import { findSpecDanglingArtifacts, titlesCoverArtifact, danglingMissingText, danglingCarryText } from './artifact-closure.js';
|
|
38
40
|
import { LAUNCH_EXTRACT_PROMPT, enumerateScriptCandidates, parseScriptLines, keepGroundedScripts, appendDeclaredScripts } from './launch-contract.js';
|
|
39
41
|
// Hard ceiling on clarify questions per feature. The loop is open-ended (it stops
|
|
40
42
|
// when the model emits NONE), but a model that never says NONE would otherwise
|
|
@@ -493,6 +495,25 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
493
495
|
catch {
|
|
494
496
|
// best-effort channel
|
|
495
497
|
}
|
|
498
|
+
// Artifact-production closure, plan side (mx5 run 13, PROMPT 2): runtime
|
|
499
|
+
// files the spec REFERENCES (server snippets, prose "serve the built
|
|
500
|
+
// index.html") that neither its file tree, its parsed build outputs, nor the
|
|
501
|
+
// existing scaffold produce. Sentence-grounded coverage credited the SERVING
|
|
502
|
+
// side and reported "0 unowned" while nothing ever CREATED the file — so
|
|
503
|
+
// these ride the coverage loop's `missing` list as unowned areas until some
|
|
504
|
+
// task title claims the artifact (grounded in titles, which the coverage-map
|
|
505
|
+
// model cannot fake — the run-12 lesson). Deterministic and best-effort.
|
|
506
|
+
let specDangling = [];
|
|
507
|
+
try {
|
|
508
|
+
specDangling = findSpecDanglingArtifacts(featureForModel, rel => existsSync(path.join(cwd, rel)));
|
|
509
|
+
if (specDangling.length > 0) {
|
|
510
|
+
logPlanDebug(cwd, `artifact closure: ${specDangling.length} dangling runtime artifact(s) in the `
|
|
511
|
+
+ `spec: ${specDangling.map(d => d.path).join(', ')}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
// best-effort channel
|
|
516
|
+
}
|
|
496
517
|
// decompose
|
|
497
518
|
const decomposePrompt = AUTO_DECOMPOSE_PROMPT(featureForModel, clarifications, buildRequirementsLedger(reqEntries));
|
|
498
519
|
// Parse + FIDELITY RECONCILIATION (mx5 run 11, goal B): ground each title's
|
|
@@ -589,6 +610,13 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
589
610
|
}
|
|
590
611
|
}
|
|
591
612
|
const missing = [...verdictMissing, ...(acc?.unmapped ?? []).map(e => `"${e.quote}"`)];
|
|
613
|
+
// Unclaimed dangling artifacts are unowned areas: they force a coverage
|
|
614
|
+
// round that assigns a producing task, and clear as soon as a title
|
|
615
|
+
// names the file.
|
|
616
|
+
for (const d of specDangling) {
|
|
617
|
+
if (!titlesCoverArtifact(titles, d))
|
|
618
|
+
missing.push(danglingMissingText(d));
|
|
619
|
+
}
|
|
592
620
|
return {
|
|
593
621
|
plan: { titles, covered, missing },
|
|
594
622
|
accounting: acc,
|
|
@@ -707,12 +735,23 @@ export async function planAuto(ctx, cwd, feature, deps) {
|
|
|
707
735
|
const carriedCrossCutting = accounting?.crossCutting ?? [];
|
|
708
736
|
const carriedUnmapped = accounting?.unmapped ?? [];
|
|
709
737
|
const carriedJudge = best.judgeMissing;
|
|
710
|
-
|
|
711
|
-
|
|
738
|
+
// Dangling artifacts still unclaimed by any title of the SHIPPING plan are a
|
|
739
|
+
// fourth channel: the producing obligation travels verbatim into every task
|
|
740
|
+
// (whichever task builds the referencing side must also produce the file),
|
|
741
|
+
// and the final gate re-checks the shipped tree regardless.
|
|
742
|
+
const carriedDangling = specDangling
|
|
743
|
+
.filter(d => !titlesCoverArtifact(planTitles, d))
|
|
744
|
+
.map(danglingCarryText);
|
|
745
|
+
if (carriedCrossCutting.length > 0
|
|
746
|
+
|| carriedUnmapped.length > 0
|
|
747
|
+
|| carriedJudge.length > 0
|
|
748
|
+
|| carriedDangling.length > 0) {
|
|
749
|
+
await appendCarriedRequirements(cwd, carriedCrossCutting, carriedUnmapped, carriedJudge, carriedDangling);
|
|
712
750
|
const parts = [
|
|
713
751
|
carriedCrossCutting.length > 0 ? `${carriedCrossCutting.length} cross-cutting` : '',
|
|
714
752
|
carriedUnmapped.length > 0 ? `${carriedUnmapped.length} unowned` : '',
|
|
715
|
-
carriedJudge.length > 0 ? `${carriedJudge.length} judge-flagged` : ''
|
|
753
|
+
carriedJudge.length > 0 ? `${carriedJudge.length} judge-flagged` : '',
|
|
754
|
+
carriedDangling.length > 0 ? `${carriedDangling.length} dangling-artifact` : ''
|
|
716
755
|
].filter(p => p.length > 0);
|
|
717
756
|
ctx.ui.notify(`/task-auto: carrying ${parts.join(', ')} requirement(s) into every task`
|
|
718
757
|
+ ' — see .pi-tasks/requirements.md.', 'info');
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
/**
|
|
3
|
+
* Command watchdog — cancels a single tool execution that overruns the
|
|
4
|
+
* configured ceiling and reminds the model to bound its own commands.
|
|
5
|
+
*
|
|
6
|
+
* WHY: a local model in the MAIN session routinely runs a command that never
|
|
7
|
+
* returns — `godot --headless --check-only` with no timeout, a dev server, a
|
|
8
|
+
* hung test — and the run wedges until the user manually aborts and tells the
|
|
9
|
+
* model to add a timeout. pi's bash tool takes an OPTIONAL `timeout` with NO
|
|
10
|
+
* default (see pi-coding-agent tools/bash.js), so any command the model didn't
|
|
11
|
+
* bound runs forever. This supplies the missing default from the host side.
|
|
12
|
+
*
|
|
13
|
+
* HOW: arm a wall-clock timer on `tool_execution_start`, disarm it on
|
|
14
|
+
* `tool_execution_end`. If it elapses, `ctx.abort()` cancels the in-flight
|
|
15
|
+
* operation — which fires the tool's AbortSignal, and pi's bash executor kills
|
|
16
|
+
* the whole process tree on abort — then a follow-up user turn tells the model
|
|
17
|
+
* what happened so it retries with a timeout instead of hanging again.
|
|
18
|
+
*
|
|
19
|
+
* Tool-agnostic: it arms on ANY tool, honouring "any command can run forever",
|
|
20
|
+
* though in practice only bash runs long enough to trip it. The pure timer
|
|
21
|
+
* state lives in {@link CommandWatchdog}; all side effects (abort, reminder,
|
|
22
|
+
* per-call ctx lookup) live in the registration's `onFire`, so the machine is
|
|
23
|
+
* unit-testable without a real pi session.
|
|
24
|
+
*/
|
|
25
|
+
/** Opaque timer handle — a real `setTimeout` return in production, anything the
|
|
26
|
+
* test's fake scheduler hands back under test. */
|
|
27
|
+
export type TimerHandle = unknown;
|
|
28
|
+
export interface WatchdogDeps {
|
|
29
|
+
/**
|
|
30
|
+
* The ceiling in ms, read PER command-start so a /task-config change takes
|
|
31
|
+
* effect on the next command with no reload. 0 (or any non-positive value)
|
|
32
|
+
* means the watchdog is off and never arms.
|
|
33
|
+
*/
|
|
34
|
+
getTimeoutMs: () => number;
|
|
35
|
+
schedule: (fn: () => void, ms: number) => TimerHandle;
|
|
36
|
+
cancel: (handle: TimerHandle) => void;
|
|
37
|
+
/** Invoked when a command overruns: the registration aborts + reminds here. */
|
|
38
|
+
onFire: (toolCallId: string, toolName: string, timeoutMs: number) => void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The reminder delivered to the model after its command is cancelled. Kept pure
|
|
42
|
+
* and exported so a test can assert its shape without driving the whole session.
|
|
43
|
+
*/
|
|
44
|
+
export declare function reminderMessage(toolName: string, timeoutMs: number): string;
|
|
45
|
+
export declare class CommandWatchdog {
|
|
46
|
+
private readonly deps;
|
|
47
|
+
/** Armed timers, keyed by the tool call they guard. Tool executions are
|
|
48
|
+
* sequential, so this holds at most one entry in normal operation, but the
|
|
49
|
+
* map keeps it correct even if pi ever overlaps two calls. */
|
|
50
|
+
private readonly active;
|
|
51
|
+
constructor(deps: WatchdogDeps);
|
|
52
|
+
/** Arm a timer for a starting tool. No-op when the watchdog is off. */
|
|
53
|
+
onStart(toolCallId: string, toolName: string): void;
|
|
54
|
+
/** Disarm the timer for a finished tool. */
|
|
55
|
+
onEnd(toolCallId: string): void;
|
|
56
|
+
/** Cancel every armed timer — a turn-end / session-shutdown safety net so no
|
|
57
|
+
* stray timer can fire into a later, unrelated command. */
|
|
58
|
+
clearAll(): void;
|
|
59
|
+
private disarm;
|
|
60
|
+
private fire;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Wire the watchdog into the main session. Only ever active in the host session
|
|
64
|
+
* (children run `--no-extensions`), which is exactly where the observed hangs
|
|
65
|
+
* happen.
|
|
66
|
+
*/
|
|
67
|
+
export declare function registerCommandWatchdog(pi: ExtensionAPI): void;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { getConfig } from '../config/config.js';
|
|
2
|
+
/**
|
|
3
|
+
* The reminder delivered to the model after its command is cancelled. Kept pure
|
|
4
|
+
* and exported so a test can assert its shape without driving the whole session.
|
|
5
|
+
*/
|
|
6
|
+
export function reminderMessage(toolName, timeoutMs) {
|
|
7
|
+
const mins = Math.max(1, Math.round(timeoutMs / 60_000));
|
|
8
|
+
return (`[SYSTEM] Your \`${toolName}\` call ran longer than ${mins} minute`
|
|
9
|
+
+ `${mins === 1 ? '' : 's'} and was automatically cancelled — it looked stuck. `
|
|
10
|
+
// Anti-fabrication: a live run showed the model react to the cancel by
|
|
11
|
+
// reporting the killed command as succeeded ("the server is now running").
|
|
12
|
+
// State plainly that it produced nothing so the model can't claim success.
|
|
13
|
+
+ `The command was killed before it finished and produced NO result, so do not `
|
|
14
|
+
+ `report it as completed or successful, and do not claim that anything it would `
|
|
15
|
+
+ `have started (a server, build, or process) is now running. `
|
|
16
|
+
+ `If it was a genuinely long-running command, you MUST re-run it with an explicit `
|
|
17
|
+
+ `timeout — set the bash tool's \`timeout\` parameter (in seconds) so it cannot hang `
|
|
18
|
+
+ `again — or break it into smaller steps. Do NOT simply retry the same unbounded command.`);
|
|
19
|
+
}
|
|
20
|
+
export class CommandWatchdog {
|
|
21
|
+
deps;
|
|
22
|
+
/** Armed timers, keyed by the tool call they guard. Tool executions are
|
|
23
|
+
* sequential, so this holds at most one entry in normal operation, but the
|
|
24
|
+
* map keeps it correct even if pi ever overlaps two calls. */
|
|
25
|
+
active = new Map();
|
|
26
|
+
constructor(deps) {
|
|
27
|
+
this.deps = deps;
|
|
28
|
+
}
|
|
29
|
+
/** Arm a timer for a starting tool. No-op when the watchdog is off. */
|
|
30
|
+
onStart(toolCallId, toolName) {
|
|
31
|
+
const ms = this.deps.getTimeoutMs();
|
|
32
|
+
if (!(ms > 0))
|
|
33
|
+
return;
|
|
34
|
+
// A duplicate start for the same id must not leak the previous timer.
|
|
35
|
+
this.disarm(toolCallId);
|
|
36
|
+
const handle = this.deps.schedule(() => this.fire(toolCallId, toolName, ms), ms);
|
|
37
|
+
this.active.set(toolCallId, handle);
|
|
38
|
+
}
|
|
39
|
+
/** Disarm the timer for a finished tool. */
|
|
40
|
+
onEnd(toolCallId) {
|
|
41
|
+
this.disarm(toolCallId);
|
|
42
|
+
}
|
|
43
|
+
/** Cancel every armed timer — a turn-end / session-shutdown safety net so no
|
|
44
|
+
* stray timer can fire into a later, unrelated command. */
|
|
45
|
+
clearAll() {
|
|
46
|
+
for (const handle of this.active.values())
|
|
47
|
+
this.deps.cancel(handle);
|
|
48
|
+
this.active.clear();
|
|
49
|
+
}
|
|
50
|
+
disarm(toolCallId) {
|
|
51
|
+
const handle = this.active.get(toolCallId);
|
|
52
|
+
if (handle !== undefined) {
|
|
53
|
+
this.deps.cancel(handle);
|
|
54
|
+
this.active.delete(toolCallId);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
fire(toolCallId, toolName, ms) {
|
|
58
|
+
// If the tool ended in the same tick the timer fired, its entry is gone
|
|
59
|
+
// already — never abort a command that has just finished cleanly.
|
|
60
|
+
if (!this.active.has(toolCallId))
|
|
61
|
+
return;
|
|
62
|
+
this.active.delete(toolCallId);
|
|
63
|
+
this.deps.onFire(toolCallId, toolName, ms);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Wire the watchdog into the main session. Only ever active in the host session
|
|
68
|
+
* (children run `--no-extensions`), which is exactly where the observed hangs
|
|
69
|
+
* happen.
|
|
70
|
+
*/
|
|
71
|
+
export function registerCommandWatchdog(pi) {
|
|
72
|
+
// The ctx that owns each in-flight tool's AbortSignal, captured per start so
|
|
73
|
+
// the timer callback (which fires outside the event handler) aborts the
|
|
74
|
+
// right operation.
|
|
75
|
+
const ctxByCall = new Map();
|
|
76
|
+
const watchdog = new CommandWatchdog({
|
|
77
|
+
getTimeoutMs: () => getConfig().requestTimeoutMs,
|
|
78
|
+
schedule: (fn, ms) => {
|
|
79
|
+
const handle = setTimeout(fn, ms);
|
|
80
|
+
// Don't let a pending watchdog timer keep the process alive on exit.
|
|
81
|
+
if (typeof handle.unref === 'function') {
|
|
82
|
+
;
|
|
83
|
+
handle.unref();
|
|
84
|
+
}
|
|
85
|
+
return handle;
|
|
86
|
+
},
|
|
87
|
+
cancel: handle => clearTimeout(handle),
|
|
88
|
+
onFire: (toolCallId, toolName, timeoutMs) => {
|
|
89
|
+
const ctx = ctxByCall.get(toolCallId);
|
|
90
|
+
ctxByCall.delete(toolCallId);
|
|
91
|
+
// Cancel the stuck command (kills the tool's whole process tree via
|
|
92
|
+
// the turn's AbortSignal), then start a fresh turn telling the model
|
|
93
|
+
// to bound its next attempt.
|
|
94
|
+
ctx?.abort();
|
|
95
|
+
pi.sendUserMessage(reminderMessage(toolName, timeoutMs), { deliverAs: 'followUp' });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
pi.on('tool_execution_start', (event, ctx) => {
|
|
99
|
+
ctxByCall.set(event.toolCallId, ctx);
|
|
100
|
+
watchdog.onStart(event.toolCallId, event.toolName);
|
|
101
|
+
});
|
|
102
|
+
pi.on('tool_execution_end', event => {
|
|
103
|
+
ctxByCall.delete(event.toolCallId);
|
|
104
|
+
watchdog.onEnd(event.toolCallId);
|
|
105
|
+
});
|
|
106
|
+
// Safety net: nothing should outlive its turn, but if a start ever lacks a
|
|
107
|
+
// matching end, clear on turn/session teardown so no timer fires stale.
|
|
108
|
+
const reset = () => {
|
|
109
|
+
watchdog.clearAll();
|
|
110
|
+
ctxByCall.clear();
|
|
111
|
+
};
|
|
112
|
+
pi.on('turn_end', reset);
|
|
113
|
+
pi.on('session_shutdown', reset);
|
|
114
|
+
}
|
package/dist/task/final-gate.js
CHANGED
|
@@ -47,6 +47,7 @@ import { readDeclaredScripts, missingDeclaredScripts, runnableDeclaredScripts }
|
|
|
47
47
|
import { readEnvNotes, parseEnvNotes, isExcuseNote } from './env-notes.js';
|
|
48
48
|
import { runRenderCheck } from './render-check.js';
|
|
49
49
|
import { taskThatIntroduced } from './task-provenance.js';
|
|
50
|
+
import { findDanglingArtifacts, danglingGateFailureText } from './artifact-closure.js';
|
|
50
51
|
function packageScripts(cwd) {
|
|
51
52
|
try {
|
|
52
53
|
const j = JSON.parse(readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
@@ -382,6 +383,12 @@ export function runBootCheck(cwd, [bin, args], graceMs = 10_000, opts = {}) {
|
|
|
382
383
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
383
384
|
env: { ...process.env }
|
|
384
385
|
});
|
|
386
|
+
// Best-effort cleanup only: killGroup below can silently fail to reap the
|
|
387
|
+
// process (platform/sandbox-specific — observed on a GH Actions Linux
|
|
388
|
+
// runner where the group-kill did not take, hanging the whole `bun test
|
|
389
|
+
// --isolate` run on the leaked child's piped stdio). unref() so a child
|
|
390
|
+
// we already tried to kill can never itself keep this process alive.
|
|
391
|
+
child.unref();
|
|
385
392
|
let out = '';
|
|
386
393
|
let err = '';
|
|
387
394
|
let listenerSeen = false;
|
|
@@ -772,6 +779,21 @@ export async function runFinalIntegrationGate(cwd, timeoutMs = 900_000, bootGrac
|
|
|
772
779
|
warnings.push(b.renderNote);
|
|
773
780
|
}
|
|
774
781
|
}
|
|
782
|
+
// Artifact-production closure (mx5 run 13, PROMPT 2): a runtime file
|
|
783
|
+
// reference with NO producer anywhere ships silently — the server read
|
|
784
|
+
// `Bun.file('dist/index.html')` while the build emitted only app.css +
|
|
785
|
+
// main.js, so every non-API GET 404'd behind 32/32 green checkoffs.
|
|
786
|
+
// Deterministic scan of the shipped tree (literal refs only, positive
|
|
787
|
+
// producer evidence required — see artifact-closure.ts); each dangle is a
|
|
788
|
+
// ranked failure naming referencer + missing path. Rank 0: "the app cannot
|
|
789
|
+
// serve what it references" is the same load-bearing class as boot/render.
|
|
790
|
+
try {
|
|
791
|
+
for (const d of findDanglingArtifacts(cwd))
|
|
792
|
+
fail(danglingGateFailureText(d), 0);
|
|
793
|
+
}
|
|
794
|
+
catch {
|
|
795
|
+
// best-effort scan — a scanner fault must never break the gate
|
|
796
|
+
}
|
|
775
797
|
if (failures.length > 0) {
|
|
776
798
|
// Stable sort: boot/render (rank 0) leads, everything else keeps execution
|
|
777
799
|
// order. One failure keeps the exact single-failure wording; several become
|
|
@@ -88,8 +88,13 @@ export declare function readRequirements(cwd: string): Promise<string>;
|
|
|
88
88
|
* was warned-about then dropped). These are plain strings, not quotes of the
|
|
89
89
|
* source; marked distinctly so a task can tell an inferred area from a verbatim
|
|
90
90
|
* obligation.
|
|
91
|
+
* • `danglingArtifacts` — runtime files the spec references but nothing
|
|
92
|
+
* produces (mx5 run 13: the served `index.html` no task, tree entry, or
|
|
93
|
+
* build output ever created), still unclaimed by any title at coverage
|
|
94
|
+
* exhaustion. Deterministically extracted (artifact-closure.ts), so like
|
|
95
|
+
* judge areas they are host-authored strings, not source quotes.
|
|
91
96
|
*/
|
|
92
|
-
export declare function appendCarriedRequirements(cwd: string, crossCutting: RequirementEntry[], unresolved?: RequirementEntry[], judgeFlagged?: string[]): Promise<void>;
|
|
97
|
+
export declare function appendCarriedRequirements(cwd: string, crossCutting: RequirementEntry[], unresolved?: RequirementEntry[], judgeFlagged?: string[], danglingArtifacts?: string[]): Promise<void>;
|
|
93
98
|
/**
|
|
94
99
|
* The read-only block refine/compose receive when carried requirements exist.
|
|
95
100
|
* Verbatim content travels with every task (the directive pattern that works),
|
|
@@ -320,10 +320,19 @@ function formatEntry(e, marker) {
|
|
|
320
320
|
* was warned-about then dropped). These are plain strings, not quotes of the
|
|
321
321
|
* source; marked distinctly so a task can tell an inferred area from a verbatim
|
|
322
322
|
* obligation.
|
|
323
|
+
* • `danglingArtifacts` — runtime files the spec references but nothing
|
|
324
|
+
* produces (mx5 run 13: the served `index.html` no task, tree entry, or
|
|
325
|
+
* build output ever created), still unclaimed by any title at coverage
|
|
326
|
+
* exhaustion. Deterministically extracted (artifact-closure.ts), so like
|
|
327
|
+
* judge areas they are host-authored strings, not source quotes.
|
|
323
328
|
*/
|
|
324
|
-
export async function appendCarriedRequirements(cwd, crossCutting, unresolved = [], judgeFlagged = []) {
|
|
325
|
-
if (crossCutting.length === 0
|
|
329
|
+
export async function appendCarriedRequirements(cwd, crossCutting, unresolved = [], judgeFlagged = [], danglingArtifacts = []) {
|
|
330
|
+
if (crossCutting.length === 0
|
|
331
|
+
&& unresolved.length === 0
|
|
332
|
+
&& judgeFlagged.length === 0
|
|
333
|
+
&& danglingArtifacts.length === 0) {
|
|
326
334
|
return;
|
|
335
|
+
}
|
|
327
336
|
try {
|
|
328
337
|
const existing = (await readRequirements(cwd)).split('\n').filter(l => l.trim().length > 0);
|
|
329
338
|
const seen = new Set(existing.map(l => {
|
|
@@ -337,6 +346,10 @@ export async function appendCarriedRequirements(cwd, crossCutting, unresolved =
|
|
|
337
346
|
[
|
|
338
347
|
judgeFlagged.map(q => ({ quote: q, anchor: '' })),
|
|
339
348
|
'judge-flagged uncovered area, no task owns this — surfaced at plan time'
|
|
349
|
+
],
|
|
350
|
+
[
|
|
351
|
+
danglingArtifacts.map(q => ({ quote: q, anchor: '' })),
|
|
352
|
+
'dangling runtime artifact, nothing produces it — surfaced at plan time'
|
|
340
353
|
]
|
|
341
354
|
]) {
|
|
342
355
|
for (const e of entries) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.26",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|