pi-daddy 0.26.1 → 0.27.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.
Files changed (104) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/PRODUCT-GUIDE.md +125 -0
  3. package/README.md +16 -1
  4. package/REQUIREMENTS.md +41 -0
  5. package/dist/cli.d.ts +2 -2
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +35 -7
  8. package/dist/cli.js.map +1 -1
  9. package/dist/daily-dashboard-host.d.ts +14 -1
  10. package/dist/daily-dashboard-host.d.ts.map +1 -1
  11. package/dist/daily-dashboard-host.js +40 -17
  12. package/dist/daily-dashboard-host.js.map +1 -1
  13. package/dist/daily-panel.d.ts +36 -0
  14. package/dist/daily-panel.d.ts.map +1 -0
  15. package/dist/daily-panel.js +84 -0
  16. package/dist/daily-panel.js.map +1 -0
  17. package/dist/daily-view-render.d.ts +2 -1
  18. package/dist/daily-view-render.d.ts.map +1 -1
  19. package/dist/daily-view-render.js +2 -1
  20. package/dist/daily-view-render.js.map +1 -1
  21. package/dist/daily-view.d.ts +3 -0
  22. package/dist/daily-view.d.ts.map +1 -1
  23. package/dist/daily-view.js +6 -0
  24. package/dist/daily-view.js.map +1 -1
  25. package/dist/dashboard-cli.d.ts +2 -0
  26. package/dist/dashboard-cli.d.ts.map +1 -1
  27. package/dist/dashboard-cli.js +37 -15
  28. package/dist/dashboard-cli.js.map +1 -1
  29. package/dist/dashboard-host-contract.d.ts +2 -0
  30. package/dist/dashboard-host-contract.d.ts.map +1 -1
  31. package/dist/dashboard-host-contract.js.map +1 -1
  32. package/dist/dashboard-host-transport.d.ts.map +1 -1
  33. package/dist/dashboard-host-transport.js +6 -3
  34. package/dist/dashboard-host-transport.js.map +1 -1
  35. package/dist/dashboard-host.d.ts +30 -2
  36. package/dist/dashboard-host.d.ts.map +1 -1
  37. package/dist/dashboard-host.js +73 -11
  38. package/dist/dashboard-host.js.map +1 -1
  39. package/dist/dashboard-menu.d.ts +16 -0
  40. package/dist/dashboard-menu.d.ts.map +1 -0
  41. package/dist/dashboard-menu.js +33 -0
  42. package/dist/dashboard-menu.js.map +1 -0
  43. package/dist/index.d.ts +7 -1
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +7 -1
  46. package/dist/index.js.map +1 -1
  47. package/dist/learning-connection.d.ts +127 -0
  48. package/dist/learning-connection.d.ts.map +1 -0
  49. package/dist/learning-connection.js +54 -0
  50. package/dist/learning-connection.js.map +1 -0
  51. package/dist/model-preflight.d.ts +5 -0
  52. package/dist/model-preflight.d.ts.map +1 -1
  53. package/dist/model-preflight.js +9 -0
  54. package/dist/model-preflight.js.map +1 -1
  55. package/dist/product-files.d.ts +6 -0
  56. package/dist/product-files.d.ts.map +1 -0
  57. package/dist/product-files.js +55 -0
  58. package/dist/product-files.js.map +1 -0
  59. package/dist/work-command.d.ts.map +1 -1
  60. package/dist/work-command.js +14 -6
  61. package/dist/work-command.js.map +1 -1
  62. package/dist/work-policy-registry.d.ts +133 -0
  63. package/dist/work-policy-registry.d.ts.map +1 -0
  64. package/dist/work-policy-registry.js +128 -0
  65. package/dist/work-policy-registry.js.map +1 -0
  66. package/dist/work-run.d.ts +56 -0
  67. package/dist/work-run.d.ts.map +1 -0
  68. package/dist/work-run.js +102 -0
  69. package/dist/work-run.js.map +1 -0
  70. package/dist/work-setup.d.ts +40 -0
  71. package/dist/work-setup.d.ts.map +1 -0
  72. package/dist/work-setup.js +174 -0
  73. package/dist/work-setup.js.map +1 -0
  74. package/extensions/daily-dashboard-session.ts +16 -7
  75. package/extensions/execute-child.ts +2 -2
  76. package/extensions/grants-command.ts +2 -2
  77. package/extensions/grants.ts +6 -2
  78. package/extensions/learning-outcome-session.ts +38 -0
  79. package/extensions/learning-session.ts +59 -0
  80. package/extensions/ordinary-runtime.ts +2 -2
  81. package/extensions/run-delegation.ts +2 -2
  82. package/extensions/work-policy-session.ts +139 -0
  83. package/extensions/work-run-session.ts +75 -0
  84. package/extensions/work-runtime.ts +2 -2
  85. package/extensions/work-session.ts +132 -0
  86. package/package.json +9 -1
  87. package/src/cli.ts +26 -7
  88. package/src/daily-dashboard-host.ts +32 -15
  89. package/src/daily-panel.ts +80 -0
  90. package/src/daily-view-render.ts +2 -1
  91. package/src/daily-view.ts +8 -0
  92. package/src/dashboard-cli.ts +29 -17
  93. package/src/dashboard-host-contract.ts +2 -0
  94. package/src/dashboard-host-transport.ts +7 -4
  95. package/src/dashboard-host.ts +48 -13
  96. package/src/dashboard-menu.ts +28 -0
  97. package/src/index.ts +7 -1
  98. package/src/learning-connection.ts +70 -0
  99. package/src/model-preflight.ts +9 -0
  100. package/src/product-files.ts +34 -0
  101. package/src/work-command.ts +11 -6
  102. package/src/work-policy-registry.ts +81 -0
  103. package/src/work-run.ts +79 -0
  104. package/src/work-setup.ts +141 -0
@@ -5,9 +5,9 @@ import { releaseDelegationWorkspace,type PreparedWorkspace } from "./workspace-r
5
5
  import type { GrantsSession } from "./session.ts";
6
6
  import type { DelegationOutcome } from "./execute-child.ts";
7
7
  /** One wrapper around the original executor. It neither consumes nor replaces the caller's promise. */
