@miller-tech/uap 1.106.0 → 1.108.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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/dashboard/data-service.d.ts +4 -0
- package/dist/dashboard/data-service.d.ts.map +1 -1
- package/dist/dashboard/data-service.js +4 -0
- package/dist/dashboard/data-service.js.map +1 -1
- package/dist/dashboard/orchestration-tree.d.ts +45 -0
- package/dist/dashboard/orchestration-tree.d.ts.map +1 -0
- package/dist/dashboard/orchestration-tree.js +110 -0
- package/dist/dashboard/orchestration-tree.js.map +1 -0
- package/dist/dashboard/savings.d.ts +31 -0
- package/dist/dashboard/savings.d.ts.map +1 -0
- package/dist/dashboard/savings.js +106 -0
- package/dist/dashboard/savings.js.map +1 -0
- package/package.json +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/adr_guard.py +189 -0
- package/src/policies/enforcers/bearer_lockdown.py +298 -0
- package/src/policies/enforcers/local_build_before_push.py +125 -0
- package/src/policies/enforcers/ship_loop_gate.py +113 -0
- package/src/policies/enforcers/workdir_scope.py +13 -2
- package/src/policies/schemas/policies/adr-guard.md +35 -0
- package/src/policies/schemas/policies/bearer-lockdown.md +38 -0
- package/src/policies/schemas/policies/local-build-before-push.md +38 -0
- package/src/policies/schemas/policies/ship-loop-gate.md +35 -0
- package/src/policies/schemas/policies/workdir-scope.md +5 -2
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/tests/test_workdir_scope_enforcer.py +10 -0
- package/web/dashboard.html +74 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""ship-loop-gate enforcer.
|
|
3
|
+
|
|
4
|
+
A task may not be marked `completed` until evidence exists for all four
|
|
5
|
+
ship-loop phases. Enforcement point: `TaskUpdate` operations that set
|
|
6
|
+
`status` to `completed` (and `TaskCreate` calls that try to land in
|
|
7
|
+
`completed` directly).
|
|
8
|
+
|
|
9
|
+
Required evidence keys (under `metadata.shipped`):
|
|
10
|
+
- merged : PR URL or merge commit SHA. The change is on `main`.
|
|
11
|
+
- deployed : deploy run URL / artifact ID. The change is in production.
|
|
12
|
+
- monitored : log/metric query window + result. The change isn't
|
|
13
|
+
producing new errors / regressions.
|
|
14
|
+
- verified : behavioural assertion against the deployed change
|
|
15
|
+
(URL probed, golden-path replayed, etc.). The change
|
|
16
|
+
produces the intended user-visible outcome.
|
|
17
|
+
|
|
18
|
+
Anything else (string content) is fine — the gate just checks each key
|
|
19
|
+
has a non-empty value, putting the burden on the engineer to write it
|
|
20
|
+
down. The values land in task metadata and become the audit trail.
|
|
21
|
+
|
|
22
|
+
Why this gate exists
|
|
23
|
+
--------------------
|
|
24
|
+
Multiple incidents in the last quarter where work was reported as
|
|
25
|
+
"done" once a PR merged, but the deploy regressed silently or the
|
|
26
|
+
behaviour was never verified. The mere act of having to write down
|
|
27
|
+
*proof of deploy + monitor + verify* eliminates the most common
|
|
28
|
+
class of "done" lies.
|
|
29
|
+
|
|
30
|
+
Bypass
|
|
31
|
+
------
|
|
32
|
+
Set the `metadata.shipped.bypass_reason` to a non-empty justification
|
|
33
|
+
when a task legitimately cannot be verified end-to-end (docs-only PR,
|
|
34
|
+
research spike, internal refactor with no deployable surface). The
|
|
35
|
+
bypass still persists into task metadata so the audit trail captures
|
|
36
|
+
*why* the gate was waived. Do not use bypass to dodge real verification
|
|
37
|
+
on changes with a deployable surface.
|
|
38
|
+
"""
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import sys
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
45
|
+
|
|
46
|
+
from _common import emit, parse_cli # noqa: E402
|
|
47
|
+
|
|
48
|
+
REQUIRED_EVIDENCE_KEYS = ("merged", "deployed", "monitored", "verified")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _example_payload() -> str:
|
|
52
|
+
return (
|
|
53
|
+
" metadata = {\n"
|
|
54
|
+
' "shipped": {\n'
|
|
55
|
+
' "merged": "PR #1234 merged at SHA abc1234",\n'
|
|
56
|
+
' "deployed": "cd-frontend-multicloud run 25281634698 success",\n'
|
|
57
|
+
' "monitored": "OO logs 14:00-14:15 UTC: 0 new errors on app.pay2u.com.au",\n'
|
|
58
|
+
' "verified": "curl https://app.pay2u.com.au/feature returned 200 with expected payload"\n'
|
|
59
|
+
" }\n"
|
|
60
|
+
" }"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main() -> None:
|
|
65
|
+
op, args = parse_cli()
|
|
66
|
+
|
|
67
|
+
# Only fire on attempts to mark a task `completed`.
|
|
68
|
+
if op not in {"TaskUpdate", "TaskCreate"}:
|
|
69
|
+
emit(True, "ship-loop-gate: not a task-status operation")
|
|
70
|
+
|
|
71
|
+
status = (args.get("status") or "").lower()
|
|
72
|
+
if status != "completed":
|
|
73
|
+
emit(True, "ship-loop-gate: not a completion update")
|
|
74
|
+
|
|
75
|
+
metadata = args.get("metadata") or {}
|
|
76
|
+
if not isinstance(metadata, dict):
|
|
77
|
+
emit(False, "ship-loop-gate: metadata must be an object containing `shipped` evidence")
|
|
78
|
+
|
|
79
|
+
shipped = metadata.get("shipped")
|
|
80
|
+
if not isinstance(shipped, dict):
|
|
81
|
+
emit(
|
|
82
|
+
False,
|
|
83
|
+
(
|
|
84
|
+
"ship-loop-gate: cannot mark task completed — no `metadata.shipped` "
|
|
85
|
+
"evidence block. Add proof of merge + deploy + monitor + verify before "
|
|
86
|
+
"claiming Done. Example:\n"
|
|
87
|
+
f"{_example_payload()}"
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
bypass = (shipped.get("bypass_reason") or "").strip()
|
|
92
|
+
if bypass:
|
|
93
|
+
emit(True, f"ship-loop-gate: bypassed with reason: {bypass}")
|
|
94
|
+
|
|
95
|
+
missing = [k for k in REQUIRED_EVIDENCE_KEYS if not str(shipped.get(k) or "").strip()]
|
|
96
|
+
if missing:
|
|
97
|
+
emit(
|
|
98
|
+
False,
|
|
99
|
+
(
|
|
100
|
+
"ship-loop-gate: cannot mark task completed — missing evidence for: "
|
|
101
|
+
f"{', '.join(missing)}. Each key under metadata.shipped must be a "
|
|
102
|
+
"non-empty string describing what happened in that phase. Use "
|
|
103
|
+
"`metadata.shipped.bypass_reason` only when the task has no "
|
|
104
|
+
"deployable surface (docs / research / pure refactor). Example:\n"
|
|
105
|
+
f"{_example_payload()}"
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
emit(True, "ship-loop-gate: all four phases (merged, deployed, monitored, verified) attested")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
main()
|
|
@@ -13,7 +13,8 @@ Allowed targets:
|
|
|
13
13
|
* anything under the current working tree (UAP_WORKTREE_ROOT) or the main
|
|
14
14
|
checkout (UAP_REPO_ROOT) — worktrees included;
|
|
15
15
|
* relative paths (they resolve under the project root);
|
|
16
|
-
* a scratch allow-list: /tmp, $TMPDIR, ~/.cache/uap, ~/.config/uap,
|
|
16
|
+
* a scratch allow-list: /tmp, $TMPDIR, ~/.cache/uap, ~/.config/uap,
|
|
17
|
+
~/.claude/projects (Claude Code auto-memory/session storage), plus any
|
|
17
18
|
colon-separated prefixes in UAP_WORKDIR_ALLOW.
|
|
18
19
|
|
|
19
20
|
Escape hatch: UAP_WORKDIR_SCOPE_OFF=1 allows everything (operator override).
|
|
@@ -58,7 +59,17 @@ def _allowed_roots() -> list[Path]:
|
|
|
58
59
|
|
|
59
60
|
add(worktree_root())
|
|
60
61
|
add(repo_root())
|
|
61
|
-
|
|
62
|
+
# ~/.claude/projects is the Claude Code harness's own storage (auto-memory
|
|
63
|
+
# topic files, MEMORY.md index, session/transcript data). The harness
|
|
64
|
+
# instructs agents to persist memories there; blocking it silently breaks
|
|
65
|
+
# memory recording (observed on pay2u 2026-07-05).
|
|
66
|
+
for p in (
|
|
67
|
+
"/tmp",
|
|
68
|
+
os.environ.get("TMPDIR", "/tmp"),
|
|
69
|
+
"~/.cache/uap",
|
|
70
|
+
"~/.config/uap",
|
|
71
|
+
"~/.claude/projects",
|
|
72
|
+
):
|
|
62
73
|
add(_expand(p))
|
|
63
74
|
for p in os.environ.get("UAP_WORKDIR_ALLOW", "").split(":"):
|
|
64
75
|
if p.strip():
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# adr-guard
|
|
2
|
+
|
|
3
|
+
**Category**: architecture
|
|
4
|
+
**Level**: RECOMMENDED
|
|
5
|
+
**Enforcement Stage**: pre-exec
|
|
6
|
+
**Tags**: adr, architecture, invariants, write-gate
|
|
7
|
+
|
|
8
|
+
## Rule
|
|
9
|
+
|
|
10
|
+
Write/Edit tool calls MUST satisfy every machine-readable rule block declared
|
|
11
|
+
in the project's ADRs. An ADR under `docs/adr/` may embed a `<!-- uap-rules
|
|
12
|
+
... -->` HTML comment declaring `forbid` / `require` regexes scoped by path
|
|
13
|
+
prefix, exclusion substring, and extension. A write whose content violates a
|
|
14
|
+
matching rule is **blocked** with the ADR's own message.
|
|
15
|
+
|
|
16
|
+
## Why
|
|
17
|
+
|
|
18
|
+
Architectural and security invariants decided in an ADR decay unless they are
|
|
19
|
+
enforced where the regression would be written. Declaring the rule inside the
|
|
20
|
+
ADR itself keeps a single source of truth: accept the decision, and the gate
|
|
21
|
+
comes with it. The policy is inert in projects whose ADRs declare no rule
|
|
22
|
+
blocks.
|
|
23
|
+
|
|
24
|
+
## Enforcement
|
|
25
|
+
|
|
26
|
+
Python enforcer `adr_guard.py` parses `uap-rules` blocks from every ADR under
|
|
27
|
+
`docs/adr/` (cached per mtime) and evaluates each Write/Edit target/content
|
|
28
|
+
against the matching rules. Rule fields: `id`, `scope.include/exclude/ext`,
|
|
29
|
+
`forbid` (regex must NOT match), `require` (regex MUST match), `message`.
|
|
30
|
+
|
|
31
|
+
```rules
|
|
32
|
+
- title: "Writes must satisfy the machine-readable rule blocks embedded in ADRs"
|
|
33
|
+
keywords: [write, edit, adr, invariant, forbid, require]
|
|
34
|
+
antiPatterns: []
|
|
35
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# bearer-lockdown
|
|
2
|
+
|
|
3
|
+
**Category**: security
|
|
4
|
+
**Level**: REQUIRED
|
|
5
|
+
**Enforcement Stage**: pre-exec
|
|
6
|
+
**Tags**: auth, cookie-only, bearer, security, write-gate
|
|
7
|
+
|
|
8
|
+
## Rule
|
|
9
|
+
|
|
10
|
+
Write/Edit tool calls MUST NOT reintroduce any cookie-only-frontend
|
|
11
|
+
(ADR-0007-style) invariant violation: frontend code setting
|
|
12
|
+
`Authorization: Bearer`, token keys in `localStorage`/`sessionStorage`,
|
|
13
|
+
`--skip-jwt-bearer-tokens=true` on oauth2-proxy, backend reading the client
|
|
14
|
+
`Authorization` header, `/auth/me` emitting a raw user id instead of an opaque
|
|
15
|
+
ref, or a committed RS256 JWT. Violating writes are **blocked** at tool-call
|
|
16
|
+
time.
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
A cookie-only auth architecture is one careless write away from regression,
|
|
21
|
+
and CI-time scanning catches it only after the agent has already built on top
|
|
22
|
+
of the mistake. Gating at write time prevents the regression from ever
|
|
23
|
+
entering the tree. Mirrors the originating project's bearer-lockdown CI scan.
|
|
24
|
+
|
|
25
|
+
## Enforcement
|
|
26
|
+
|
|
27
|
+
Python enforcer `bearer_lockdown.py` applies per-invariant regex checks scoped
|
|
28
|
+
by path (frontend trees, infra, backend). Path prefixes default to the
|
|
29
|
+
originating project's layout (`apps/web/`, `apps/cms/`, `apps/marketing/`,
|
|
30
|
+
`apps/api/`, `infra/`) — adjust the `path_include`/`path_exclude` tuples when
|
|
31
|
+
installing into a differently-shaped repo. Runtime-only invariants (e.g.
|
|
32
|
+
"Istio DENY policy alive") stay with cluster tooling, not this gate.
|
|
33
|
+
|
|
34
|
+
```rules
|
|
35
|
+
- title: "No write may reintroduce a bearer-token/cookie-only-frontend regression"
|
|
36
|
+
keywords: [write, edit, authorization, bearer, localStorage, jwt, oauth2-proxy]
|
|
37
|
+
antiPatterns: ["Authorization: Bearer in frontend code", "token in localStorage"]
|
|
38
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# local-build-before-push
|
|
2
|
+
|
|
3
|
+
**Category**: quality
|
|
4
|
+
**Level**: RECOMMENDED
|
|
5
|
+
**Enforcement Stage**: pre-exec
|
|
6
|
+
**Tags**: build, push, ci, docker, compile-gate
|
|
7
|
+
|
|
8
|
+
## Rule
|
|
9
|
+
|
|
10
|
+
`git push` / `gh pr create` / `gh pr merge` are **blocked** while the local
|
|
11
|
+
branch contains compiled-source changes (default scope: the project's C++ API
|
|
12
|
+
tree) that have not passed a local Docker builder-stage compile. The pass
|
|
13
|
+
marker (`.uap/local-build-pass.txt`, written by the project's local-build
|
|
14
|
+
script) must record the current HEAD SHA.
|
|
15
|
+
|
|
16
|
+
## Why
|
|
17
|
+
|
|
18
|
+
The CI compile gate can take 10-50 minutes; the identical local Docker build
|
|
19
|
+
takes 2-5 minutes cached. Pushing unverified compiled-source changes burns CI
|
|
20
|
+
cycles on errors a local build catches immediately. The marker-ties-to-HEAD
|
|
21
|
+
design means "built something earlier" never satisfies the gate — only the
|
|
22
|
+
exact tree being pushed.
|
|
23
|
+
|
|
24
|
+
## Enforcement
|
|
25
|
+
|
|
26
|
+
Python enforcer `local_build_before_push.py` intercepts push/PR Bash commands,
|
|
27
|
+
diffs the branch against the base, and blocks when watched paths (default
|
|
28
|
+
`apps/api/` sources/CMake/Dockerfiles — adjust per project) changed without a
|
|
29
|
+
matching pass marker. Bypass: `UAP_SKIP_LOCAL_BUILD=1` (session env — justify
|
|
30
|
+
in the commit message). NOTE: the enforcer diffs against the LOCAL base ref;
|
|
31
|
+
keep local main fast-forwarded or already-merged changes read as phantom
|
|
32
|
+
diffs.
|
|
33
|
+
|
|
34
|
+
```rules
|
|
35
|
+
- title: "Compiled-source changes must pass a local Docker build before push/PR"
|
|
36
|
+
keywords: [git push, gh pr create, gh pr merge, docker, build, compile]
|
|
37
|
+
antiPatterns: ["push without local build verification"]
|
|
38
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# ship-loop-gate
|
|
2
|
+
|
|
3
|
+
**Category**: process
|
|
4
|
+
**Level**: REQUIRED
|
|
5
|
+
**Enforcement Stage**: pre-exec
|
|
6
|
+
**Tags**: ship-loop, task, completion, evidence, merge-deploy-monitor-verify
|
|
7
|
+
|
|
8
|
+
## Rule
|
|
9
|
+
|
|
10
|
+
A task MUST NOT be marked `completed` without evidence for all four ship-loop
|
|
11
|
+
phases under `metadata.shipped`: `merged` (PR URL / merge SHA on main),
|
|
12
|
+
`deployed` (deploy run URL), `monitored` (log/metric window + result), and
|
|
13
|
+
`verified` (behavioural assertion against the deployed change). `TaskUpdate`
|
|
14
|
+
setting `status=completed` — or `TaskCreate` landing directly in `completed` —
|
|
15
|
+
without all four non-empty keys is **blocked**.
|
|
16
|
+
|
|
17
|
+
## Why
|
|
18
|
+
|
|
19
|
+
"Merged" is not "done": deploys regress silently and features ship dark.
|
|
20
|
+
Requiring written evidence per phase turns the merge -> deploy -> monitor ->
|
|
21
|
+
verify loop from a convention into a contract, and the recorded values become
|
|
22
|
+
the audit trail. Companion to the `merge-deploy-monitor-verify` policy — this
|
|
23
|
+
is its enforcement point at task-completion time.
|
|
24
|
+
|
|
25
|
+
## Enforcement
|
|
26
|
+
|
|
27
|
+
Python enforcer `ship_loop_gate.py` inspects TaskUpdate/TaskCreate arguments
|
|
28
|
+
and rejects completion without the four evidence keys, echoing an example of
|
|
29
|
+
the expected `metadata.shipped` shape in the denial message.
|
|
30
|
+
|
|
31
|
+
```rules
|
|
32
|
+
- title: "Task completion requires merged/deployed/monitored/verified evidence"
|
|
33
|
+
keywords: [taskupdate, taskcreate, completed, shipped, evidence]
|
|
34
|
+
antiPatterns: ["marking a task completed with no ship evidence"]
|
|
35
|
+
```
|
|
@@ -13,8 +13,11 @@ command whose create/move destination (`mkdir`, `touch`, `cp`, `mv`, `install`,
|
|
|
13
13
|
`tee`, output redirection) — resolves OUTSIDE the working tree is **blocked**.
|
|
14
14
|
|
|
15
15
|
In scope (allowed): the current working tree and main checkout (worktrees
|
|
16
|
-
included), relative paths,
|
|
17
|
-
|
|
16
|
+
included), relative paths, `/dev/*` device sinks (`2>/dev/null` is not a
|
|
17
|
+
write), and a scratch allow-list (`/tmp`, `$TMPDIR`, `~/.cache/uap`,
|
|
18
|
+
`~/.config/uap`, `~/.claude/projects` — the Claude Code harness's own
|
|
19
|
+
auto-memory/session storage, which agents are instructed to write to — plus
|
|
20
|
+
`UAP_WORKDIR_ALLOW` prefixes).
|
|
18
21
|
|
|
19
22
|
## Why
|
|
20
23
|
|
|
Binary file
|
|
@@ -76,6 +76,16 @@ class TestWorkdirScopeEnforcer(unittest.TestCase):
|
|
|
76
76
|
out, c = run("Write", {"file_path": "/tmp/uap-scratch/x"}, self.root)
|
|
77
77
|
self._allow(out, c, "/tmp scratch")
|
|
78
78
|
|
|
79
|
+
def test_claude_memory_dir_allowed(self):
|
|
80
|
+
# ~/.claude/projects is the Claude Code harness's auto-memory/session
|
|
81
|
+
# storage; the harness instructs agents to write memories there.
|
|
82
|
+
out, c = run(
|
|
83
|
+
"Write",
|
|
84
|
+
{"file_path": str(Path.home() / ".claude/projects/-x-proj/memory/note.md")},
|
|
85
|
+
self.root,
|
|
86
|
+
)
|
|
87
|
+
self._allow(out, c, "claude memory dir")
|
|
88
|
+
|
|
79
89
|
def test_worktree_path_allowed(self):
|
|
80
90
|
out, c = run("Write", {"file_path": str(self.root / ".worktrees/001-x/f.ts")}, self.root)
|
|
81
91
|
self._allow(out, c, "worktree path")
|
package/web/dashboard.html
CHANGED
|
@@ -578,6 +578,23 @@
|
|
|
578
578
|
</div>
|
|
579
579
|
</div>
|
|
580
580
|
|
|
581
|
+
<!-- Token Savings by Influence -->
|
|
582
|
+
<div class="full-row" id="savings-panel">
|
|
583
|
+
<div class="panel">
|
|
584
|
+
<h2>Token Savings by Influence</h2>
|
|
585
|
+
<div id="savings-summary" style="margin-bottom:12px;font-size:14px"></div>
|
|
586
|
+
<div id="savings-table" class="table-wrap"><div class="empty">No savings data</div></div>
|
|
587
|
+
</div>
|
|
588
|
+
</div>
|
|
589
|
+
|
|
590
|
+
<!-- Orchestrations & Hierarchy -->
|
|
591
|
+
<div class="full-row" id="orch-panel">
|
|
592
|
+
<div class="panel">
|
|
593
|
+
<h2>Orchestrations & Hierarchy <span style="font-size:11px;color:var(--fg3);font-weight:400" id="orch-label"></span></h2>
|
|
594
|
+
<div id="orch-tree"><div class="empty">No orchestrations yet</div></div>
|
|
595
|
+
</div>
|
|
596
|
+
</div>
|
|
597
|
+
|
|
581
598
|
<!-- Task Board -->
|
|
582
599
|
<div class="full-row" id="kanban-panel" style="display:none">
|
|
583
600
|
<div class="panel">
|
|
@@ -1107,6 +1124,8 @@
|
|
|
1107
1124
|
});
|
|
1108
1125
|
|
|
1109
1126
|
safe('kanban', () => renderKanban(tasks));
|
|
1127
|
+
safe('savings', () => renderSavings(data.savingsByInfluence));
|
|
1128
|
+
safe('orchestration', () => renderOrchestration(data.orchestrationTree));
|
|
1110
1129
|
|
|
1111
1130
|
safe('performance', () => {
|
|
1112
1131
|
const hp = (data.performance||{}).hotPaths || [];
|
|
@@ -1135,6 +1154,60 @@
|
|
|
1135
1154
|
const TYPE_ICONS = {task:'\u25C6',bug:'\uD83D\uDC1B',feature:'\u2728',epic:'\uD83C\uDFAF',chore:'\uD83D\uDD27',story:'\uD83D\uDCD6'};
|
|
1136
1155
|
const PRIO_LABELS = ['P0','P1','P2','P3','P4'];
|
|
1137
1156
|
let prevCardMap = new Map();
|
|
1157
|
+
function renderSavings(sv) {
|
|
1158
|
+
sv = sv || { influences: [], totalTokensSaved: 0, totalCostSavedUsd: 0 };
|
|
1159
|
+
const badge = (q) => {
|
|
1160
|
+
const c = q === 'measured' ? '#3fb950' : q === 'estimated' ? '#d29922' : '#484f58';
|
|
1161
|
+
return `<span style="color:${c};font-size:10px;text-transform:uppercase;border:1px solid ${c};border-radius:3px;padding:1px 5px">${q}</span>`;
|
|
1162
|
+
};
|
|
1163
|
+
setHTML('savings-summary',
|
|
1164
|
+
`<strong style="color:#58a6ff;font-size:18px">${fmt(sv.totalTokensSaved)}</strong> tokens · ` +
|
|
1165
|
+
`<strong style="color:#3fb950;font-size:18px">$${(sv.totalCostSavedUsd||0).toFixed(2)}</strong> saved across all UAP influences`);
|
|
1166
|
+
if (!sv.influences.length) { setHTML('savings-table', '<div class="empty">No savings data</div>'); return; }
|
|
1167
|
+
const rows = sv.influences.map(i =>
|
|
1168
|
+
`<tr><td>${esc(i.influence)}</td><td style="text-align:right">${fmt(i.tokensSaved)}</td>` +
|
|
1169
|
+
`<td style="text-align:right;color:#3fb950">$${(i.costSavedUsd||0).toFixed(4)}</td>` +
|
|
1170
|
+
`<td>${badge(i.quality)}</td><td style="color:var(--fg3);font-size:11px">${esc(i.detail||'')}</td></tr>`).join('');
|
|
1171
|
+
setHTML('savings-table',
|
|
1172
|
+
`<table><thead><tr><th>Influence</th><th style="text-align:right">Tokens saved</th>` +
|
|
1173
|
+
`<th style="text-align:right">Cost saved</th><th>Quality</th><th>Detail</th></tr></thead><tbody>${rows}</tbody></table>`);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function orchNode(n, depth) {
|
|
1177
|
+
const pad = depth * 16;
|
|
1178
|
+
const sc = { done:'#3fb950', in_progress:'#58a6ff', blocked:'#f85149', failed:'#f85149', open:'#8b949e', pending:'#8b949e' }[n.status] || '#8b949e';
|
|
1179
|
+
const icon = n.type === 'epic' ? '🎯' : n.type === 'feature' ? '✨' : n.type === 'phase' ? '▶' : '•';
|
|
1180
|
+
const agents = (n.agents && n.agents.length) ? ` <span style="color:#a371f7;font-size:10px">[${n.agents.join(', ')}]</span>` : '';
|
|
1181
|
+
let html = `<div style="padding:3px 0 3px ${pad}px;border-left:1px solid var(--border)">` +
|
|
1182
|
+
`<span style="color:${sc}">●</span> ${icon} <span>${esc((n.title||n.id).slice(0,70))}</span>` +
|
|
1183
|
+
` <span style="color:var(--fg3);font-size:10px">${n.status}</span>${agents}</div>`;
|
|
1184
|
+
for (const c of (n.children||[])) html += orchNode(c, depth + 1);
|
|
1185
|
+
return html;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function renderOrchestration(ot) {
|
|
1189
|
+
ot = ot || { missions: [], ledger: null, agents: [], hasHierarchy: false };
|
|
1190
|
+
setText('orch-label', `${ot.missions.length} root(s) · ${ot.agents.length} agent(s)`);
|
|
1191
|
+
let html = '';
|
|
1192
|
+
if (ot.ledger) {
|
|
1193
|
+
const L = ot.ledger;
|
|
1194
|
+
html += `<div style="margin-bottom:14px;padding:10px;background:var(--bg2);border-radius:6px">` +
|
|
1195
|
+
`<div style="font-weight:600;margin-bottom:6px">Active build: ${L.mission.slice(0,80)} ` +
|
|
1196
|
+
`<span style="color:#58a6ff">${L.done}/${L.total} (${L.pct}%)</span></div>`;
|
|
1197
|
+
html += L.items.map(it => {
|
|
1198
|
+
const sc = { done:'#3fb950', in_progress:'#58a6ff', failed:'#f85149', pending:'#8b949e' }[it.status] || '#8b949e';
|
|
1199
|
+
const dep = it.deps && it.deps.length ? ` <span style="color:var(--fg3);font-size:10px">⇠ ${it.deps.join(',')}</span>` : '';
|
|
1200
|
+
return `<div style="padding:2px 0"><span style="color:${sc}">●</span> ${it.kind==='epic'?'🎯':'•'} ${esc(it.title.slice(0,70))} <span style="color:var(--fg3);font-size:10px">${it.status}</span>${dep}</div>`;
|
|
1201
|
+
}).join('');
|
|
1202
|
+
html += `</div>`;
|
|
1203
|
+
}
|
|
1204
|
+
const withKids = ot.missions.filter(m => (m.children||[]).length > 0);
|
|
1205
|
+
const shown = (withKids.length ? withKids : ot.missions).slice(0, 20);
|
|
1206
|
+
if (!ot.ledger && shown.length === 0) { setHTML('orch-tree', '<div class="empty">No orchestrations yet — run an epic/orchestrated build</div>'); return; }
|
|
1207
|
+
html += shown.map(m => orchNode(m, 0)).join('');
|
|
1208
|
+
setHTML('orch-tree', html);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1138
1211
|
function renderKanban(tasks) {
|
|
1139
1212
|
const items=tasks.items||[], cols={open:[],in_progress:[],blocked:[],done:[],wont_do:[]}, ncm=new Map();
|
|
1140
1213
|
for (const i of items) { if(cols[i.status]) cols[i.status].push(i); ncm.set(i.id,i.status); }
|
|
@@ -1159,6 +1232,7 @@
|
|
|
1159
1232
|
|
|
1160
1233
|
// ── Helpers ──
|
|
1161
1234
|
function setText(id,val){const e=document.getElementById(id);if(e)e.textContent=String(val);}
|
|
1235
|
+
function setHTML(id,html){const e=document.getElementById(id);if(e)e.innerHTML=html;}
|
|
1162
1236
|
function fmt(n){if(typeof n!=='number'||isNaN(n))return'0';return Math.round(n).toLocaleString();}
|
|
1163
1237
|
function capitalize(s){return s?s.charAt(0).toUpperCase()+s.slice(1):'';}
|
|
1164
1238
|
const MODEL_NAMES={'opus-4.6':'Claude Opus 4.6','sonnet-4.6':'Claude Sonnet 4.6','gpt-5.4':'GPT 5.4','gpt-5.3-codex':'GPT 5.3 Codex','qwen35':'Qwen 3.5 35B A3B','qwen35-a3b':'Qwen 3.5 35B A3B'};
|