@miller-tech/uap 1.104.2 → 1.106.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 (37) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/bin/cli.js +11 -0
  3. package/dist/bin/cli.js.map +1 -1
  4. package/dist/cli/deliver.d.ts.map +1 -1
  5. package/dist/cli/deliver.js +12 -0
  6. package/dist/cli/deliver.js.map +1 -1
  7. package/dist/cli/handsfree.d.ts +33 -0
  8. package/dist/cli/handsfree.d.ts.map +1 -0
  9. package/dist/cli/handsfree.js +273 -0
  10. package/dist/cli/handsfree.js.map +1 -0
  11. package/dist/cli/hooks.d.ts.map +1 -1
  12. package/dist/cli/hooks.js +15 -0
  13. package/dist/cli/hooks.js.map +1 -1
  14. package/dist/coordination/persistence-inject.d.ts +16 -0
  15. package/dist/coordination/persistence-inject.d.ts.map +1 -0
  16. package/dist/coordination/persistence-inject.js +45 -0
  17. package/dist/coordination/persistence-inject.js.map +1 -0
  18. package/dist/coordination/reactor.d.ts.map +1 -1
  19. package/dist/coordination/reactor.js +13 -0
  20. package/dist/coordination/reactor.js.map +1 -1
  21. package/dist/delivery/completion-ledger.d.ts +84 -0
  22. package/dist/delivery/completion-ledger.d.ts.map +1 -0
  23. package/dist/delivery/completion-ledger.js +182 -0
  24. package/dist/delivery/completion-ledger.js.map +1 -0
  25. package/dist/delivery/handsfree-config.d.ts +12 -0
  26. package/dist/delivery/handsfree-config.d.ts.map +1 -0
  27. package/dist/delivery/handsfree-config.js +44 -0
  28. package/dist/delivery/handsfree-config.js.map +1 -0
  29. package/dist/delivery/persistence-profile.d.ts +47 -0
  30. package/dist/delivery/persistence-profile.d.ts.map +1 -0
  31. package/dist/delivery/persistence-profile.js +78 -0
  32. package/dist/delivery/persistence-profile.js.map +1 -0
  33. package/package.json +1 -1
  34. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  35. package/templates/hooks/session-start.sh +9 -0
  36. package/templates/hooks/uap-todo-ledger.sh +12 -0
  37. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Completion Ledger (Option B) — the persistent, objective "definition of done"