8
- export async function withOrdinaryChild(input:{session:GrantsSession;executionId:string;parentExecutionId:string|null;childId:string;toolCallId?:string;signal?:AbortSignal;preparedWorkspace?:PreparedWorkspace},operation:(signal?:AbortSignal)=>Promise<DelegationOutcome>){
8
+ export async function withOrdinaryChild(input:{session:GrantsSession;executionId:string;parentExecutionId:string|null;childId:string;toolCallId?:string;signal?:AbortSignal;preparedWorkspace?:PreparedWorkspace;declaredWork?:GrantsSession["declaredWork"]},operation:(signal?:AbortSignal)=>Promise<DelegationOutcome>){
9
9
  let retained:ReturnType<typeof retainSessionChild>;
10
- try{retained=retainSessionChild(input.session,{executionId:input.executionId,parentExecutionId:input.parentExecutionId,toolCallId:input.toolCallId??null},input.signal);}
10
+ try{if(input.declaredWork&&dataDigest(input.declaredWork.selectedSnapshot)!==dataDigest(input.session.declaredWork?.selectedSnapshot??null))throw Error("declared work changed before original child admission");retained=retainSessionChild(input.session,{executionId:input.executionId,parentExecutionId:input.parentExecutionId,toolCallId:input.toolCallId??null},input.signal);}
11
11
  catch(error){return runWithFinalizers(async()=>{throw error;},[{label:"ordinary admission workspace release",run:async()=>{await releaseDelegationWorkspace({prepared:input.preparedWorkspace,childId:input.childId,executionId:input.executionId,parentExecutionId:input.parentExecutionId,ledgerPath:input.session.ledgerPath,reason:"refused"});}}]);}
12
12
  try{const outcome=await operation(retained?.signal??input.signal);retained?.settle({ok:outcome.ok,exitCode:outcome.exitCode,aborted:outcome.aborted??false,outputDigest:dataDigest(outcome.text)},outcome.control??"not-assessed",outcome.control==="failed"?"not-assessed":outcome.control??"not-assessed");return outcome;}
13
13
  catch(error){retained?.settle({failure:"original executor threw; inspect original caller"},"unknown");throw error;}
@@ -204,7 +204,7 @@ export async function runOneDelegation(
204
204
  */
205
205
  options: {
206
206
  /** Actual public execute argument, never read from model-authored correlation or output. */
207
- toolCallId?: string;
207
+ toolCallId?: string; declaredWork?: GrantsSession["declaredWork"];
208
208
  /** Progress for the parent's status block (ADR-0032). Display only. */
209
209
  onProgress?: (update: {
210
210
  /** Appended (process executor: a genuine byte stream). */
@@ -389,7 +389,7 @@ export async function runOneDelegation(
389
389
  childId: ids.childId,
390
390
  executionId: ids.executionId,
391
391
  parentExecutionId: ids.parentExecutionId,
392
- toolCallId: options.toolCallId,
392
+ toolCallId: options.toolCallId, declaredWork: options.declaredWork,
393
393
  cwd: ctx.cwd,
394
394
  preparedWorkspace,
395
395
  signal,
@@ -0,0 +1,139 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { mkdir, readdir } from "node:fs/promises";
5
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
6
+ import { supportedModelEfforts as getSupportedThinkingLevels } from "../src/model-preflight.ts";
7
+ import { createWorkPolicyRegistry, openWorkPolicyRegistry, policyForSetup, workPolicy, workPolicyDigest, workPolicyActivationDigest, type WorkPolicy, type WorkPolicyRegistry, type WorkPolicyActivation } from "../src/work-policy-registry.ts";
8
+ import { loadWorkSetup, type WorkSetup } from "../src/work-setup.ts";
9
+ import { bindLearningAdoption, learningScopeDigest, type LearningHarness, type LearningWorkspace } from "../src/learning-connection.ts";
10
+ import type { DeclaredWorkState } from "../src/work-command.ts";
11
+ import { privateDirectory, readProductJson, writeProductJson } from "../src/product-files.ts";
12
+ import type { AdoptionBinding, AdoptionFacts } from "../src/vendor/adoption.ts";
13
+ import type { FactoryAuthority } from "../src/factory-contract.ts";
14
+ import { experimentHash } from "../src/experiment-contract.ts";
15
+
16
+ const descriptor = (cwd: string) => join(cwd, ".pi", "work-policy-registry.json");
17
+ const policyPath = (cwd: string, digest: string) => join(cwd, ".pi", "work-policies", `${digest}.json`);
18
+ const registryHistoryPath=(cwd:string,scope:string)=>join(cwd,".pi","work-registry-bindings",`${scope}.json`);
19
+ async function retainRegistryBinding(cwd:string,binding:WorkPolicyRegistry){const path=registryHistoryPath(cwd,binding.initial.scopeDigest),old=await readProductJson(path);if(old&&experimentHash(old)!==experimentHash(binding))throw Error("scoped registry binding history conflict");if(!old)await writeProductJson(path,binding);}
20
+ function authority(id: string, binding?: AdoptionBinding, facts?: AdoptionFacts): FactoryAuthority { return { id, orderDigests: [], decisionDigests: [], activationDigests: [], migrationDigests: [], adoption: { id, adoptions: binding ? [binding.id] : [], rollbacks: [] }, facts: binding && facts ? [{ bindingId: binding.id, facts }] : [] }; }
21
+ async function independentFacts(ctx: ExtensionCommandContext, b: AdoptionBinding): Promise<AdoptionFacts | null> {
22
+ if (!await ctx.ui.confirm("Independent eligibility attestation — not a quality preference", `Have you independently verified that this exact comparison/candidate meets the scoped assessment policy, and is still eligible?\nExperiment: ${b.experimentDigest}\nCandidate: ${b.candidateDigest}\nScope: ${b.scopeDigest}\nAssessment: ${b.assessmentPolicyDigest}\n\nConfirm only with real independent evidence. Missing/uncertain evidence means No. This does not fabricate calibration or accept any result.`)) return null;
23
+ return { experimentDigest: b.experimentDigest, candidateDigest: b.candidateDigest, scopeDigest: b.scopeDigest, assessmentPolicyDigest: b.assessmentPolicyDigest, eligible: true };
24
+ }
25
+ export async function loadWorkPolicyRegistry(cwd: string, state: DeclaredWorkState): Promise<WorkPolicyRegistry | null> {
26
+ const binding = await readProductJson(descriptor(cwd)) as WorkPolicyRegistry | null;
27
+ if (binding && binding.initial.scopeDigest !== learningScopeDigest(state)) throw Error("Configured adoption registry belongs to different work. Use /grants learning → Adoption / rollback to reconnect or create scoped settings; no adopted setting was carried forward.");
28
+ if (binding) await openWorkPolicyRegistry(binding).inspect(); return binding;
29
+ }
30
+ async function savePolicy(cwd: string, name: string, policy: WorkPolicy) {
31
+ if(typeof name!=="string"||!name.trim()||Buffer.byteLength(name)>128||/[\p{Cc}\p{Cf}]/u.test(name))throw Error("bounded plain candidate name required");
32
+ const path = policyPath(cwd, workPolicyDigest(policy)), old = await readProductJson(path);
33
+ if(old){await readPolicy(cwd,workPolicyDigest(policy));return;}
34
+ await writeProductJson(path, { version: "named-work-policy-v1", name, policy });
35
+ }
36
+ async function readPolicy(cwd: string, digest: string) {
37
+ const saved = await readProductJson(policyPath(cwd, digest)) as { version: string; name: string; policy: WorkPolicy } | null;
38
+ if (!saved || Object.keys(saved).sort().join()!=="name,policy,version" || typeof saved.name!=="string" || !saved.name.trim() || Buffer.byteLength(saved.name)>128 || /[\p{Cc}\p{Cf}]/u.test(saved.name) || saved.version !== "named-work-policy-v1" || workPolicyDigest(saved.policy) !== digest) throw Error("Exact candidate policy bytes are missing; prepare the candidate profile first");
39
+ return workPolicy(saved.policy);
40
+ }
41
+ function retain(h: LearningHarness, root: string, parser: string, value: unknown) {
42
+ return h.retainArchiveSource(root, { sourceId: `${parser}-${experimentHash(value).slice(0, 24)}`, parser: { id: parser, version: "1" }, retention: "exact", bytes: Buffer.from(JSON.stringify(value)) }).manifestId;
43
+ }
44
+ export async function workPolicyMenu(ctx: ExtensionCommandContext, state: DeclaredWorkState, workspace: LearningWorkspace, harness: LearningHarness): Promise<void> {
45
+ const setup = await loadWorkSetup(state); if (!setup) throw Error("Declare a multi-task work setup before configuring ordinary model/effort adoption");
46
+ let binding = await readProductJson(descriptor(ctx.cwd)) as WorkPolicyRegistry|null;
47
+ if(binding)await openWorkPolicyRegistry(binding).inspect();
48
+ if(binding&&binding.initial.scopeDigest!==learningScopeDigest(state)){
49
+ if(!await ctx.ui.confirm("Settings belong to different selected work","Connect settings for this exact work scope? Previous registry and active/history-bearing orders remain untouched. No adopted setting carries across scopes."))return;
50
+ await retainRegistryBinding(ctx.cwd,binding);binding=null;
51
+ }
52
+ if(!binding){
53
+ const saved=await readProductJson(registryHistoryPath(ctx.cwd,learningScopeDigest(state))) as WorkPolicyRegistry|null;
54
+ if(saved){const view=await openWorkPolicyRegistry(saved).inspect();if(view.scopeDigest!==learningScopeDigest(state))throw Error("saved registry scope mismatch");if(!await ctx.ui.confirm("Reconnect saved settings for this work?",`${view.candidate.profiles.map(p=>`${p.model} / ${p.thinking}`).join(", ")}\nFuture runs still require current eligibility; existing runs do not migrate.`))return;binding=saved;await writeProductJson(descriptor(ctx.cwd),binding,true);}
55
+ }
56
+ if (!binding) {
57
+ const baseline = policyForSetup(setup.setup, learningScopeDigest(state), state.policy.digest);
58
+ if (!await ctx.ui.confirm("Create a scoped next-order registry?", "Baseline is the selected task models/effort. Future adoption can only change model/effort for these same definitions. No existing or active run changes.")) return;
59
+ const root = join(homedir(), ".local", "state", "pi-daddy", "work-registries"); await mkdir(root, { recursive: true, mode: 0o700 }); await privateDirectory(root);
60
+ binding = await createWorkPolicyRegistry({ directory: join(root, randomUUID()), authorityId: "local-operator", baseline });
61
+ await savePolicy(ctx.cwd, "Baseline", baseline); await retainRegistryBinding(ctx.cwd,binding); await writeProductJson(descriptor(ctx.cwd), binding, true);
62
+ }
63
+ const registry = openWorkPolicyRegistry(binding), current = await registry.inspect();
64
+ const menu = ["Prepare candidate model/effort profile", "Activate adopted comparison for next orders", "Roll back active adoption", "Inspect registry", "Recover learning link (no registry change)"];
65
+ const choice = await ctx.ui.select("Scoped settings — existing runs stay pinned", menu);
66
+ if (!choice) return;
67
+ if (choice === menu[3]) { await ctx.ui.editor("Registry details (read-only; editor changes are discarded)", JSON.stringify(current, null, 2)); return; }
68
+ if (choice === menu[0]) {
69
+ const candidate = structuredClone(current.candidate);
70
+ for (const profile of candidate.profiles) {
71
+ const models = ctx.scopedModels?.length ? ctx.scopedModels.map(entry => entry.model) : ctx.modelRegistry.getAvailable();
72
+ if(!models.length)throw Error("No available Pi models. Configure a model in Pi, then reopen settings; no fallback is chosen.");
73
+ const labels = models.map(m => `${m.provider}/${m.id}`), selected = await ctx.ui.select(`Model for ${setup.setup.tasks.find(t => t.id === profile.taskId)!.outcome}`, labels), model = models[labels.indexOf(selected ?? "")]; if (!model) return;
74
+ const thinking = await ctx.ui.select("Requested effort", getSupportedThinkingLevels(model)); if (!thinking) return;
75
+ profile.model = `${model.provider}/${model.id}`; profile.thinking = thinking as typeof profile.thinking;
76
+ }
77
+ const name = await ctx.ui.input("Candidate name", "Alternative profile"); if (!name?.trim()) return;
78
+ await savePolicy(ctx.cwd, name, candidate);
79
+ ctx.ui.notify(`Candidate retained, not adopted. Comparison must bind configuration digest ${workPolicyDigest(candidate)} to actual outputs. Use Learning to review full artifacts, record quality and choose adopt/reject/defer.`, "info"); return;
80
+ }
81
+ const view = workspace.inspect(Date.now()), comparisons = view.comparisons.filter(c => c.state !== "deferred");
82
+ if(!comparisons.length)throw Error("Adoption deferred: no ready retained comparison. Open Learning to connect/review actual evidence and inspect defer reasons. No setting was activated.");
83
+ const labels = comparisons.map((c, i) => `${i + 1}. ${c.title}`), picked = await ctx.ui.select("Retained comparison", labels), comparison = comparisons[labels.indexOf(picked ?? "")]; if (!comparison) return;
84
+ if (choice === menu[1]) {
85
+ let adoptionBinding = workspace.comparisonContext(comparison.name).adoptionBinding;
86
+ if (!adoptionBinding) {
87
+ const files=await readdir(join(ctx.cwd,".pi","work-policies")); if(files.length>128)throw Error("candidate profile bound exceeded");
88
+ const candidates: {name:string;policy:WorkPolicy}[]=[];
89
+ for(const file of files.filter(f=>/^[a-f0-9]{64}\.json$/.test(f))){const saved=await readProductJson(join(ctx.cwd,".pi","work-policies",file)) as {name:string;policy:WorkPolicy};const policy=await readPolicy(ctx.cwd,file.slice(0,-5));if(policy.scopeDigest===current.scopeDigest)candidates.push({name:saved.name,policy});}
90
+ const names=candidates.map((c,i)=>`${i+1}. ${c.name}: ${c.policy.profiles.map(p=>`${p.model}/${p.thinking}`).join(", ")}`),picked=await ctx.ui.select("Candidate already compared in this retained evidence",names),selected=candidates[names.indexOf(picked??"")];if(!selected)return;
91
+ if(!await ctx.ui.confirm("Bind this scoped adoption proposal?","This requires linked confirmed cases, hypothesis and a durable full-artifact choice of this exact candidate. It does not create eligibility or activate anything."))return;
92
+ adoptionBinding=bindLearningAdoption(workspace,comparison.name,{candidateDigest:workPolicyDigest(selected.policy),rollbackCandidateDigest:current.candidateDigest,assessmentPolicyDigest:selected.policy.assessmentPolicyDigest,expiresAt:Date.now()+7*86400000});
93
+ }
94
+ const candidate = await readPolicy(ctx.cwd, adoptionBinding.candidateDigest), facts = await independentFacts(ctx, adoptionBinding); if (!facts) return;
95
+ const a = authority(binding.initial.authorityId, adoptionBinding, facts), receipt = workspace.prepareAdoption(comparison.name, a.adoption, facts, Date.now());
96
+ const request: WorkPolicyActivation = { version: "ordinary-work-activation-v1", requestId: `activate-${randomUUID()}`, expectedRevision: current.revision, expectedCandidateDigest: current.candidateDigest, candidate, binding: adoptionBinding, receipt };
97
+ if (!await ctx.ui.confirm("Activate for NEXT orders only?", `${comparison.title}\n${candidate.profiles.map(p => `${p.taskId}: ${p.model} / ${p.thinking}`).join("\n")}\nNo active work changes. This is separate from your quality choice.`)) return;
98
+ a.activationDigests = [workPolicyActivationDigest(request)];
99
+ const applied = await registry.activate(request, a), archiveRoot = workspace.configuration().archiveRoot;
100
+ const registryManifestId = retain(harness, archiveRoot, "producer-registry", applied);
101
+ const wrapper = retain(harness, archiveRoot, "learning-registry-receipt", { version: "learning-registry-receipt-v1", operation: "activate", requestId: request.requestId, adoptionId: receipt.id, scopeDigest: applied.scopeDigest, candidateDigest: applied.candidateDigest, revision: applied.revision, registryManifestId });
102
+ try { workspace.linkActivation(comparison.name, wrapper, [wrapper]); }
103
+ catch (error) { throw Error(`Registry activated but learning link failed (${String(error)}). Do not activate again; use Recover learning link. Retained receipt ${wrapper}.`); }
104
+ ctx.ui.notify("Activated for subsequent bounded work runs. Existing runs remain pinned; later outcomes are still unobserved.", "info"); return;
105
+ }
106
+ if(choice===menu[4]){
107
+ const change=current.lastChange,receipt=workspace.preparedAdoption(comparison.name);
108
+ if(!change||!receipt||change.adoptionId!==receipt.id)throw Error("Last original registry change does not belong to this comparison");
109
+ if(!await ctx.ui.confirm("Reconnect actual registry observation?",`The last ${change.operation} is already applied at revision ${current.revision}. This only retains and links that exact original registry observation; no effect is replayed.`))return;
110
+ const root=workspace.configuration().archiveRoot,registryManifestId=retain(harness,root,"producer-registry",current),wrapper=retain(harness,root,"learning-registry-receipt",{version:"learning-registry-receipt-v1",operation:change.operation,requestId:change.requestId,adoptionId:receipt.id,scopeDigest:current.scopeDigest,candidateDigest:current.candidateDigest,revision:current.revision,registryManifestId});
111
+ if(change.operation==="activate")workspace.linkActivation(comparison.name,wrapper,[wrapper]);else workspace.linkRollback(comparison.name,wrapper,[wrapper]);
112
+ ctx.ui.notify("Original registry observation linked. No activation or rollback replayed.","info");return;
113
+ }
114
+ if (!current.activation) throw Error("No current adoption to roll back");
115
+ const receipt = workspace.preparedAdoption(comparison.name);
116
+ if (!receipt || receipt.id !== current.activation.receipt.id) throw Error("Selected comparison is not the active adoption");
117
+ const root = workspace.configuration().archiveRoot;
118
+ const evidence = retain(harness, root, "producer-rollback-request", { reason: "operator-request", adoptionId: receipt.id, observedRevision: current.revision });
119
+ const request = workspace.previewRollback(comparison.name, "operator-request", [evidence], Date.now() + 120_000);
120
+ const restore = await registry.previewRollback(request);
121
+ if (!await ctx.ui.confirm("Roll back NEXT orders?", `Restore candidate: ${restore.restoreCandidateDigest}\n${restore.candidate.profiles.map(p => `${p.taskId}: ${p.model} / ${p.thinking}`).join("\n")}\nNo active run changes; no defect or improvement is inferred.`)) return;
122
+ const a = authority(binding.initial.authorityId); a.adoption = { id: a.id, adoptions: [], rollbacks: [request.id] };
123
+ workspace.prepareRollback(comparison.name, request, a.adoption, Date.now(), { adoptionId: receipt.id, candidateDigest: current.candidateDigest, scopeDigest: current.scopeDigest });
124
+ const applied = await registry.rollback(request, a), registryManifestId = retain(harness, root, "producer-registry", applied);
125
+ const wrapper = retain(harness, root, "learning-registry-receipt", { version: "learning-registry-receipt-v1", operation: "rollback", requestId: request.id, adoptionId: receipt.id, scopeDigest: applied.scopeDigest, candidateDigest: applied.candidateDigest, revision: applied.revision, registryManifestId });
126
+ try { workspace.linkRollback(comparison.name, wrapper, [wrapper]); }
127
+ catch (error) { throw Error(`Rollback applied but learning link failed (${String(error)}). Use Recover learning link; do not repeat the effect. Retained receipt ${wrapper}.`); }
128
+ ctx.ui.notify("Rollback applied for next orders. Active/history-bearing runs were not changed.", "info");
129
+ }
130
+ /** Fresh human facts at use; a stored lifecycle/receipt is never treated as independent live authority. */
131
+ export async function prepareNextWorkPolicy(ctx: ExtensionCommandContext, state: DeclaredWorkState, setup: WorkSetup) {
132
+ const binding = await loadWorkPolicyRegistry(ctx.cwd, state); if (!binding) return { setup, pin: async (_id: string) => null };
133
+ const registry = openWorkPolicyRegistry(binding), current = await registry.inspect();
134
+ const facts = current.activation ? await independentFacts(ctx, current.activation.binding) : undefined;
135
+ if (facts === null) throw Error("Next-order adoption deferred; no fallback profile or child launched");
136
+ const a = authority(binding.initial.authorityId, current.activation?.binding, facts);
137
+ const configured = { ...setup, tasks: setup.tasks.map(t => { const profile = current.candidate.profiles.find(p => p.taskId === t.id); if (!profile || profile.agent !== t.agent) throw Error("adopted profile no longer matches task definition"); return { ...t, model: profile.model, thinking: profile.thinking }; }) };
138
+ return { setup: configured, async pin(orderId: string) { const pinned = await registry.pin(orderId, a, { revision: current.revision, candidateDigest: current.candidateDigest }); if (pinned.candidateDigest !== current.candidateDigest || pinned.revision !== current.revision) throw Error("registry changed before order pin; no child launched"); return { orderId, candidateDigest: pinned.candidateDigest, adoptionId: pinned.adoptionId, revision: pinned.revision, scopeDigest: binding.initial.scopeDigest }; } };
139
+ }
@@ -0,0 +1,75 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { mkdir } from "node:fs/promises";
5
+ import { BorderedLoader, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
6
+ import { supportedModelEfforts as getSupportedThinkingLevels } from "../src/model-preflight.ts";
7
+ import { splitBudget } from "../src/fanout.ts";
8
+ import { runWorkSetup, type WorkRunResult } from "../src/work-run.ts";
9
+ import { loadWorkSetup, type RecordedWorkSetup } from "../src/work-setup.ts";
10
+ import { privateDirectory, writeProductJson } from "../src/product-files.ts";
11
+ import { loadDeclaredWork } from "../src/work-command.ts";
12
+ import { readDailySnapshot } from "../src/daily-view-input.ts";
13
+ import { ordinaryChildrenFor } from "../src/ordinary-children.ts";
14
+ import { runOneDelegation } from "./run-delegation.ts";
15
+ import { newDelegationOccurrence } from "./execution-occurrence.ts";
16
+ import { assertDelegationAuthority } from "./delegation-authority.ts";
17
+ import type { GrantsSession } from "./session.ts";
18
+ import { prepareNextWorkPolicy } from "./work-policy-session.ts";
19
+
20
+ export async function runSelectedWork(session: GrantsSession, ctx: ExtensionCommandContext, setup: RecordedWorkSetup, priorities?: string[], lifetime?: { signal: AbortSignal; started: (promise: Promise<WorkRunResult>) => void }): Promise<WorkRunResult | null> {
21
+ assertDelegationAuthority(session);
22
+ if (!ctx.isIdle() || !ordinaryChildrenFor(session).quiescent()) throw Error("Wait for original work to settle before starting a bounded run");
23
+ const loaded = await loadWorkSetup(setup.state);
24
+ if (!loaded || session.declaredWork?.selectedSnapshot.snapshot.digest !== setup.state.selectedSnapshot.snapshot.digest) throw Error("selected work changed");
25
+ const available=splitBudget(session.fanoutBudget,setup.setup.tasks.length);if(!available.ok)throw Error(available.reason);
26
+ const policy = await prepareNextWorkPolicy(ctx, setup.state, setup.setup), configured = policy.setup;
27
+ // Resolve every profile before any ledger, result store or child. No provider/model/effort fallback.
28
+ for (const task of configured.tasks) {
29
+ const slash = task.model.indexOf("/"), model = ctx.modelRegistry.find(task.model.slice(0, slash), task.model.slice(slash + 1));
30
+ if (!model || !getSupportedThinkingLevels(model).includes(task.thinking)) throw Error(`Unsupported profile for ${task.outcome}: ${task.model} / ${task.thinking}; choose a listed model and effort`);
31
+ }
32
+ if (!await ctx.ui.confirm("Run this work?", `${setup.setup.tasks.length} attempts; at most ${setup.setup.maxParallel} parallel. Each child's existing timeout/output/grant bounds apply. Dependencies receive complete bounded predecessor output. No automatic retries; no universal token/cost cap on ordinary children.\n\n${configured.tasks.map(t => `${t.outcome} — ${t.agent ?? "no tools"}, ${t.model}, ${t.thinking}`).join("\n")}`)) return null;
33
+ const selectedNow=await loadDeclaredWork(setup.state.statePath);
34
+ if(selectedNow?.selectedSnapshot.snapshot.digest!==setup.state.selectedSnapshot.snapshot.digest||session.declaredWork?.selectedSnapshot.snapshot.digest!==setup.state.selectedSnapshot.snapshot.digest)throw Error("work selection changed during confirmation; no order started");
35
+ const root = join(homedir(), ".local", "state", "pi-daddy", "work-runs");
36
+ await mkdir(root, { recursive: true, mode: 0o700 }); await privateDirectory(root);
37
+ const abort = new AbortController(), signal = lifetime ? AbortSignal.any([abort.signal, lifetime.signal]) : abort.signal;
38
+ signal.throwIfAborted(); const orderId = randomUUID(), policyPin = await policy.pin(orderId);
39
+ const run = (update: (text: string) => void) => { const pending = runWorkSetup({ directory: join(root, orderId), orderId, policyPin, selectionDigest: setup.state.selectedSnapshot.snapshot.digest, setup: configured, budget: session.fanoutBudget, priorities, signal, admissionOpen:()=>(ordinaryChildrenFor(session).inspect() as {admission:string}).admission==="open",
40
+ onUpdate: rows => update(rows.map(row => `${row.state}: ${setup.setup.tasks.find(t => t.id === row.id)!.outcome}`).join("\n")),
41
+ execute: async (task, prompt, budget, started) => {
42
+ assertDelegationAuthority(session);
43
+ if(session.declaredWork?.selectedSnapshot.snapshot.digest!==setup.state.selectedSnapshot.snapshot.digest)throw Error("selected work changed; remaining order attempts were not launched");
44
+ const ids = newDelegationOccurrence(session, setup.setup.tasks.findIndex(t => t.id === task.id));
45
+ await started(ids.executionId);
46
+ const state = { ...setup.state, obligation: setup.tasks.find(t => t.id === task.id)!.obligation };
47
+ return runOneDelegation(session, { task: prompt, ...(task.agent ? { agent: task.agent } : { tools: [] }), model: task.model, thinking: task.thinking, correlation: { schema_version: "1.0", task_id: task.id, context_id: `work-${setup.setup.id}` } }, ids, budget, ctx, signal, { declaredWork: state, toolCallId: `work-run:${ids.executionId}` });
48
+ },
49
+ }); lifetime?.started(pending); return pending; };
50
+ let result: WorkRunResult;
51
+ if (ctx.mode === "tui") {
52
+ result = await ctx.ui.custom<WorkRunResult>((tui, theme, _keys, done) => {
53
+ const loader = new BorderedLoader(tui, theme, "Running declared work — Esc requests cancellation; awaiting all original children");
54
+ loader.onAbort = () => abort.abort();
55
+ void run(text => ctx.ui.setWidget("pi-daddy-work", text.split("\n"))).then(done).catch(error => done({ error } as unknown as WorkRunResult));
56
+ return loader;
57
+ });
58
+ ctx.ui.setWidget("pi-daddy-work", undefined);
59
+ if ("error" in result) throw (result as unknown as { error: unknown }).error;
60
+ } else result = await run(text => ctx.ui.notify(text, "info"));
61
+ // Navigation index only; the original occurrence ledger/control journal remains the result authority.
62
+ await writeProductJson(join(session.cwd, ".pi", "work-last-run.json"), result.binding, true);
63
+ return result;
64
+ }
65
+ export async function showWorkResults(ctx: ExtensionCommandContext, result: WorkRunResult): Promise<void> {
66
+ const choices = result.tasks.map((row, i) => `${i + 1}. ${row.state} — ${result.binding.initial.setup.tasks.find(t => t.id === row.id)?.outcome ?? row.id}`);
67
+ const choice = await ctx.ui.select("Results — execution is not acceptance", choices);
68
+ const row = result.tasks[choices.indexOf(choice ?? "")]; if (!row) return;
69
+ if (!row.resultPath || !row.resultDigest) { ctx.ui.notify(row.reason ?? "No retained output; original acknowledgement unavailable", "warning"); return; }
70
+ if (row.resultPath !== join(result.binding.directory, `${row.id}.txt`)) throw Error("result path mismatch");
71
+ const source = await readDailySnapshot(row.resultPath, 1024 * 1024);
72
+ if (source.status !== "read" || source.sha256 !== row.resultDigest) throw Error("retained output missing or changed");
73
+ const output = new TextDecoder("utf8", { fatal: true }).decode(source.bytes);
74
+ await ctx.ui.editor("Retained full output (controls escaped; edits do not alter the artifact)", output.replace(/[\p{Cc}\p{Cf}]/gu, c => c === "\n" || c === "\t" ? c : `\\u${c.charCodeAt(0).toString(16).padStart(4,"0")}`));
75
+ }
@@ -11,12 +11,12 @@ export async function beginDeclaredWorkAttempt(input: {
11
11
  childId: string;
12
12
  executionId: string;
13
13
  parentExecutionId: string | null;
14
- toolCallId?: string;
14
+ toolCallId?: string; declaredWork?: GrantsSession["declaredWork"];
15
15
  preparedWorkspace?: PreparedWorkspace;
16
16
  configuredTimeoutMs: number;
17
17
  startedAt: Date;
18
18
  }): Promise<null | { finish(state: Extract<WorkOccurrencePayload["state"], "completed" | "failed">): Promise<void> }> {
19
- const state = input.session.declaredWork;
19
+ const state = input.declaredWork ?? input.session.declaredWork;
20
20
  if (!state) return null;
21
21
  const argument = (flag: string): string | null => {
22
22
  const index = input.plan.args.indexOf(flag);
@@ -0,0 +1,132 @@
1
+ import { randomUUID, createHash } from "node:crypto";
2
+ import { join } from "node:path";
3
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
4
+ import { supportedModelEfforts as getSupportedThinkingLevels } from "../src/model-preflight.ts";
5
+ import { workSetup, recordWorkSetup, selectRecordedWork, loadWorkSetup, listWorkSetups, type WorkSetup, type WorkTaskSetup } from "../src/work-setup.ts";
6
+ import { loadDeclaredWork } from "../src/work-command.ts";
7
+ import { intentKey } from "../src/intent-control.ts";
8
+ import { ordinaryChildrenFor } from "../src/ordinary-children.ts";
9
+ import { inspectWorkRun, type WorkRunInitial, type WorkRunResult } from "../src/work-run.ts";
10
+ import type { ControlBinding } from "../src/control-journal.ts";
11
+ import { readProductJson } from "../src/product-files.ts";
12
+ import { dashboardActionFeedback } from "../src/dashboard-cli.ts";
13
+ import { runSelectedWork, showWorkResults } from "./work-run-session.ts";
14
+ import type { GrantsSession } from "./session.ts";
15
+ import type { createDailyDashboardSession } from "./daily-dashboard-session.ts";
16
+
17
+ const cancelled = () => Error("Work setup cancelled; nothing selected or launched");
18
+ async function ask(ctx: ExtensionCommandContext, title: string, initial = ""): Promise<string> {
19
+ const value = await ctx.ui.editor(title, initial); if (!value?.trim()) throw cancelled(); return value.trim();
20
+ }
21
+ async function profile(ctx: ExtensionCommandContext, session: GrantsSession, previous?: WorkTaskSetup) {
22
+ const agents = ["No tools (text-only)", ...session.definitions.keys()];
23
+ const agent = await ctx.ui.select("Agent definition — existing grants still apply", agents);
24
+ if (!agent) throw cancelled();
25
+ const available = ctx.scopedModels?.length ? ctx.scopedModels.map(entry => entry.model) : ctx.modelRegistry.getAvailable();
26
+ if(!available.length)throw Error("No available Pi models. Configure a model in Pi, then reopen work setup; no fallback is chosen.");
27
+ const labels = available.map(model => `${model.provider}/${model.id}`);
28
+ const selected = await ctx.ui.select("Model — no silent fallback", labels);
29
+ const model = available[labels.indexOf(selected ?? "")]; if (!model) throw cancelled();
30
+ const thinking = await ctx.ui.select("Requested effort (not proof of provider-internal reasoning)", getSupportedThinkingLevels(model));
31
+ if (!thinking) throw cancelled();
32
+ return { agent: agent === agents[0] ? null : agent, model: `${model.provider}/${model.id}`, thinking: thinking as WorkTaskSetup["thinking"] };
33
+ }
34
+ async function author(ctx: ExtensionCommandContext, session: GrantsSession): Promise<WorkSetup> {
35
+ const outcome = await ask(ctx, "What outcome should this work produce?");
36
+ const tasks: WorkTaskSetup[] = [];
37
+ do {
38
+ const outcome = await ask(ctx, `Task ${tasks.length + 1}: outcome / instructions`);
39
+ const selected = await profile(ctx, session);
40
+ const dependencies: string[] = [];
41
+ while (tasks.length) {
42
+ const candidates = tasks.filter(t => !dependencies.includes(t.id));
43
+ const labels = ["Dependencies complete", ...candidates.map((t, i) => `${i + 1}. ${t.outcome}`)];
44
+ const choice = await ctx.ui.select("Wait for which prior tasks? Failed dependencies block only their branch", labels);
45
+ if (!choice) throw cancelled(); if (choice === labels[0]) break;
46
+ dependencies.push(candidates[labels.indexOf(choice) - 1].id);
47
+ }
48
+ tasks.push({ id: `task${tasks.length + 1}`, outcome, ...selected, dependencies });
49
+ } while (tasks.length < 8 && await ctx.ui.confirm("Add another task?", "Independent tasks can run in parallel; dependent tasks receive predecessor results."));
50
+ const width = await ctx.ui.select("Maximum parallel agents", Array.from({ length: tasks.length }, (_, i) => String(i + 1)));
51
+ if (!width) throw cancelled();
52
+ return workSetup({ version: "work-setup-v1", id: `work-${randomUUID().slice(0, 8)}`, outcome, tasks, maxParallel: Number(width) });
53
+ }
54
+ async function revise(ctx: ExtensionCommandContext, session: GrantsSession, previous: WorkSetup): Promise<WorkSetup> {
55
+ const setup = structuredClone(previous);
56
+ while (true) {
57
+ const labels = ["Save revision", "Overall outcome", "Parallel limit", ...setup.tasks.map((t,i) => `${i+1}. ${t.outcome}`)];
58
+ const choice = await ctx.ui.select("Revise scope/configuration (topology and permissions stay fixed)", labels);
59
+ if (!choice) throw cancelled();
60
+ const index = labels.indexOf(choice);
61
+ if (index === 0) return workSetup(setup);
62
+ if (index === 1) setup.outcome = await ask(ctx, "Revised outcome", setup.outcome);
63
+ else if (index === 2) { const width = await ctx.ui.select("Parallel limit", Array.from({ length: setup.tasks.length }, (_, i) => String(i + 1))); if (!width) throw cancelled(); setup.maxParallel = Number(width); }
64
+ else { const task = setup.tasks[index - 3]; task.outcome = await ask(ctx, "Task outcome", task.outcome); Object.assign(task, await profile(ctx, session, task)); }
65
+ }
66
+ }
67
+
68
+ export function createWorkSession(session: GrantsSession, host: ReturnType<typeof createDailyDashboardSession>, rebind: () => void) {
69
+ let pending: Promise<void> | null = null, owned: Promise<WorkRunResult> | null = null;
70
+ const shutdown = new AbortController();
71
+ const run = async (verb: string, ctx: ExtensionCommandContext) => {
72
+ if (!ctx.hasUI) throw Error("Use /grants work in interactive Pi for setup and explicit run confirmation");
73
+ const current = await loadDeclaredWork(join(ctx.cwd, ".pi", "work-current.json"));
74
+ if (current) { session.declaredWork = current; rebind(); }
75
+ const selected = current ? await loadWorkSetup(current) : null;
76
+ const menu = ["Run selected work", "Declare new work", "Revise scope / configuration", "Record alternative", "Select saved work", "Select task for ordinary delegation", "Change priority", "Inspect results"];
77
+ const choice = verb ? ({ run: menu[0], new: menu[1], results: menu[7] } as Record<string, string>)[verb] : await ctx.ui.select(current ? selected?.setup.outcome ?? `Declared work: ${current.id}` : "No work selected", menu);
78
+ if (!choice) { if (verb) throw Error("Use /grants work, work new, work run or work results"); return; }
79
+ const guard = () => { if (!ctx.isIdle() || !ordinaryChildrenFor(session).quiescent()) throw Error("Wait for the original active work to settle before changing setup"); };
80
+ if (choice === menu[0]) {
81
+ if (!selected) throw Error("Declare multi-task work first; existing ordinary delegation remains available");
82
+ let priorities: string[] | undefined;
83
+ if (host.running) {
84
+ const frame = await host.frame(), controls = frame.controls as { intent?: { priorities: { obligation: { digest: string } }[] } };
85
+ priorities = controls.intent?.priorities.map(p => selected.tasks.find(t => t.obligation.digest === p.obligation.digest)?.id ?? "");
86
+ }
87
+ const result = await runSelectedWork(session, ctx, selected, priorities, { signal: shutdown.signal, started: promise => { owned = promise; } });
88
+ if (shutdown.signal.aborted) return;
89
+ if (result) { ctx.ui.notify(`${result.tasks.filter(t => t.state === "finished").length} finished; ${result.tasks.filter(t => t.state !== "finished").length} failed/blocked. No acceptance inferred.`, "info"); await showWorkResults(ctx, result); }
90
+ return;
91
+ }
92
+ if (choice === menu[7]) {
93
+ const binding = await readProductJson(join(ctx.cwd, ".pi", "work-last-run.json")) as ControlBinding<WorkRunInitial> | null;
94
+ if (!binding) throw Error("No bounded run result is retained for this project yet");
95
+ await showWorkResults(ctx, await inspectWorkRun(binding)); return;
96
+ }
97
+ if (choice === menu[6]) {
98
+ if (!host.running) throw Error("Start /grants host to change priority through the current dispatch owner");
99
+ const frame = await host.frame(), actions = frame.actions.filter(a => a.key.startsWith("priority-"));
100
+ const labels = actions.map((a, i) => `${i + 1}. ${a.label}`), picked = await ctx.ui.select("Next-work priority (does not rearrange active children)", labels);
101
+ const action = actions[labels.indexOf(picked ?? "")]; if (action) ctx.ui.notify(dashboardActionFeedback(action.label, await host.choose(action.key,{tip:frame.tip,requestDigest:action.requestDigest})), "info"); return;
102
+ }
103
+ guard();
104
+ if (choice === menu[5]) {
105
+ if (!selected || !current) throw Error("No multi-task setup selected");
106
+ const labels = selected.setup.tasks.map((t, i) => `${i + 1}. ${t.outcome}`), picked = await ctx.ui.select("Ordinary delegate binds which task?", labels), task = selected.tasks[labels.indexOf(picked ?? "")];
107
+ if (!task) return;
108
+ guard(); session.declaredWork = await selectRecordedWork({ ...selected, state: { ...selected.state, obligation: task.obligation } }, current.selectedSnapshot); rebind();
109
+ ctx.ui.notify("Task selected for subsequent ordinary delegation. Existing attempts were not rebound.", "info"); return;
110
+ }
111
+ if (choice === menu[4]) {
112
+ const setups = await listWorkSetups(ctx.cwd), labels = setups.map((s, i) => `${i + 1}. ${s.setup.outcome}`), picked = await ctx.ui.select("Select saved setup (existing work stays pinned)", labels), target = setups[labels.indexOf(picked ?? "")];
113
+ if (!target) return;
114
+ guard();
115
+ if (host.running) { if (!await ctx.ui.confirm("Stop the current host and select saved work?", "The old host's evidence remains intact. Start /grants host again for the selected setup.")) return; await host.run("stop"); }
116
+ session.declaredWork = await selectRecordedWork(target, current?.selectedSnapshot ?? null); rebind(); return;
117
+ }
118
+ const mode = choice === menu[1] ? "new" : choice === menu[2] ? "revise" : "alternative";
119
+ if (mode !== "new" && !selected) throw Error("Existing single-task work remains supported; declare a new multi-task setup to use revisions");
120
+ const draft = mode === "new" ? await author(ctx, session) : await revise(ctx, session, selected!.setup);
121
+ if (!await ctx.ui.confirm("Save this work setup?", `${draft.outcome}\n${draft.tasks.length} obligations; up to ${draft.maxParallel} parallel. Outcome/instruction text is retained in private project files, not governance ledgers. Nothing runs now.`)) return;
122
+ guard();
123
+ if (mode === "new" && host.running) { if (!await ctx.ui.confirm("Stop current host?", "A new topology needs a new host binding. Old evidence is preserved; no running work changes.")) return; await host.run("stop"); }
124
+ const recorded = await recordWorkSetup(ctx.cwd, draft, mode, current);
125
+ if (host.running) {
126
+ const prefix = mode === "revise" ? "scope" : "alternative", digest = createHash("sha256").update(intentKey(recorded.state.selectedSnapshot)).digest("hex").slice(0, 12);
127
+ ctx.ui.notify(dashboardActionFeedback("Select recorded work", await host.choose(`${prefix}-${digest}`)), "info");
128
+ } else { session.declaredWork = await selectRecordedWork(recorded, current?.selectedSnapshot ?? null); rebind(); }
129
+ ctx.ui.notify("Work saved. /grants work run starts it explicitly; /grants host connects the panel.", "info");
130
+ };
131
+ return { async close() { shutdown.abort(); if (owned) await owned.catch(() => undefined); }, async run(verb: string, ctx: ExtensionCommandContext) { shutdown.signal.throwIfAborted(); if (pending) throw Error("A work setup/run command is already active"); pending = run(verb, ctx); try { await pending; } finally { pending = null; owned = null; } } };
132
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-daddy",
3
- "version": "0.26.1",
3
+ "version": "0.27.0",
4
4
  "description": "Capability governance for pi sub-agents: spawn Agent Skills (SKILL.md) definitions whose allowed-tools becomes a grant that can only narrow going down a delegation tree, enforced by pi's own --tools allowlist, with an append-only ledger.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -37,6 +37,12 @@
37
37
  "types": "./dist/ledger.d.ts",
38
38
  "default": "./dist/ledger.js"
39
39
  },
40
+ "./work-setup": { "types": "./dist/work-setup.d.ts", "default": "./dist/work-setup.js" },
41
+ "./work-command": { "types": "./dist/work-command.d.ts", "default": "./dist/work-command.js" },
42
+ "./work-run": { "types": "./dist/work-run.d.ts", "default": "./dist/work-run.js" },
43
+ "./work-policy-registry": { "types": "./dist/work-policy-registry.d.ts", "default": "./dist/work-policy-registry.js" },
44
+ "./factory-registry": { "types": "./dist/factory-registry.d.ts", "default": "./dist/factory-registry.js" },
45
+ "./learning-connection": { "types": "./dist/learning-connection.d.ts", "default": "./dist/learning-connection.js" },
40
46
  "./work-ledger": {
41
47
  "types": "./dist/work-ledger.d.ts",
42
48
  "default": "./dist/work-ledger.js"
@@ -241,6 +247,8 @@
241
247
  "THIRD_PARTY_LICENSES",
242
248
  "THIRD_PARTY_NOTICES.md",
243
249
  "README.md",
250
+ "PRODUCT-GUIDE.md",
251
+ "REQUIREMENTS.md",
244
252
  "CHANGELOG.md",
245
253
  "LICENSE"
246
254
  ],
package/src/cli.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * `pi-daddy` — the command line, which today has exactly one subcommand.
3
+ * `pi-daddy` — scaffolding, read-only work inspection and the installed product guide.
4
4
  *
5
5
  * Thin on purpose: argv in, `discoverSkillPackages` + `planInit` + `applyInit`, report out. Every decision
6
6
  * lives in `./init.ts`, `./grant-env.ts` and `./skill-packages.ts` as functions that touch no argv and print
@@ -21,7 +21,9 @@ import { UnsafeGrantError } from "./grant-env.ts";
21
21
  import { applyInit, countDeclaring, planInit, type InitPlan } from "./init.ts";
22
22
  import { registeredWorkspaceIds } from "./workspace.ts";
23
23
  import { discoverSkillPackages, skillPackageRoots, type RefusedSkill, type SkillPackage } from "./skill-packages.ts";
24
- import { declareWork } from "./work-command.ts";
24
+ import { declareWork, loadDeclaredWork } from "./work-command.ts";
25
+ import { listWorkSetups, workPresentation } from "./work-setup.ts";
26
+ import { panelText } from "./daily-panel.ts";
25
27
 
26
28
  const USAGE = `pi-daddy — capability governance for pi sub-agents
27
29
 
@@ -30,6 +32,11 @@ Usage:
30
32
  packages that declare skills (package.json "pi": {"skills": …})
31
33
  pi-daddy work add --id <id> --outcome <text> [--dir <path>]
32
34
  declare one current obligation for ordinary delegation
35
+ pi-daddy work list | work show [--dir <path>]
36
+ inspect saved setups or selected outcome (no model calls)
37
+ In Pi: /grants work guided multi-task/model/effort/dependency setup and run
38
+ /grants learning retained review, trust, adoption and later outcomes
39
+ pi-daddy guide | current installed product guide / current requirement register
33
40
  pi-daddy --help | --version
34
41
 
35
42
  init copies each declared SKILL.md into .pi/skills/ and writes a grant naming exactly what those files
@@ -41,7 +48,7 @@ placeholder and stays unspawnable until you fill it in. Capabilities that can ch
41
48
  It never rewrites .pi/grants.env — delete that file if you want it regenerated.`;
42
49
 
43
50
  export interface ParsedArgs {
44
- command: "init" | "work-add" | "help" | "version";
51
+ command: "init" | "work-add" | "work-list" | "work-show" | "guide" | "current" | "help" | "version";
45
52
  dir?: string;
46
53
  force: boolean;
47
54
  id?: string;
@@ -64,10 +71,12 @@ export function parseArgs(argv: string[]): ParsedArgs {
64
71
  if (args.includes("--version") || args.includes("-v")) return { command: "version", force: false, errors: [] };
65
72
 
66
73
  const [command, ...tail] = args;
74
+ if(command==="guide"||command==="current")return{command,force:false,errors:tail.length?[`${command} takes no arguments`]:[]};
67
75
  if (command !== "init" && command !== "work") return { command: "help", force: false, errors: [`unknown command "${command}"`] };
68
76
  const work = command === "work";
69
- const rest = work && tail[0] === "add" ? tail.slice(1) : tail;
70
- if (work && tail[0] !== "add") return { command: "work-add", force: false, errors: ["work needs subcommand add"] };
77
+ const workVerb = tail[0];
78
+ const rest = work ? tail.slice(1) : tail;
79
+ if (work && !["add", "list", "show"].includes(workVerb)) return { command: "work-add", force: false, errors: ["work needs add, list or show; /grants work opens guided setup in Pi"] };
71
80
 
72
81
  const errors: string[] = [];
73
82
  let dir: string | undefined;
@@ -77,11 +86,11 @@ export function parseArgs(argv: string[]): ParsedArgs {
77
86
  const arg = rest[i];
78
87
  if (arg === "--force" && !work) {
79
88
  force = true;
80
- } else if (arg === "--id" && work) {
89
+ } else if (arg === "--id" && work && workVerb === "add") {
81
90
  const value = rest[i + 1];
82
91
  if (value === undefined || value.startsWith("-")) errors.push("--id needs a value");
83
92
  else { id = value; i += 1; }
84
- } else if (arg === "--outcome" && work) {
93
+ } else if (arg === "--outcome" && work && workVerb === "add") {
85
94
  const value = rest[i + 1];
86
95
  if (value === undefined || value.startsWith("-")) errors.push("--outcome needs text");
87
96
  else { outcome = value; i += 1; }
@@ -104,6 +113,7 @@ export function parseArgs(argv: string[]): ParsedArgs {
104
113
  }
105
114
  }
106
115
 
116
+ if (work && workVerb !== "add") return { command: workVerb === "list" ? "work-list" : "work-show", force: false, errors, ...(dir ? { dir } : {}) };
107
117
  if (work) {
108
118
  if (!id && !errors.some(error => error.startsWith("--id"))) errors.push("--id needs a value");
109
119
  if (!outcome && !errors.some(error => error.startsWith("--outcome"))) errors.push("--outcome needs text");
@@ -282,6 +292,15 @@ export async function main(argv: string[]): Promise<number> {
282
292
  console.log(USAGE);
283
293
  return 0;
284
294
  }
295
+ if(parsed.command==="guide"||parsed.command==="current"){
296
+ console.log(await readFile(new URL(parsed.command==="guide"?"../PRODUCT-GUIDE.md":"../REQUIREMENTS.md",import.meta.url),"utf8"));return 0;
297
+ }
298
+ if (parsed.command === "work-list" || parsed.command === "work-show") {
299
+ const cwd=resolvePath(parsed.dir??process.cwd());
300
+ if(parsed.command==="work-list") {const setups=await listWorkSetups(cwd);console.log(setups.map((s,i)=>`${i+1}. ${panelText(s.setup.outcome)} — ${s.setup.tasks.length} tasks; up to ${s.setup.maxParallel} parallel`).join("\n")||"No saved multi-task setups. In Pi: /grants work");}
301
+ else {const state=await loadDeclaredWork(resolvePath(cwd,".pi/work-current.json")),view=state?await workPresentation(state):null;console.log(view?[panelText(view.outcome),...view.obligations.map(o=>`- ${panelText(o.outcome)}`)].join("\n"):state?"Selected legacy work; outcome text unavailable":"No work selected. In Pi: /grants work");}
302
+ return 0;
303
+ }
285
304
  if (parsed.command === "work-add") {
286
305
  const declared = await declareWork({
287
306
  cwd: resolvePath(parsed.dir ?? process.cwd()),