@themoltnet/pi-extension 0.20.2 → 0.22.1

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/index.d.ts CHANGED
@@ -521,7 +521,7 @@ export declare interface PiTaskExecutionPlan {
521
521
  sessionPersistence?: PiSessionPersistencePlan | null;
522
522
  }
523
523
 
524
- export declare type PiTaskExecutionPlanFactory = (claimedTask: ClaimedTask) => PiTaskExecutionPlan | null;
524
+ export declare type PiTaskExecutionPlanFactory = (claimedTask: ClaimedTask) => Promise<PiTaskExecutionPlan | null> | PiTaskExecutionPlan | null;
525
525
 
526
526
  declare interface PiWorkspaceAttachmentPlan {
527
527
  mountPath: string;
@@ -685,6 +685,8 @@ export declare type SubagentToolParameters = Static<typeof SubagentToolParameter
685
685
  declare const Task: TObject< {
686
686
  id: TString;
687
687
  taskType: TString;
688
+ title: TUnion<[TString, TNull]>;
689
+ tags: TArray<TString>;
688
690
  teamId: TString;
689
691
  diaryId: TUnion<[TString, TNull]>;
690
692
  outputKind: TUnion<[TLiteral<"artifact">, TLiteral<"judgment">]>;
package/dist/index.js CHANGED
@@ -11,8 +11,8 @@ import { Type, getModel } from "@earendil-works/pi-ai";
11
11
  import { MemoryProvider, RealFSProvider, ShadowProvider, VM, VmCheckpoint, createHttpHooks, createShadowPathPredicate, ensureImageSelector, loadGuestAssets } from "@earendil-works/gondolin";
12
12
  import { parseEnv } from "node:util";
13
13
  import { SpanStatusCode, context, metrics, trace } from "@opentelemetry/api";
14
- import { Value } from "@sinclair/typebox/value";
15
14
  import { FormatRegistry, Type as Type$1 } from "@sinclair/typebox";
15
+ import { Value } from "@sinclair/typebox/value";
16
16
  //#region \0rolldown/runtime.js
17
17
  var __defProp = Object.defineProperty;
18
18
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -8817,90 +8817,6 @@ async function resolvePersistentSessionManager(args) {
8817
8817
  return SessionManager.continueRecent(args.cwd, args.sessionDir);
8818
8818
  }
8819
8819
  //#endregion
8820
- //#region ../agent-runtime/src/context-bindings.ts
8821
- var PROMPT_SEPARATOR = "\n\n---\n\n";
8822
- /**
8823
- * Resolve `task.input.context[]` into delivered side-effects (skills
8824
- * persisted via `deliver.skill`) and prompt fragments
8825
- * (`systemPromptPrefix`, `userInlineSuffix`) the caller weaves into the
8826
- * built prompt.
8827
- *
8828
- * Per-binding semantics (V1):
8829
- * - `skill` → `deliver.skill({ slug, content })` once per ref.
8830
- * Slug collisions on distinct contents are
8831
- * refused loudly.
8832
- * - `context_inline`→ persist raw bytes via `deliver.contextFile(...)`
8833
- * and inject them into the prompt in an explicit,
8834
- * named block. Intended for eval/context experiments
8835
- * where the content must be in the model context
8836
- * window, not merely discoverable as a skill.
8837
- * - `prompt_prefix` → content appended to `systemPromptPrefix` with
8838
- * the canonical `\n\n---\n\n` separator (in
8839
- * declared order).
8840
- * - `user_inline` → content appended to `userInlineSuffix` in
8841
- * declared order, same separator.
8842
- *
8843
- * No fetching, no hashing — bytes are inlined in `ContextRef.content`,
8844
- * and the task's `inputCid` already pins the entire input. The proposer
8845
- * chose these bytes; the resolver just dispatches them.
8846
- *
8847
- * The function is pure with respect to its arguments: file writes are
8848
- * confined to the injected `deliver` callback, which makes the
8849
- * resolver trivial to test.
8850
- */
8851
- async function resolveTaskContext(args) {
8852
- const promptParts = [];
8853
- const userParts = [];
8854
- const injected = [];
8855
- const usedSlugs = /* @__PURE__ */ new Map();
8856
- for (const ref of args.context) {
8857
- if (ref.binding === "skill") {
8858
- const prior = usedSlugs.get(ref.slug);
8859
- if (prior !== void 0) {
8860
- if (prior !== ref.content) throw new Error(`slug collision on '${ref.slug}': two skill entries share the same slug but have different content`);
8861
- injected.push(ref);
8862
- continue;
8863
- }
8864
- usedSlugs.set(ref.slug, ref.content);
8865
- await args.deliver.skill({
8866
- slug: ref.slug,
8867
- content: ref.content
8868
- });
8869
- } else if (ref.binding === "context_inline") {
8870
- await args.deliver.contextFile({
8871
- slug: ref.slug,
8872
- content: ref.content,
8873
- suggestedFileName: `${ref.slug}.md`
8874
- });
8875
- promptParts.push(formatInlineContextBlock(ref.slug, ref.content));
8876
- } else if (ref.binding === "prompt_prefix") promptParts.push(ref.content);
8877
- else userParts.push(ref.content);
8878
- injected.push(ref);
8879
- }
8880
- return {
8881
- injected,
8882
- systemPromptPrefix: promptParts.join(PROMPT_SEPARATOR),
8883
- userInlineSuffix: userParts.join(PROMPT_SEPARATOR)
8884
- };
8885
- }
8886
- function formatInlineContextBlock(slug, content) {
8887
- return [
8888
- "### Injected Task Context",
8889
- "",
8890
- `Context id: \`${slug}\``,
8891
- "The following raw context was supplied by the task creator. Treat it",
8892
- "as task-relevant background that may override generic coding instincts",
8893
- "when it contains repo- or workflow-specific constraints.",
8894
- "The same content is also materialized in the workspace as",
8895
- "`/workspace/context-pack.md` and mirrored in `AGENTS.md` for",
8896
- "repo-context discovery.",
8897
- "",
8898
- "<context>",
8899
- content,
8900
- "</context>"
8901
- ].join("\n");
8902
- }
8903
- //#endregion
8904
8820
  //#region ../tasks/src/formats.ts
8905
8821
  /**
8906
8822
  * Register TypeBox string formats used across Task / TaskOutput / task-type
@@ -9386,6 +9302,14 @@ var FreeformExecutionOptions = Type$1.Object({ workspace: Type$1.Optional(Type$1
9386
9302
  $id: "FreeformExecutionOptions",
9387
9303
  additionalProperties: false
9388
9304
  });
9305
+ var FreeformContinueFrom = Type$1.Object({
9306
+ taskId: Type$1.String({ format: "uuid" }),
9307
+ attemptN: Type$1.Integer({ minimum: 1 }),
9308
+ mode: Type$1.Optional(Type$1.Union([Type$1.Literal("extend"), Type$1.Literal("fork")]))
9309
+ }, {
9310
+ $id: "FreeformContinueFrom",
9311
+ additionalProperties: false
9312
+ });
9389
9313
  var FreeformTaskTypeProposal = Type$1.Object({
9390
9314
  name: Type$1.String({ minLength: 1 }),
9391
9315
  rationale: Type$1.String({ minLength: 1 }),
@@ -9396,14 +9320,14 @@ var FreeformTaskTypeProposal = Type$1.Object({
9396
9320
  additionalProperties: false
9397
9321
  });
9398
9322
  var FreeformInput = Type$1.Object({
9399
- title: Type$1.Optional(Type$1.String({ minLength: 1 })),
9400
9323
  brief: Type$1.String({ minLength: 1 }),
9401
9324
  expectedOutput: Type$1.Optional(Type$1.String({ minLength: 1 })),
9402
9325
  constraints: Type$1.Optional(Type$1.Array(Type$1.String({ minLength: 1 }), { maxItems: 20 })),
9403
9326
  suggestedTaskType: Type$1.Optional(Type$1.String({ minLength: 1 })),
9404
9327
  successCriteria: Type$1.Optional(SuccessCriteria),
9405
9328
  context: Type$1.Optional(TaskContext),
9406
- execution: Type$1.Optional(FreeformExecutionOptions)
9329
+ execution: Type$1.Optional(FreeformExecutionOptions),
9330
+ continueFrom: Type$1.Optional(FreeformContinueFrom)
9407
9331
  }, {
9408
9332
  $id: "FreeformInput",
9409
9333
  additionalProperties: false
@@ -9429,6 +9353,84 @@ var FreeformOutput = Type$1.Object({
9429
9353
  $id: "FreeformOutput",
9430
9354
  additionalProperties: false
9431
9355
  });
9356
+ /**
9357
+ * Server-side preflight for `freeform` task-create. Runs after the
9358
+ * sync TypeBox check passes and only kicks in when
9359
+ * `input.continueFrom` is set — i.e. the proposer is asking to
9360
+ * resume a prior freeform attempt's warm slot (#1287).
9361
+ *
9362
+ * Failure modes, in evaluation order:
9363
+ * 1. `freeform.sourceTaskNotFound` — source task id does not resolve
9364
+ * (does not exist OR caller can't read it; we don't distinguish).
9365
+ * 2. `freeform.sourceTaskTypeNotSupported` — source isn't `freeform`.
9366
+ * v1 only supports freeform → freeform continuation.
9367
+ * 3. `freeform.sourceAttemptNotCompleted` — named attempt is missing
9368
+ * or not in `completed` state; warm continuation only makes sense
9369
+ * once the parent has produced a terminal output.
9370
+ * 4. `freeform.forkModeNotImplemented` — `mode: 'fork'` is the wire
9371
+ * surface for copy-on-write continuation tracked in #1293; v1
9372
+ * rejects it server-side so daemons never have to branch.
9373
+ * 5. `freeform.executionWorkspaceNotInheritable` — caller set
9374
+ * `execution.workspace` together with `continueFrom`. Workspace
9375
+ * mode for a continuation is inherited from the parent slot
9376
+ * (`maybeAttachWarmSlotContext` forces `dedicated_worktree` +
9377
+ * the parent's worktreeBranch), so any caller-supplied override
9378
+ * is silently dropped at the daemon plan stage. Reject explicitly
9379
+ * so misconfiguration surfaces at create time.
9380
+ * 6. `freeform.sourceNotResumeEligible` — `daemonState` is null or
9381
+ * `slotResumableUntil` is null. Older completions (pre-#1287) and
9382
+ * daemons that opt out fall here.
9383
+ * 7. `freeform.sourceResumeExpired` — `slotResumableUntil` is in the
9384
+ * past; the warm slot's TTL has elapsed and no daemon is
9385
+ * guaranteed to still hold it.
9386
+ *
9387
+ * Returns on the first failure (no "report all six") — the checks
9388
+ * are sequential preconditions, later ones presume earlier ones hold.
9389
+ */
9390
+ async function validateFreeformInputAsync(input, ctx) {
9391
+ const cf = input.continueFrom;
9392
+ if (!cf) return [];
9393
+ const source = await ctx.resolveTask(cf.taskId);
9394
+ if (!source) return [{
9395
+ field: "input/continueFrom/taskId",
9396
+ message: `Source task ${cf.taskId} does not resolve to a task you can read`,
9397
+ code: "freeform.sourceTaskNotFound"
9398
+ }];
9399
+ if (source.taskType !== "freeform") return [{
9400
+ field: "input/continueFrom/taskId",
9401
+ message: `Source task type '${source.taskType}' is not continuable; only freeform → freeform is supported in v1`,
9402
+ code: "freeform.sourceTaskTypeNotSupported"
9403
+ }];
9404
+ if (cf.mode === "fork") return [{
9405
+ field: "input/continueFrom/mode",
9406
+ message: "fork mode not yet implemented; see https://github.com/getlarge/themoltnet/issues/1293",
9407
+ code: "freeform.forkModeNotImplemented"
9408
+ }];
9409
+ if (input.execution?.workspace) return [{
9410
+ field: "input/execution/workspace",
9411
+ message: "execution.workspace is inherited from the parent slot when continueFrom is set; omit it",
9412
+ code: "freeform.executionWorkspaceNotInheritable"
9413
+ }];
9414
+ if (ctx.deferReadinessChecks) return [];
9415
+ const attempt = (await ctx.listAttempts(cf.taskId)).find((a) => a.attemptN === cf.attemptN);
9416
+ if (!attempt || attempt.status !== "completed") return [{
9417
+ field: "input/continueFrom/attemptN",
9418
+ message: `Source attempt ${cf.attemptN} on task ${cf.taskId} is not in 'completed' state`,
9419
+ code: "freeform.sourceAttemptNotCompleted"
9420
+ }];
9421
+ if (!attempt.daemonState || attempt.daemonState.slotResumableUntil === null) return [{
9422
+ field: "input/continueFrom",
9423
+ message: "Source attempt did not report continuation eligibility (older completion or daemon opted out)",
9424
+ code: "freeform.sourceNotResumeEligible"
9425
+ }];
9426
+ const expiresAt = new Date(attempt.daemonState.slotResumableUntil).getTime();
9427
+ if (Number.isNaN(expiresAt) || expiresAt <= Date.now()) return [{
9428
+ field: "input/continueFrom",
9429
+ message: `Source attempt's warm slot expired at ${attempt.daemonState.slotResumableUntil} (reported at ${attempt.daemonState.reportedAt})`,
9430
+ code: "freeform.sourceResumeExpired"
9431
+ }];
9432
+ return [];
9433
+ }
9432
9434
  //#endregion
9433
9435
  //#region ../tasks/src/task-types/fulfill-brief.ts
9434
9436
  /**
@@ -9441,7 +9443,6 @@ var FreeformOutput = Type$1.Object({
9441
9443
  var FULFILL_BRIEF_TYPE = "fulfill_brief";
9442
9444
  var FulfillBriefInput = Type$1.Object({
9443
9445
  brief: Type$1.String({ minLength: 1 }),
9444
- title: Type$1.Optional(Type$1.String()),
9445
9446
  successCriteria: Type$1.Optional(SuccessCriteria),
9446
9447
  seedFiles: Type$1.Optional(Type$1.Array(Type$1.String())),
9447
9448
  scopeHint: Type$1.Optional(Type$1.String())
@@ -9999,7 +10000,8 @@ var BUILT_IN_TASK_TYPES = {
9999
10000
  sessionScope: "correlation",
10000
10001
  acceptsInputWorkspaceOverride: true,
10001
10002
  requiresReferences: false,
10002
- validateOutput: requireVerificationWhenCriteriaPresent
10003
+ validateOutput: requireVerificationWhenCriteriaPresent,
10004
+ validateInputAsync: validateFreeformInputAsync
10003
10005
  },
10004
10006
  [FULFILL_BRIEF_TYPE]: {
10005
10007
  name: FULFILL_BRIEF_TYPE,
@@ -10224,6 +10226,25 @@ var Cid = Type$1.String({ minLength: 1 });
10224
10226
  var IsoTimestamp = Type$1.String({ format: "date-time" });
10225
10227
  var MAX_CLAIM_CONDITION_BRANCHES = 8;
10226
10228
  var MAX_CLAIM_CONDITION_STATUSES = 8;
10229
+ /**
10230
+ * Daemon-asserted runtime state stamped onto a `TaskAttemptSummary` at
10231
+ * attempt-completion time. The server persists this block verbatim and
10232
+ * reads `slotResumableUntil` for `tasks_continue` create-time
10233
+ * eligibility; the daemon-side claim-affinity filter is the runtime
10234
+ * truth. The block carries its own `reportedAt` so consumers can reason
10235
+ * about staleness without reading documentation. All daemon-asserted
10236
+ * state lives here — top-level attempt fields stay server-authoritative.
10237
+ *
10238
+ * Adding new fields requires explicit design review (intentional
10239
+ * boundary; see docs/superpowers/specs/2026-06-04-tasks-continue-design.md).
10240
+ */
10241
+ var DaemonState = Type$1.Object({
10242
+ reportedAt: IsoTimestamp,
10243
+ slotResumableUntil: Type$1.Union([IsoTimestamp, Type$1.Null()])
10244
+ }, {
10245
+ $id: "DaemonState",
10246
+ additionalProperties: false
10247
+ });
10227
10248
  var ClaimCondition = Type$1.Recursive((Self) => Type$1.Union([
10228
10249
  Type$1.Object({
10229
10250
  op: Type$1.Literal("all"),
@@ -10320,6 +10341,8 @@ Type$1.Object({
10320
10341
  Type$1.Object({
10321
10342
  id: Uuid,
10322
10343
  taskType: Type$1.String({ minLength: 1 }),
10344
+ title: Type$1.Union([Type$1.String(), Type$1.Null()]),
10345
+ tags: Type$1.Array(Type$1.String()),
10323
10346
  teamId: Uuid,
10324
10347
  diaryId: Type$1.Union([Uuid, Type$1.Null()]),
10325
10348
  outputKind: OutputKind,
@@ -10372,7 +10395,8 @@ Type$1.Object({
10372
10395
  error: Type$1.Union([TaskError, Type$1.Null()]),
10373
10396
  usage: Type$1.Union([TaskUsage, Type$1.Null()]),
10374
10397
  contentSignature: Type$1.Union([Type$1.String(), Type$1.Null()]),
10375
- signedAt: Type$1.Union([IsoTimestamp, Type$1.Null()])
10398
+ signedAt: Type$1.Union([IsoTimestamp, Type$1.Null()]),
10399
+ daemonState: Type$1.Union([DaemonState, Type$1.Null()])
10376
10400
  }, {
10377
10401
  $id: "TaskAttempt",
10378
10402
  additionalProperties: false
@@ -10425,6 +10449,90 @@ Type$1.Object({
10425
10449
  additionalProperties: false
10426
10450
  });
10427
10451
  //#endregion
10452
+ //#region ../agent-runtime/src/context-bindings.ts
10453
+ var PROMPT_SEPARATOR = "\n\n---\n\n";
10454
+ /**
10455
+ * Resolve `task.input.context[]` into delivered side-effects (skills
10456
+ * persisted via `deliver.skill`) and prompt fragments
10457
+ * (`systemPromptPrefix`, `userInlineSuffix`) the caller weaves into the
10458
+ * built prompt.
10459
+ *
10460
+ * Per-binding semantics (V1):
10461
+ * - `skill` → `deliver.skill({ slug, content })` once per ref.
10462
+ * Slug collisions on distinct contents are
10463
+ * refused loudly.
10464
+ * - `context_inline`→ persist raw bytes via `deliver.contextFile(...)`
10465
+ * and inject them into the prompt in an explicit,
10466
+ * named block. Intended for eval/context experiments
10467
+ * where the content must be in the model context
10468
+ * window, not merely discoverable as a skill.
10469
+ * - `prompt_prefix` → content appended to `systemPromptPrefix` with
10470
+ * the canonical `\n\n---\n\n` separator (in
10471
+ * declared order).
10472
+ * - `user_inline` → content appended to `userInlineSuffix` in
10473
+ * declared order, same separator.
10474
+ *
10475
+ * No fetching, no hashing — bytes are inlined in `ContextRef.content`,
10476
+ * and the task's `inputCid` already pins the entire input. The proposer
10477
+ * chose these bytes; the resolver just dispatches them.
10478
+ *
10479
+ * The function is pure with respect to its arguments: file writes are
10480
+ * confined to the injected `deliver` callback, which makes the
10481
+ * resolver trivial to test.
10482
+ */
10483
+ async function resolveTaskContext(args) {
10484
+ const promptParts = [];
10485
+ const userParts = [];
10486
+ const injected = [];
10487
+ const usedSlugs = /* @__PURE__ */ new Map();
10488
+ for (const ref of args.context) {
10489
+ if (ref.binding === "skill") {
10490
+ const prior = usedSlugs.get(ref.slug);
10491
+ if (prior !== void 0) {
10492
+ if (prior !== ref.content) throw new Error(`slug collision on '${ref.slug}': two skill entries share the same slug but have different content`);
10493
+ injected.push(ref);
10494
+ continue;
10495
+ }
10496
+ usedSlugs.set(ref.slug, ref.content);
10497
+ await args.deliver.skill({
10498
+ slug: ref.slug,
10499
+ content: ref.content
10500
+ });
10501
+ } else if (ref.binding === "context_inline") {
10502
+ await args.deliver.contextFile({
10503
+ slug: ref.slug,
10504
+ content: ref.content,
10505
+ suggestedFileName: `${ref.slug}.md`
10506
+ });
10507
+ promptParts.push(formatInlineContextBlock(ref.slug, ref.content));
10508
+ } else if (ref.binding === "prompt_prefix") promptParts.push(ref.content);
10509
+ else userParts.push(ref.content);
10510
+ injected.push(ref);
10511
+ }
10512
+ return {
10513
+ injected,
10514
+ systemPromptPrefix: promptParts.join(PROMPT_SEPARATOR),
10515
+ userInlineSuffix: userParts.join(PROMPT_SEPARATOR)
10516
+ };
10517
+ }
10518
+ function formatInlineContextBlock(slug, content) {
10519
+ return [
10520
+ "### Injected Task Context",
10521
+ "",
10522
+ `Context id: \`${slug}\``,
10523
+ "The following raw context was supplied by the task creator. Treat it",
10524
+ "as task-relevant background that may override generic coding instincts",
10525
+ "when it contains repo- or workflow-specific constraints.",
10526
+ "The same content is also materialized in the workspace as",
10527
+ "`/workspace/context-pack.md` and mirrored in `AGENTS.md` for",
10528
+ "repo-context discovery.",
10529
+ "",
10530
+ "<context>",
10531
+ content,
10532
+ "</context>"
10533
+ ].join("\n");
10534
+ }
10535
+ //#endregion
10428
10536
  //#region ../agent-runtime/src/output-tools.ts
10429
10537
  /**
10430
10538
  * Submit-output tool contract.
@@ -10935,6 +11043,23 @@ function buildCuratePackUserPrompt(input, ctx) {
10935
11043
  }
10936
11044
  //#endregion
10937
11045
  //#region ../agent-runtime/src/prompts/freeform.ts
11046
+ var INLINE_ARTIFACT_THRESHOLD = 16 * 1024;
11047
+ var TOTAL_INLINE_BUDGET = 32 * 1024;
11048
+ function buildPriorContextSection(priorContext) {
11049
+ if (!priorContext) return null;
11050
+ const lines = ["# Prior context", ""];
11051
+ if (priorContext.summary) lines.push("## Summary", priorContext.summary, "");
11052
+ let inlineUsed = priorContext.summary?.length ?? 0;
11053
+ priorContext.artifacts?.forEach((art, i) => {
11054
+ const bodyLen = art.body?.length ?? 0;
11055
+ if (art.body && bodyLen < INLINE_ARTIFACT_THRESHOLD && inlineUsed + bodyLen < TOTAL_INLINE_BUDGET) {
11056
+ lines.push(`## Artifact ${i}: ${art.title} (${art.kind})`, art.body, "");
11057
+ inlineUsed += bodyLen;
11058
+ } else lines.push(`## Artifact ${i} (pointer): kind: ${art.kind}, title: ${art.title}`, "(body omitted; use tasks_get to retrieve full content)", "");
11059
+ });
11060
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
11061
+ return lines.join("\n");
11062
+ }
10938
11063
  function buildFreeformUserPrompt(input, ctx) {
10939
11064
  const header = [
10940
11065
  "# Freeform Task Agent",
@@ -10956,18 +11081,12 @@ function buildFreeformUserPrompt(input, ctx) {
10956
11081
  "4. If the request reveals a recurring task shape, include a",
10957
11082
  " `proposedTaskType` in the final output with a concise rationale."
10958
11083
  ].join("\n");
10959
- return assembleTaskPrompt("freeform", [
11084
+ const sections = [
10960
11085
  {
10961
11086
  id: "freeform.header",
10962
11087
  source: "header",
10963
11088
  body: header
10964
11089
  },
10965
- {
10966
- id: "freeform.title",
10967
- source: "task_input",
10968
- header: "Title",
10969
- body: input.title ?? ""
10970
- },
10971
11090
  {
10972
11091
  id: "freeform.brief",
10973
11092
  source: "task_input",
@@ -11020,7 +11139,14 @@ function buildFreeformUserPrompt(input, ctx) {
11020
11139
  ].join("\n")
11021
11140
  })
11022
11141
  }
11023
- ]);
11142
+ ];
11143
+ const priorContextBody = buildPriorContextSection(ctx.priorContext);
11144
+ if (priorContextBody) sections.splice(sections.findIndex((s) => s.id === "freeform.workflow") + 1, 0, {
11145
+ id: "freeform.prior_context",
11146
+ source: "task_input",
11147
+ body: priorContextBody
11148
+ });
11149
+ return assembleTaskPrompt("freeform", sections);
11024
11150
  }
11025
11151
  //#endregion
11026
11152
  //#region ../agent-runtime/src/prompts/fulfill-brief.ts
@@ -11032,7 +11158,7 @@ function buildFreeformUserPrompt(input, ctx) {
11032
11158
  * is told to inspect them itself.
11033
11159
  */
11034
11160
  function buildFulfillBriefUserPrompt(input, ctx) {
11035
- const { brief, title, seedFiles, scopeHint } = input;
11161
+ const { brief, seedFiles, scopeHint } = input;
11036
11162
  const header = [
11037
11163
  "# Fulfill Brief Agent",
11038
11164
  "",
@@ -11042,7 +11168,7 @@ function buildFulfillBriefUserPrompt(input, ctx) {
11042
11168
  "invariants for this task: identity, gh authentication, diary discipline,",
11043
11169
  "and the accountable-commit shape. Follow it for every commit.",
11044
11170
  "",
11045
- `## Task: ${title ?? "Fulfill brief"}`,
11171
+ "## Task: Fulfill brief",
11046
11172
  "",
11047
11173
  `Task id: \`${ctx.taskId}\``
11048
11174
  ].join("\n");
@@ -11781,7 +11907,10 @@ function buildTaskUserPrompt(task, ctx) {
11781
11907
  const errors = [...Value.Errors(FreeformInput, task.input)];
11782
11908
  throw new Error(`freeform input failed validation: ${JSON.stringify(errors.slice(0, 3))}`);
11783
11909
  }
11784
- return buildFreeformUserPrompt(task.input, { taskId: ctx.taskId });
11910
+ return buildFreeformUserPrompt(task.input, {
11911
+ taskId: ctx.taskId,
11912
+ priorContext: ctx.priorContext
11913
+ });
11785
11914
  case FULFILL_BRIEF_TYPE:
11786
11915
  if (!Value.Check(FulfillBriefInput, task.input)) {
11787
11916
  const errors = [...Value.Errors(FulfillBriefInput, task.input)];
@@ -15474,6 +15603,30 @@ function clip(s, max) {
15474
15603
  return s.length > max ? s.slice(0, max) : s;
15475
15604
  }
15476
15605
  //#endregion
15606
+ //#region src/runtime/resolve-prior-context.ts
15607
+ /**
15608
+ * Fetch the named attempt's output and project it into the prompt's
15609
+ * priorContext shape. Returns `null` when the attempt cannot be located
15610
+ * or its output is missing/malformed — the prompt builder treats that
15611
+ * as "no prior context", which is harmless.
15612
+ */
15613
+ async function resolvePriorContext(agent, continueFrom) {
15614
+ const attempt = (await agent.tasks.listAttempts(continueFrom.taskId)).find((a) => a.attemptN === continueFrom.attemptN);
15615
+ if (!attempt || !attempt.output) return null;
15616
+ const output = attempt.output;
15617
+ const summary = typeof output.summary === "string" ? output.summary : void 0;
15618
+ const artifacts = Array.isArray(output.artifacts) ? output.artifacts.filter((a) => !!a && typeof a.kind === "string" && typeof a.title === "string").map((a) => ({
15619
+ kind: a.kind,
15620
+ title: a.title,
15621
+ body: a.body
15622
+ })) : void 0;
15623
+ if (!summary && (!artifacts || artifacts.length === 0)) return null;
15624
+ return {
15625
+ summary,
15626
+ artifacts
15627
+ };
15628
+ }
15629
+ //#endregion
15477
15630
  //#region src/runtime/runtime-instructor.ts
15478
15631
  /**
15479
15632
  * Build the daemon-controlled invariant prose injected into the system prompt
@@ -16249,7 +16402,7 @@ async function executePiTask(claimedTask, reporter, opts) {
16249
16402
  const attemptN = claimedTask.attemptN;
16250
16403
  const startTime = Date.now();
16251
16404
  const requestedMountPath = opts.mountPath ?? process.cwd();
16252
- const executionPlan = opts.makeExecutionPlan?.(claimedTask) ?? null;
16405
+ const executionPlan = await opts.makeExecutionPlan?.(claimedTask) ?? null;
16253
16406
  let workspace = null;
16254
16407
  let mountPath = requestedMountPath;
16255
16408
  let cwdPath = requestedMountPath;
@@ -16387,6 +16540,34 @@ async function executePiTask(claimedTask, reporter, opts) {
16387
16540
  workspaceMode: activeWorkspace.mode,
16388
16541
  workspaceBranch: activeWorkspace.branch
16389
16542
  });
16543
+ let resolvedPriorContext;
16544
+ const continueFrom = task.input?.continueFrom;
16545
+ if (task.taskType === "freeform" && continueFrom) try {
16546
+ const resolved = await resolvePriorContext(await connect({ configDir: managed.agentDir }), continueFrom);
16547
+ if (resolved) {
16548
+ resolvedPriorContext = resolved;
16549
+ await emit("info", {
16550
+ event: "prior_context_resolved",
16551
+ sourceTaskId: continueFrom.taskId,
16552
+ sourceAttemptN: continueFrom.attemptN,
16553
+ hasSummary: !!resolved.summary,
16554
+ artifactCount: resolved.artifacts?.length ?? 0
16555
+ });
16556
+ } else await emit("info", {
16557
+ event: "prior_context_empty",
16558
+ sourceTaskId: continueFrom.taskId,
16559
+ sourceAttemptN: continueFrom.attemptN
16560
+ });
16561
+ } catch (err) {
16562
+ const message = err instanceof Error ? err.message : String(err);
16563
+ await emit("info", {
16564
+ event: "prior_context_resolve_failed",
16565
+ severity: "warn",
16566
+ sourceTaskId: continueFrom.taskId,
16567
+ sourceAttemptN: continueFrom.attemptN,
16568
+ message
16569
+ });
16570
+ }
16390
16571
  let taskPrompt;
16391
16572
  try {
16392
16573
  const assembled = buildTaskUserPrompt(task, {
@@ -16398,7 +16579,8 @@ async function executePiTask(claimedTask, reporter, opts) {
16398
16579
  attached: executionPlan?.workspaceAttachment !== void 0 || executionPlan?.workspaceSeed?.source === "producer",
16399
16580
  source: executionPlan?.workspaceSeed?.source === "producer" ? "producer_copy" : executionPlan?.workspaceAttachment !== void 0 ? "producer_attachment" : void 0
16400
16581
  },
16401
- extras: opts.promptExtras
16582
+ extras: opts.promptExtras,
16583
+ priorContext: resolvedPriorContext
16402
16584
  });
16403
16585
  taskPrompt = assembled.text;
16404
16586
  await emit("info", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themoltnet/pi-extension",
3
- "version": "0.20.2",
3
+ "version": "0.22.1",
4
4
  "type": "module",
5
5
  "description": "MoltNet pi extension — sandboxed tool execution in Gondolin VMs with MoltNet identity and persistent memory",
6
6
  "license": "MIT",
@@ -31,8 +31,8 @@
31
31
  "@earendil-works/gondolin": "^0.9.1",
32
32
  "@opentelemetry/api": "^1.9.0",
33
33
  "@sinclair/typebox": "^0.34.0",
34
- "@themoltnet/agent-runtime": "0.20.0",
35
- "@themoltnet/sdk": "0.106.0"
34
+ "@themoltnet/sdk": "0.106.0",
35
+ "@themoltnet/agent-runtime": "0.22.1"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@earendil-works/pi-coding-agent": ">=0.74.0",
@@ -56,7 +56,8 @@
56
56
  "vite": "^8.0.0",
57
57
  "vite-plugin-dts": "^4.5.4",
58
58
  "vitest": "^3.0.0",
59
- "@moltnet/crypto-service": "0.1.0"
59
+ "@moltnet/crypto-service": "0.1.0",
60
+ "@moltnet/tasks": "0.1.0"
60
61
  },
61
62
  "engines": {
62
63
  "node": ">=22"