3
+ * for a huge multi-epic build.
4
+ *
5
+ * A single deliver run judges one task; a huge build is many epics/tasks that
6
+ * can span many turns and many sessions. The ledger externalizes the whole
7
+ * build's DAG + per-item status to `.uap/completion-ledger.json`, so the
8
+ * hands-free machinery (Stop-hook block, reactor injection) can answer one
9
+ * question at any moment: "is the WHOLE build 100% done, and if not, what
10
+ * remains?" — independent of any model's self-assessment.
11
+ *
12
+ * Pure + fail-soft: every reader tolerates a missing/corrupt file by returning
13
+ * null, so the ledger can never wedge a session on its own.
14
+ */
15
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs';
16
+ import { dirname, join } from 'path';
17
+ /** Where the ledger lives for a project. */
18
+ export function ledgerPath(cwd = process.cwd()) {
19
+ return join(cwd, '.uap', 'completion-ledger.json');
20
+ }
21
+ export function ledgerExists(cwd = process.cwd()) {
22
+ return existsSync(ledgerPath(cwd));
23
+ }
24
+ /** Load the ledger, or null when absent/unreadable/corrupt (fail-soft). */
25
+ export function loadLedger(cwd = process.cwd()) {
26
+ try {
27
+ const p = ledgerPath(cwd);
28
+ if (!existsSync(p))
29
+ return null;
30
+ const raw = JSON.parse(readFileSync(p, 'utf-8'));
31
+ if (!raw || !Array.isArray(raw.items))
32
+ return null;
33
+ return raw;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ export function saveLedger(cwd, ledger) {
40
+ const p = ledgerPath(cwd);
41
+ mkdirSync(dirname(p), { recursive: true });
42
+ ledger.updatedAt = nowISO();
43
+ writeFileSync(p, JSON.stringify(ledger, null, 2) + '\n');
44
+ }
45
+ export function clearLedger(cwd = process.cwd()) {
46
+ try {
47
+ const p = ledgerPath(cwd);
48
+ if (existsSync(p))
49
+ rmSync(p);
50
+ }
51
+ catch {
52
+ /* best-effort */
53
+ }
54
+ }
55
+ /**
56
+ * Create (or replace) the ledger for a mission from a set of items. Existing
57
+ * items with the same id preserve their status so re-planning is non-destructive.
58
+ */
59
+ export function initLedger(cwd, mission, items) {
60
+ const prior = loadLedger(cwd);
61
+ const priorById = new Map((prior?.items ?? []).map((i) => [i.id, i]));
62
+ const ledger = {
63
+ mission: mission.slice(0, 500),
64
+ createdAt: prior?.createdAt ?? nowISO(),
65
+ updatedAt: nowISO(),
66
+ items: items.map((it) => {
67
+ const existing = priorById.get(it.id);
68
+ return {
69
+ id: it.id,
70
+ title: it.title.slice(0, 200),
71
+ kind: it.kind ?? 'epic',
72
+ status: existing?.status ?? 'pending',
73
+ ...(it.deps && it.deps.length ? { deps: it.deps } : {}),
74
+ ...(it.criteria && it.criteria.length ? { criteria: it.criteria } : {}),
75
+ ...(existing?.note ? { note: existing.note } : {}),
76
+ updatedAt: existing?.updatedAt ?? nowISO(),
77
+ };
78
+ }),
79
+ };
80
+ saveLedger(cwd, ledger);
81
+ return ledger;
82
+ }
83
+ /** Update one item's status. No-op (returns false) if the ledger/item is absent. */
84
+ export function markItem(cwd, id, status, note) {
85
+ const ledger = loadLedger(cwd);
86
+ if (!ledger)
87
+ return false;
88
+ const item = ledger.items.find((i) => i.id === id);
89
+ if (!item)
90
+ return false;
91
+ item.status = status;
92
+ item.updatedAt = nowISO();
93
+ if (note !== undefined)
94
+ item.note = note.slice(0, 300);
95
+ saveLedger(cwd, ledger);
96
+ return true;
97
+ }
98
+ /** Items that are not yet done (pending / in_progress / failed). */
99
+ export function remainingItems(ledger) {
100
+ return ledger.items.filter((i) => i.status !== 'done');
101
+ }
102
+ /** The whole build is complete only when EVERY item is done. */
103
+ export function isComplete(ledger) {
104
+ return ledger.items.length > 0 && ledger.items.every((i) => i.status === 'done');
105
+ }
106
+ export function progress(ledger) {
107
+ const total = ledger.items.length;
108
+ const done = ledger.items.filter((i) => i.status === 'done').length;
109
+ const failed = ledger.items.filter((i) => i.status === 'failed').length;
110
+ const pending = total - done - failed;
111
+ return { total, done, failed, pending, pct: total ? Math.round((done / total) * 100) : 0 };
112
+ }
113
+ /** A compact, model-facing summary of what still needs doing. */
114
+ export function formatRemaining(ledger, max = 12) {
115
+ const rem = remainingItems(ledger);
116
+ if (rem.length === 0)
117
+ return 'All ledger items complete.';
118
+ const p = progress(ledger);
119
+ const lines = rem.slice(0, max).map((i) => {
120
+ const mark = i.status === 'failed' ? 'x' : i.status === 'in_progress' ? '~' : ' ';
121
+ return ` [${mark}] ${i.id}: ${i.title}${i.note ? ` (${i.note})` : ''}`;
122
+ });
123
+ const more = rem.length > max ? `\n ...and ${rem.length - max} more` : '';
124
+ return `Build progress ${p.done}/${p.total} (${p.pct}%). REMAINING:\n${lines.join('\n')}${more}`;
125
+ }
126
+ function nowISO() {
127
+ // Date.now/new Date are fine at runtime (not inside a workflow script).
128
+ return new Date().toISOString();
129
+ }
130
+ /** Slugify a todo's content into a stable ledger id. */
131
+ function todoSlug(content, index) {
132
+ const base = content
133
+ .toLowerCase()
134
+ .replace(/[^a-z0-9]+/g, '-')
135
+ .replace(/^-+|-+$/g, '')
136
+ .slice(0, 40);
137
+ return base || `item-${index + 1}`;
138
+ }
139
+ function mapTodoStatus(s) {
140
+ if (s === 'completed')
141
+ return 'done';
142
+ if (s === 'in_progress')
143
+ return 'in_progress';
144
+ return 'pending';
145
+ }
146
+ /**
147
+ * Mirror a model's plan (Claude Code TodoWrite list) into the completion
148
+ * ledger — the auto-seed for interactive multi-step builds. The todo list IS
149
+ * the plan-in-progress, with authoritative statuses, so the ledger simply
150
+ * tracks it: created on first sync, statuses updated on every sync, and the
151
+ * mission/createdAt preserved across syncs. Duplicate slugs are de-collided.
152
+ * Returns the resulting ledger, or null when there is nothing to seed.
153
+ */
154
+ export function syncLedgerFromTodos(cwd, todos, mission) {
155
+ const clean = (todos || []).filter((t) => t && typeof t.content === 'string' && t.content.trim());
156
+ if (clean.length === 0)
157
+ return null;
158
+ const prior = loadLedger(cwd);
159
+ const seen = new Set();
160
+ const items = clean.map((t, i) => {
161
+ let id = todoSlug(t.content, i);
162
+ while (seen.has(id))
163
+ id = `${id}-${i}`;
164
+ seen.add(id);
165
+ return {
166
+ id,
167
+ title: t.content.trim().slice(0, 200),
168
+ kind: 'task',
169
+ status: mapTodoStatus(t.status),
170
+ updatedAt: nowISO(),
171
+ };
172
+ });
173
+ const ledger = {
174
+ mission: (prior?.mission || mission || 'Interactive multi-step build').slice(0, 500),
175
+ createdAt: prior?.createdAt ?? nowISO(),
176
+ updatedAt: nowISO(),
177
+ items,
178
+ };
179
+ saveLedger(cwd, ledger);
180
+ return ledger;
181
+ }
182
+ //# sourceMappingURL=completion-ledger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"completion-ledger.js","sourceRoot":"","sources":["../../src/delivery/completion-ledger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Hands-free config resolution — the master switch and active-model lookup for
3
+ * the persistence machinery (Options A-D). Auto-ON by default: hands-free
4
+ * persistence applies to any project with an active completion ledger unless
5
+ * explicitly disabled via `UAP_HANDSFREE=0` or `.uap.json` handsfree.enabled=false.
6
+ */
7
+ import type { PersistenceConfig } from './persistence-profile.js';
8
+ /** Resolve the persistence config from env + .uap.json (fail-soft, default ON). */
9
+ export declare function loadPersistenceConfig(cwd?: string): PersistenceConfig;
10
+ /** The model doing the work — env override, else the executor role, else unknown. */
11
+ export declare function resolveActiveModel(cwd?: string): string;
12
+ //# sourceMappingURL=handsfree-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handsfree-config.d.ts","sourceRoot":"","sources":["../../src/delivery/handsfree-config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,GAAG,GAAE,MAAsB,GAAG,iBAAiB,CAepF;AAED,qFAAqF;AACrF,wBAAgB,kBAAkB,CAAC,GAAG,GAAE,MAAsB,GAAG,MAAM,CAWtE"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Hands-free config resolution — the master switch and active-model lookup for
3
+ * the persistence machinery (Options A-D). Auto-ON by default: hands-free
4
+ * persistence applies to any project with an active completion ledger unless
5
+ * explicitly disabled via `UAP_HANDSFREE=0` or `.uap.json` handsfree.enabled=false.
6
+ */
7
+ import { loadUapConfigRaw } from '../utils/config-loader.js';
8
+ /** Resolve the persistence config from env + .uap.json (fail-soft, default ON). */
9
+ export function loadPersistenceConfig(cwd = process.cwd()) {
10
+ if (process.env.UAP_HANDSFREE === '0')
11
+ return { enabled: false };
12
+ let raw = {};
13
+ try {
14
+ raw = loadUapConfigRaw(cwd) ?? {};
15
+ }
16
+ catch {
17
+ raw = {};
18
+ }
19
+ const hf = raw.handsfree ?? {};
20
+ const envIntensity = process.env.UAP_HANDSFREE_INTENSITY;
21
+ return {
22
+ enabled: hf.enabled !== false && process.env.UAP_HANDSFREE !== '0',
23
+ ...(envIntensity ? { intensity: envIntensity } : hf.intensity ? { intensity: hf.intensity } : {}),
24
+ ...(hf.overrides ? { overrides: hf.overrides } : {}),
25
+ };
26
+ }
27
+ /** The model doing the work — env override, else the executor role, else unknown. */
28
+ export function resolveActiveModel(cwd = process.cwd()) {
29
+ if (process.env.UAP_ACTIVE_MODEL)
30
+ return process.env.UAP_ACTIVE_MODEL;
31
+ if (process.env.ANTHROPIC_MODEL)
32
+ return process.env.ANTHROPIC_MODEL;
33
+ try {
34
+ const raw = loadUapConfigRaw(cwd) ?? {};
35
+ const mm = raw.multiModel;
36
+ if (mm?.roles?.executor)
37
+ return mm.roles.executor;
38
+ }
39
+ catch {
40
+ /* fall through */
41
+ }
42
+ return 'unknown';
43
+ }
44
+ //# sourceMappingURL=handsfree-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handsfree-config.js","sourceRoot":"","sources":["../../src/delivery/handsfree-config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAG7D,mFAAmF;AACnF,MAAM,UAAU,qBAAqB,CAAC,MAAc,OAAO,CAAC,GAAG,EAAE;IAC/D,IAAI,OAAO,CAAC,GAAG,CAAC,aAAa,KAAK,GAAG;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IACjE,IAAI,GAAG,GAA4B,EAAE,CAAC;IACtC,IAAI,CAAC;QACH,GAAG,GAAI,gBAAgB,CAAC,GAAG,CAA6B,IAAI,EAAE,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,GAAG,GAAG,EAAE,CAAC;IACX,CAAC;IACD,MAAM,EAAE,GAAI,GAAG,CAAC,SAA2C,IAAI,EAAE,CAAC;IAClE,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,uBAAqE,CAAC;IACvG,OAAO;QACL,OAAO,EAAE,EAAE,CAAC,OAAO,KAAK,KAAK,IAAI,OAAO,CAAC,GAAG,CAAC,aAAa,KAAK,GAAG;QAClE,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjG,GAAG,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACrD,CAAC;AACJ,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,kBAAkB,CAAC,MAAc,OAAO,CAAC,GAAG,EAAE;IAC5D,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IACtE,IAAI,OAAO,CAAC,GAAG,CAAC,eAAe;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;IACpE,IAAI,CAAC;QACH,MAAM,GAAG,GAAI,gBAAgB,CAAC,GAAG,CAA6B,IAAI,EAAE,CAAC;QACrE,MAAM,EAAE,GAAG,GAAG,CAAC,UAA2D,CAAC;QAC3E,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ;YAAE,OAAO,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,kBAAkB;IACpB,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Persistence Profile (Option C) — model-aware hands-free forcing intensity.
3
+ *
4
+ * Fable persists on long tasks intrinsically (trained). Frontier chat models
5
+ * (Opus/Sonnet/GPT) are capable but stop at turn boundaries and declare
6
+ * premature "done". Small local models (qwen35-a3b) additionally loop and
7
+ * mis-declare completion. So the EXTERNAL forcing the harness applies should
8
+ * scale inversely with the model's intrinsic persistence: trust Fable, nudge
9
+ * the frontier, firmly drive the local model — while every profile keeps the
10
+ * anti-spin safeguards (bounded blocks + stagnation give-up) so "never stop
11
+ * early" never becomes "spin forever".
12
+ *
13
+ * Pure + fail-soft: resolution never throws; unknown models get the safe
14
+ * `moderate` default.
15
+ */
16
+ export type PersistenceIntensity = 'off' | 'light' | 'moderate' | 'aggressive';
17
+ export type ModelFamily = 'fable' | 'frontier' | 'local' | 'unknown';
18
+ export interface PersistenceProfile {
19
+ model: string;
20
+ family: ModelFamily;
21
+ intensity: PersistenceIntensity;
22
+ /** Whether the Stop hook may block session-end while the build is incomplete. */
23
+ stopHookBlocks: boolean;
24
+ /** Hard cap on consecutive Stop-hook blocks before giving up (anti-wedge). */
25
+ maxBlocks: number;
26
+ /** Consecutive no-progress blocks that trigger give-up + escalation. */
27
+ stagnationLimit: number;
28
+ /** Whether to inject the strong autonomy directive into interactive prompts. */
29
+ injectAutonomy: boolean;
30
+ }
31
+ /** Classify a model id into a persistence family by name heuristics. */
32
+ export declare function classifyModelFamily(modelId: string): ModelFamily;
33
+ export interface PersistenceConfig {
34
+ /** hands-free master switch; default ON (auto-applied). */
35
+ enabled?: boolean;
36
+ /** force a specific intensity regardless of model. */
37
+ intensity?: PersistenceIntensity;
38
+ /** per-model overrides keyed by model id substring. */
39
+ overrides?: Record<string, Partial<PersistenceProfile>>;
40
+ }
41
+ /**
42
+ * Resolve the effective persistence profile for a model. `off` when hands-free
43
+ * is disabled. An explicit `intensity` (env/config) overrides the family
44
+ * default; per-model overrides refine individual fields.
45
+ */
46
+ export declare function resolvePersistenceProfile(modelId: string | undefined, cfg?: PersistenceConfig): PersistenceProfile;
47
+ //# sourceMappingURL=persistence-profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persistence-profile.d.ts","sourceRoot":"","sources":["../../src/delivery/persistence-profile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,MAAM,oBAAoB,GAAG,KAAK,GAAG,OAAO,GAAG,UAAU,GAAG,YAAY,CAAC;AAC/E,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC;AAErE,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,CAAC;IACpB,SAAS,EAAE,oBAAoB,CAAC;IAChC,iFAAiF;IACjF,cAAc,EAAE,OAAO,CAAC;IACxB,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,eAAe,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,cAAc,EAAE,OAAO,CAAC;CACzB;AAmBD,wEAAwE;AACxE,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,CAMhE;AAeD,MAAM,WAAW,iBAAiB;IAChC,2DAA2D;IAC3D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sDAAsD;IACtD,SAAS,CAAC,EAAE,oBAAoB,CAAC;IACjC,uDAAuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC;CACzD;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,GAAG,GAAE,iBAAsB,GAC1B,kBAAkB,CAqBpB"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Persistence Profile (Option C) — model-aware hands-free forcing intensity.
3
+ *
4
+ * Fable persists on long tasks intrinsically (trained). Frontier chat models
5
+ * (Opus/Sonnet/GPT) are capable but stop at turn boundaries and declare
6
+ * premature "done". Small local models (qwen35-a3b) additionally loop and
7
+ * mis-declare completion. So the EXTERNAL forcing the harness applies should
8
+ * scale inversely with the model's intrinsic persistence: trust Fable, nudge
9
+ * the frontier, firmly drive the local model — while every profile keeps the
10
+ * anti-spin safeguards (bounded blocks + stagnation give-up) so "never stop
11
+ * early" never becomes "spin forever".
12
+ *
13
+ * Pure + fail-soft: resolution never throws; unknown models get the safe
14
+ * `moderate` default.
15
+ */
16
+ const PROFILES = {
17
+ // Fable — trust intrinsic persistence; keep only a light safety net.
18
+ light: { intensity: 'light', stopHookBlocks: true, maxBlocks: 3, stagnationLimit: 2, injectAutonomy: false },
19
+ // Opus/Sonnet/GPT — nudge past premature-done; moderate blocking.
20
+ moderate: { intensity: 'moderate', stopHookBlocks: true, maxBlocks: 6, stagnationLimit: 3, injectAutonomy: true },
21
+ // qwen / small local — firmly drive; more blocks tolerated, tighter stagnation.
22
+ aggressive: { intensity: 'aggressive', stopHookBlocks: true, maxBlocks: 9, stagnationLimit: 3, injectAutonomy: true },
23
+ };
24
+ const OFF_PROFILE = {
25
+ intensity: 'off',
26
+ stopHookBlocks: false,
27
+ maxBlocks: 0,
28
+ stagnationLimit: 0,
29
+ injectAutonomy: false,
30
+ };
31
+ /** Classify a model id into a persistence family by name heuristics. */
32
+ export function classifyModelFamily(modelId) {
33
+ const m = (modelId || '').toLowerCase();
34
+ if (m.includes('fable'))
35
+ return 'fable';
36
+ if (/qwen|a3b|llama|gemma|mistral|deepseek|local|small|3b|7b|8b|14b|32b|35b/.test(m))
37
+ return 'local';
38
+ if (/opus|sonnet|claude|gpt|o1|o3|gemini|grok|haiku/.test(m))
39
+ return 'frontier';
40
+ return 'unknown';
41
+ }
42
+ function familyIntensity(family) {
43
+ switch (family) {
44
+ case 'fable':
45
+ return 'light';
46
+ case 'local':
47
+ return 'aggressive';
48
+ case 'frontier':
49
+ return 'moderate';
50
+ default:
51
+ return 'moderate';
52
+ }
53
+ }
54
+ /**
55
+ * Resolve the effective persistence profile for a model. `off` when hands-free
56
+ * is disabled. An explicit `intensity` (env/config) overrides the family
57
+ * default; per-model overrides refine individual fields.
58
+ */
59
+ export function resolvePersistenceProfile(modelId, cfg = {}) {
60
+ const model = modelId || 'unknown';
61
+ const family = classifyModelFamily(model);
62
+ if (cfg.enabled === false) {
63
+ return { model, family, ...OFF_PROFILE };
64
+ }
65
+ const intensity = cfg.intensity ?? familyIntensity(family);
66
+ const base = intensity === 'off' ? OFF_PROFILE : PROFILES[intensity];
67
+ let profile = { model, family, ...base, intensity };
68
+ // Apply any per-model override whose key is a substring of the model id.
69
+ if (cfg.overrides) {
70
+ for (const [key, ov] of Object.entries(cfg.overrides)) {
71
+ if (model.toLowerCase().includes(key.toLowerCase())) {
72
+ profile = { ...profile, ...ov, model, family };
73
+ }
74
+ }
75
+ }
76
+ return profile;
77
+ }
78
+ //# sourceMappingURL=persistence-profile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persistence-profile.js","sourceRoot":"","sources":["../../src/delivery/persistence-profile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAmBH,MAAM,QAAQ,GAA+F;IAC3G,qEAAqE;IACrE,KAAK,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,eAAe,EAAE,CAAC,EAAE,cAAc,EAAE,KAAK,EAAE;IAC5G,kEAAkE;IAClE,QAAQ,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,eAAe,EAAE,CAAC,EAAE,cAAc,EAAE,IAAI,EAAE;IACjH,gFAAgF;IAChF,UAAU,EAAE,EAAE,SAAS,EAAE,YAAY,EAAE,cAAc,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,eAAe,EAAE,CAAC,EAAE,cAAc,EAAE,IAAI,EAAE;CACtH,CAAC;AAEF,MAAM,WAAW,GAAiD;IAChE,SAAS,EAAE,KAAK;IAChB,cAAc,EAAE,KAAK;IACrB,SAAS,EAAE,CAAC;IACZ,eAAe,EAAE,CAAC;IAClB,cAAc,EAAE,KAAK;CACtB,CAAC;AAEF,wEAAwE;AACxE,MAAM,UAAU,mBAAmB,CAAC,OAAe;IACjD,MAAM,CAAC,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACxC,IAAI,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IACxC,IAAI,wEAAwE,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,OAAO,CAAC;IACrG,IAAI,gDAAgD,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,UAAU,CAAC;IAChF,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,eAAe,CAAC,MAAmB;IAC1C,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,OAAO;YACV,OAAO,OAAO,CAAC;QACjB,KAAK,OAAO;YACV,OAAO,YAAY,CAAC;QACtB,KAAK,UAAU;YACb,OAAO,UAAU,CAAC;QACpB;YACE,OAAO,UAAU,CAAC;IACtB,CAAC;AACH,CAAC;AAWD;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CACvC,OAA2B,EAC3B,MAAyB,EAAE;IAE3B,MAAM,KAAK,GAAG,OAAO,IAAI,SAAS,CAAC;IACnC,MAAM,MAAM,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;IAE1C,IAAI,GAAG,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;QAC1B,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,CAAC;IAC3C,CAAC;IAED,MAAM,SAAS,GAAyB,GAAG,CAAC,SAAS,IAAI,eAAe,CAAC,MAAM,CAAC,CAAC;IACjF,MAAM,IAAI,GAAG,SAAS,KAAK,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IACrE,IAAI,OAAO,GAAuB,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,CAAC;IAExE,yEAAyE;IACzE,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;QAClB,KAAK,MAAM,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YACtD,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;gBACpD,OAAO,GAAG,EAAE,GAAG,OAAO,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;YACjD,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.104.2",
3
+ "version": "1.106.0",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -241,6 +241,15 @@ if [ -f "$TASK_DB" ] && [ "$TASK_BLOCKED" -gt 0 ]; then
241
241
  fi
242
242
  fi
243
243
 
244
+ # Hands-free auto-resume: surface an in-progress build's remaining work so the
245
+ # session picks it back up without being asked. Silent when nothing is pending.
246
+ if command -v uap >/dev/null 2>&1; then
247
+ resume_banner="$( ( cd "$PROJECT_DIR" 2>/dev/null && uap handsfree resume-banner 2>/dev/null ) || true )"
248
+ if [ -n "$resume_banner" ]; then
249
+ output+=$'\n'"$resume_banner"$'\n'
250
+ fi
251
+ fi
252
+
244
253
  if [ -n "$output" ]; then
245
254
  echo "$output"
246
255
  fi
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bash
2
+ # UAP Hands-free auto-seed — PostToolUse(TodoWrite)
3
+ # Mirror the model's plan (TodoWrite todo list) into the completion ledger so an
4
+ # interactive multi-step build auto-seeds a ledger with zero manual `init`.
5
+ # Silent, fail-open: never blocks or perturbs the session.
6
+ set -uo pipefail
7
+ PROJECT_DIR="${CLAUDE_PROJECT_DIR:-${FACTORY_PROJECT_DIR:-${CURSOR_PROJECT_DIR:-.}}}"
8
+ INPUT="$(timeout 2 cat 2>/dev/null || true)"
9
+ if command -v uap >/dev/null 2>&1; then
10
+ ( cd "$PROJECT_DIR" && printf '%s' "$INPUT" | uap handsfree sync-todos >/dev/null 2>&1 ) || true
11
+ fi
12
+ exit 0