@topolo/cli 0.13.19 → 0.13.22
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/agent-packs/README.md +421 -1
- package/agent-packs/docs/selected-artifacts.md +47 -0
- package/agent-packs/documentation/cloud-task-state.md +56 -0
- package/agent-packs/domain-contexts/README.md +25 -0
- package/agent-packs/domain-contexts/cicd.json +8 -0
- package/agent-packs/gui/feed-adapter.js +4 -0
- package/agent-packs/gui/index.html +9 -0
- package/agent-packs/gui/studio.css +14 -0
- package/agent-packs/gui/studio.js +121 -0
- package/agent-packs/gui/studio_server.py +355 -0
- package/agent-packs/roles/cicd/CLAUDE.md +24 -0
- package/agent-packs/specializations/cicd--cicd.json +5 -0
- package/agent-packs/specializations/cicd--pipeline-engineering.json +5 -0
- package/agent-packs/specializations/cicd--promotion-deployment.json +5 -0
- package/agent-packs/specializations/cicd--queue-recovery.json +5 -0
- package/agent-packs/specializations/cicd--release-verification.json +5 -0
- package/agent-packs/specializations/cicd--runner-capacity.json +5 -0
- package/agent-packs/specializations/intakes--engineering-02.json +5 -0
- package/agent-packs/specializations/intakes--engineering-03.json +5 -0
- package/agent-packs/specializations/intakes--engineering.json +5 -0
- package/agent-packs/specializations/intakes--operator-01.json +5 -0
- package/agent-packs/specializations/intakes--operator-02.json +5 -0
- package/agent-packs/specializations/operator-skills--audio-and-narration-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--brand-and-copywriting-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--capability-research-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--data-analysis-and-visualization.json +5 -0
- package/agent-packs/specializations/operator-skills--financial-evidence-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--operating-template-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--operator-skill-library.json +5 -0
- package/agent-packs/specializations/operator-skills--operator-skills.json +5 -0
- package/agent-packs/specializations/operator-skills--operator-workflow-evaluation.json +5 -0
- package/agent-packs/specializations/operator-skills--privacy-and-agreement-review.json +5 -0
- package/agent-packs/specializations/operator-skills--storytelling-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--ux-and-accessibility-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--video-production-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--visual-design-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--workflow-integration-specialist.json +5 -0
- package/agent-packs/specializations/operators--access-and-privacy.json +5 -0
- package/agent-packs/specializations/operators--accounting-evidence-review.json +5 -0
- package/agent-packs/specializations/operators--agreement-operations.json +5 -0
- package/agent-packs/specializations/operators--books-operator.json +5 -0
- package/agent-packs/specializations/operators--brand-operator.json +5 -0
- package/agent-packs/specializations/operators--commerce-operations.json +5 -0
- package/agent-packs/specializations/operators--communications-operator.json +5 -0
- package/agent-packs/specializations/operators--content-and-assets-operator.json +5 -0
- package/agent-packs/specializations/operators--customer-operations.json +5 -0
- package/agent-packs/specializations/operators--design-operator.json +5 -0
- package/agent-packs/specializations/operators--design-production-02.json +5 -0
- package/agent-packs/specializations/operators--design-production-03.json +5 -0
- package/agent-packs/specializations/operators--design-production-04.json +5 -0
- package/agent-packs/specializations/operators--device-and-feed-operator.json +5 -0
- package/agent-packs/specializations/operators--director-operator.json +5 -0
- package/agent-packs/specializations/operators--experience-review.json +5 -0
- package/agent-packs/specializations/operators--growth-operations.json +5 -0
- package/agent-packs/specializations/operators--learn-operator.json +5 -0
- package/agent-packs/specializations/operators--localization-operator.json +5 -0
- package/agent-packs/specializations/operators--operators.json +5 -0
- package/agent-packs/specializations/operators--pay-operator.json +5 -0
- package/agent-packs/specializations/operators--research-and-intelligence.json +5 -0
- package/agent-packs/specializations/operators--roadmap-operator.json +5 -0
- package/agent-packs/specializations/operators--social-content-operator.json +5 -0
- package/agent-packs/specializations/operators--talent-operations.json +5 -0
- package/agent-packs/specializations/operators--web-operator.json +5 -0
- package/agent-packs/specializations/operators--workflow-operator.json +5 -0
- package/agent-packs/specializations/specialists--adversarial-qa.json +5 -0
- package/agent-packs/specializations/specialists--backlog-dispatch.json +5 -0
- package/agent-packs/specializations/specialists--build-01.json +5 -0
- package/agent-packs/specializations/specialists--build-02.json +5 -0
- package/agent-packs/specializations/specialists--build-03.json +5 -0
- package/agent-packs/specializations/specialists--developer-environment-steward.json +5 -0
- package/agent-packs/specializations/specialists--documentation-knowledge-steward.json +5 -0
- package/agent-packs/specializations/specialists--execution-readiness.json +5 -0
- package/agent-packs/specializations/specialists--incident-triage.json +5 -0
- package/agent-packs/specializations/specialists--platform-package-steward.json +5 -0
- package/agent-packs/specializations/specialists--product-direction-01.json +5 -0
- package/agent-packs/specializations/specialists--product-direction-02.json +5 -0
- package/agent-packs/specializations/specialists--release-recovery.json +5 -0
- package/agent-packs/specializations/specialists--release-watch.json +5 -0
- package/agent-packs/specializations/specialists--roadmap-planning-01.json +5 -0
- package/agent-packs/specializations/specialists--roadmap-planning-02.json +5 -0
- package/agent-packs/specializations/specialists--security-permissions-review.json +5 -0
- package/agent-packs/specializations/specialists--system-architecture-01.json +5 -0
- package/agent-packs/specializations/specialists--system-architecture-02.json +5 -0
- package/agent-packs/specializations/specialists--translation.json +5 -0
- package/agent-packs/specializations/specialists--ux-consistency-review.json +5 -0
- package/agent-packs/specializations/specialists--workflow-improvement.json +5 -0
- package/agent-packs/specializations/specialists--worktree-retirement.json +5 -0
- package/agent-packs/src/topolo_agent_pack/cicd.py +101 -0
- package/agent-packs/src/topolo_agent_pack/claude.py +22 -7
- package/agent-packs/src/topolo_agent_pack/claude_activation.py +157 -0
- package/agent-packs/src/topolo_agent_pack/claude_completion_recovery.py +216 -0
- package/agent-packs/src/topolo_agent_pack/claude_desktop.py +181 -0
- package/agent-packs/src/topolo_agent_pack/claude_dispatch.py +240 -0
- package/agent-packs/src/topolo_agent_pack/claude_prompt.py +74 -0
- package/agent-packs/src/topolo_agent_pack/claude_runner.py +207 -0
- package/agent-packs/src/topolo_agent_pack/claude_service.py +397 -0
- package/agent-packs/src/topolo_agent_pack/cli.py +181 -16
- package/agent-packs/src/topolo_agent_pack/closed_ledger_fence.py +118 -0
- package/agent-packs/src/topolo_agent_pack/cloud_attach.py +128 -0
- package/agent-packs/src/topolo_agent_pack/cloud_cutover.py +134 -0
- package/agent-packs/src/topolo_agent_pack/cloud_delta.py +162 -0
- package/agent-packs/src/topolo_agent_pack/cloud_migrate.py +142 -0
- package/agent-packs/src/topolo_agent_pack/cloud_recovery.py +42 -0
- package/agent-packs/src/topolo_agent_pack/cloud_work.py +126 -0
- package/agent-packs/src/topolo_agent_pack/coordinator_activity.py +27 -0
- package/agent-packs/src/topolo_agent_pack/coordinator_context.py +91 -0
- package/agent-packs/src/topolo_agent_pack/coordinator_refresh.py +116 -0
- package/agent-packs/src/topolo_agent_pack/desktop_acceptance.py +45 -0
- package/agent-packs/src/topolo_agent_pack/desktop_launcher.py +114 -0
- package/agent-packs/src/topolo_agent_pack/domain_context.py +38 -0
- package/agent-packs/src/topolo_agent_pack/execution.py +103 -6
- package/agent-packs/src/topolo_agent_pack/frontdoor.py +23 -0
- package/agent-packs/src/topolo_agent_pack/linked_task_context.py +80 -0
- package/agent-packs/src/topolo_agent_pack/live.py +285 -18
- package/agent-packs/src/topolo_agent_pack/native.py +160 -4
- package/agent-packs/src/topolo_agent_pack/review_context.py +33 -0
- package/agent-packs/src/topolo_agent_pack/role_catalog.py +137 -0
- package/agent-packs/src/topolo_agent_pack/role_context.py +63 -0
- package/agent-packs/src/topolo_agent_pack/role_refresh.py +50 -0
- package/agent-packs/src/topolo_agent_pack/topolo.py +7 -1
- package/agent-packs/src/topolo_agent_pack/work.py +322 -0
- package/agent-packs/src/topolo_agent_pack/work_adoption.py +122 -0
- package/agent-packs/src/topolo_agent_pack/work_artifacts.py +84 -0
- package/agent-packs/src/topolo_agent_pack/work_context.py +77 -0
- package/agent-packs/src/topolo_agent_pack/work_gui.py +51 -0
- package/agent-packs/src/topolo_agent_pack/work_native.py +414 -0
- package/agent-packs/src/topolo_agent_pack/work_wait.py +151 -0
- package/agent-packs/templates/topolo-coordinators/AGENTS.md +7 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/cicd/AGENTS.md +6 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/cicd/config.json +4 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/engineering/AGENTS.md +3 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/engineering/config.json +4 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/operator-01/AGENTS.md +3 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/operator-01/config.json +4 -0
- package/agent-packs/templates/topolo-coordinators/pack.json +56 -0
- package/dist/chunk-NYDIAYVY.js +144 -0
- package/dist/{claude-pack-backend-YEVPAOID.js → chunk-UY5LEU5C.js} +1659 -21
- package/dist/claude-desktop-profile-JCQKSY3K.js +13 -0
- package/dist/claude-pack-activation-Q25MCEPU.js +506 -0
- package/dist/claude-pack-backend-HUTKGIDS.js +90 -0
- package/dist/index.js +152 -73
- package/package.json +5 -5
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
:root{font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#17212e;background:#f6f8fa;font-synthesis:none;--line:#e4e9ee;--muted:#687789;--blue:#287b9d;--pale:#eaf4f8}*{box-sizing:border-box}body{margin:0}button,input,textarea,select{font:inherit}button,a{touch-action:manipulation}button{cursor:pointer}button:disabled{cursor:default;opacity:.45}button,a,input,textarea,select{outline-offset:4px}button:focus-visible,a:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:2px solid var(--blue)}button{border:1px solid var(--line);background:#fff;color:inherit;border-radius:9px;padding:10px 13px;transition:background .15s,transform .15s}button:hover:not(:disabled){background:#edf3f6}button:active:not(:disabled){transform:translateY(1px)}a{color:#246c8a;text-decoration:none}a:hover{text-decoration:underline}header{height:66px;background:#fff;display:flex;align-items:center;justify-content:space-between;padding:0 25px;border-bottom:1px solid var(--line);gap:12px;position:sticky;top:0;z-index:5}.wordmark{font-size:23px;font-weight:800;letter-spacing:-1.2px}.wordmark span{color:var(--blue)}.product{font-size:12px;color:var(--muted);letter-spacing:0;font-weight:500;border-left:1px solid var(--line);padding-left:13px;margin-left:13px}.head-actions{display:flex;align-items:center;gap:10px}.fresh{font-size:11px;color:var(--muted)}.icon-button{font-size:19px;padding:5px 10px}.navigation{background:#fff;display:flex;align-items:center;gap:4px;padding:0 20px;border-bottom:1px solid var(--line);overflow:auto}.navigation button{border:0;background:none;border-radius:0;white-space:nowrap;padding:16px 10px;font-size:12px;border-bottom:2px solid transparent;color:var(--muted)}.navigation button[aria-selected=true]{border-bottom-color:var(--blue);color:#1b627d;font-weight:650}.count{font-size:10px;display:inline-flex;align-items:center;justify-content:center;min-width:18px;margin-left:5px;padding:2px 5px;border-radius:5px;background:#f0f3f6;color:#657486}.navigation button[aria-selected=true] .count{background:var(--pale);color:#23667f}.notice{margin:12px 24px;background:#fff2df;border:1px solid #f0d5a8;border-radius:8px;padding:12px;font-size:12px}.workspace{display:grid;grid-template-columns:270px minmax(0,1fr);max-width:1600px;margin:auto;min-height:calc(100svh - 114px)}.queue{border-right:1px solid var(--line);padding:22px 15px;background:#f9fafb}.queue-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}.eyebrow{font-size:10px;font-weight:650;text-transform:uppercase;letter-spacing:1.4px;color:var(--blue)}.queue-heading .eyebrow{color:var(--muted)}.search{width:100%;padding:10px 12px;border:1px solid var(--line);border-radius:8px;font-size:12px;background:#fff}.filters{display:flex;gap:7px;margin:9px 0 16px}.filters select{width:50%;min-width:0;border:1px solid var(--line);border-radius:6px;background:#fff;color:var(--muted);padding:7px;font-size:11px}.queue-item{display:block;width:100%;text-align:left;border:1px solid transparent;background:transparent;padding:13px 12px;margin:3px 0}.queue-item.selected{background:#fff;border-color:#d4e3eb;box-shadow:0 2px 5px #16233305}.queue-item .name{display:block;font-size:13px;line-height:1.45;font-weight:620;margin:5px 0}.queue-item .small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.small{font-size:11px;color:var(--muted);line-height:1.5}.queue-item .state{font-size:10px;color:#526d7a}.dot{display:inline-block;width:5px;height:5px;border-radius:50%;background:#cc9348;margin-right:6px}.dot.done{background:#49967c}.content{min-width:0;padding:30px 32px 150px;background:#fff}.context-line{display:flex;justify-content:space-between;gap:10px;align-items:center}.pager{display:flex;gap:6px;align-items:center}.pager span{font-size:11px;color:var(--muted);margin-right:5px}.pager button{padding:4px 10px;font-size:20px}h1{font-size:clamp(24px,3vw,35px);line-height:1.16;letter-spacing:-1px;font-weight:660;margin:14px 0 10px;max-width:850px}.subtitle{font-size:13px;line-height:1.6;color:var(--muted);margin:0 0 23px;max-width:800px}.detail-layout{display:grid;grid-template-columns:minmax(0,1fr) 240px;gap:25px}.preview-area{min-width:0}.preview{height:min(52svh,530px);min-height:240px;background:#f3f5f7;border-radius:12px;border:1px solid var(--line);display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative}.preview img{height:100%;width:100%;object-fit:contain}.preview button{position:absolute;right:12px;bottom:12px;font-size:11px;box-shadow:0 2px 8px #17212e15}.preview-caption{display:flex;justify-content:space-between;gap:12px;font-size:11px;color:var(--muted);margin:12px 0}.thumbnails{display:flex;gap:9px;overflow:auto;padding:2px 2px 9px}.thumb{padding:3px;flex:0 0 95px;border-radius:7px;background:#f7f9fa}.thumb img{width:100%;height:68px;object-fit:cover;object-position:top;border-radius:4px}.thumb span{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10px;padding:5px 3px}.thumb[aria-pressed=true]{border:2px solid var(--blue);padding:2px}.inspector{border-left:1px solid var(--line);padding-left:23px}.inspector section{padding:0 0 18px;margin:0 0 19px;border-bottom:1px solid var(--line)}h2{font-size:12px;margin:0 0 10px;font-weight:650}.inspector p,.detail-copy{font-size:12px;line-height:1.7;color:var(--muted);margin:0}.owner{display:flex;align-items:center;gap:9px;margin-bottom:10px}.avatar{background:var(--pale);color:var(--blue);border-radius:50%;height:30px;width:30px;display:grid;place-items:center;font-size:10px;font-weight:600}.owner-name{font-size:12px;font-weight:600}.support-link{display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-bottom:1px solid #eff2f5;font-size:12px}.status-panel{padding:22px;border:1px solid var(--line);border-radius:10px;background:#fafcfd;min-height:250px}.status-panel h2{font-size:18px;line-height:1.5;letter-spacing:-.3px}.status-pill{font-size:10px;display:inline-block;padding:5px 8px;border-radius:5px;background:var(--pale);color:#23677f;margin-bottom:15px}.stage-track{display:flex;gap:6px;margin:24px 0;flex-wrap:wrap}.stage{font-size:10px;padding:7px 10px;background:#f0f3f6;color:var(--muted);border-radius:5px}.stage.pass{background:#eaf5ef;color:#36775b}.details{margin-top:20px;border-top:1px solid var(--line);padding-top:15px}.details summary{font-size:12px;color:#496575;cursor:pointer}.details p{font-size:12px;line-height:1.75;color:var(--muted)}pre{font:11px/1.6 ui-monospace,monospace;white-space:pre-wrap;word-break:break-word;background:#f6f8fa;padding:12px;border-radius:6px}.response-bar{position:fixed;bottom:0;right:0;left:270px;background:#ffffffef;backdrop-filter:blur(15px);border-top:1px solid var(--line);padding:14px 32px;z-index:4}.response-inner{max-width:1250px;margin:auto;display:flex;align-items:center;justify-content:space-between;gap:18px}.response-text{font-size:11px;color:var(--muted);max-width:400px;line-height:1.5}.response-actions{display:flex;gap:7px;flex-wrap:wrap}.primary{background:var(--blue);border-color:var(--blue);color:#fff}.primary:hover:not(:disabled){background:#226e8d}.quiet{background:transparent}.response-actions button{font-size:12px}.mobile-queue{display:none}.empty{padding:50px 20px;text-align:center;color:var(--muted);font-size:13px}.activity-row{border-bottom:1px solid var(--line);padding:16px 0}.activity-row strong{font-size:13px}.activity-row p{font-size:12px;color:var(--muted);line-height:1.6}.activity-row time{font-size:10px;color:var(--muted)}.table-wrap{overflow:auto}table{border-collapse:collapse;width:100%;font-size:12px}th,td{text-align:left;vertical-align:top;padding:14px 10px;border-bottom:1px solid var(--line);min-width:120px;line-height:1.6}th{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--muted)}dialog{width:min(520px,calc(100% - 30px));border:1px solid var(--line);border-radius:14px;padding:25px;box-shadow:0 25px 100px #15233330;color:inherit}dialog::backdrop{background:#17212e60;backdrop-filter:blur(3px)}dialog h2{font-size:21px;letter-spacing:-.5px}dialog p{font-size:12px;line-height:1.6;color:var(--muted)}dialog select,textarea{display:block;width:100%;padding:10px;border:1px solid #d8e1e8;border-radius:8px;margin:12px 0;font-size:13px}textarea{min-height:120px;resize:vertical}dialog .actions{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.toast{position:fixed;bottom:105px;left:50%;transform:translateX(-50%);background:#173e50;color:white;border-radius:8px;padding:12px 18px;font-size:12px;z-index:20;max-width:90%}.image-dialog{width:min(1000px,95vw);max-height:95svh;padding:15px}.image-dialog img{width:100%;height:78svh;object-fit:contain}.image-dialog .actions{margin:0 0 10px}.record{background:#eef6f2;padding:12px 15px;border-radius:8px;font-size:12px;line-height:1.65;margin-top:15px;color:#356b55}.intro{font-size:12px;color:var(--muted);line-height:1.6;margin:0 0 25px}.queue-item{animation:appear .15s ease-out}.focus-content{animation:appear .2s ease-out}@keyframes appear{from{opacity:.5;transform:translateY(3px)}to{opacity:1;transform:translateY(0)}}[hidden]{display:none!important}@media(min-width:1600px){.response-bar{left:max(270px,calc((100vw - 1600px)/2 + 270px));right:max(0px,calc((100vw - 1600px)/2))}}@media(max-width:1150px){.detail-layout{grid-template-columns:1fr}.inspector{border-left:0;padding-left:0;display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:25px}.content{padding:24px 25px 155px}.response-bar{padding:12px 25px}.workspace{grid-template-columns:235px minmax(0,1fr)}.response-bar{left:235px}}@media(max-width:850px){header{padding:0 20px;height:58px}.product{display:none}.workspace{display:block;min-height:calc(100svh - 108px)}.queue{display:none;position:fixed;inset:108px 0 0;z-index:6;overflow:auto;padding:18px 20px;border-right:0}.queue.open{display:block}.mobile-queue{display:inline-flex;gap:8px;align-items:center;font-size:11px;border-radius:20px;background:#f6f8fb}.content{padding:23px 22px 150px}.response-bar{left:0;padding:13px 22px}.response-inner{gap:10px}.response-text{max-width:250px}.preview{height:min(47svh,450px)}.fresh{display:none}.navigation{padding:0 12px}.navigation button{padding:15px 9px}.inspector{grid-template-columns:1fr 1fr}.queue-item .small{white-space:normal}.queue-item{padding:14px}.head-actions{gap:7px}}@media(max-width:480px){header{padding:0 15px}.wordmark{font-size:22px}.mobile-queue{max-width:170px;overflow:hidden;white-space:nowrap}.content{padding:22px 17px 170px}h1{font-size:27px;letter-spacing:-.7px}.subtitle{font-size:12px;margin-bottom:18px}.context-line{gap:6px}.eyebrow{font-size:9px;letter-spacing:1px}.inspector{display:block}.response-bar{padding:12px 17px}.response-inner{display:block}.response-text{max-width:none;margin-bottom:10px;font-size:10px}.response-actions{display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px}.response-actions button{padding:10px 8px;font-size:11px}.navigation button{font-size:11px;padding:14px 7px}.count{margin-left:3px}.preview{height:40svh;min-height:220px}.preview-caption{font-size:10px}.status-panel{padding:19px}.toast{bottom:140px}.pager span{display:none}}@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
|
2
|
+
|
|
3
|
+
.workspace.wide{display:block}.workspace.wide .content{min-height:calc(100svh - 114px)}
|
|
4
|
+
|
|
5
|
+
.status-panel .detail-copy,.inspector section>p{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4;overflow:hidden}.inspector .small{-webkit-line-clamp:2}
|
|
6
|
+
|
|
7
|
+
.workflow-progress{margin:18px 0;padding:18px;border:1px solid var(--line);border-radius:9px;font-size:12px;line-height:1.55}.workflow-progress h2{margin:0 0 12px;font-size:15px}.workflow-steps{list-style:none;margin:18px 0 0;padding:0}.workflow-steps li{border-left:2px solid #dce4e9;padding:0 0 18px 16px;position:relative}.workflow-steps li:before{content:"";position:absolute;left:-5px;top:5px;width:8px;height:8px;border-radius:50%;background:#cdd7de}.workflow-steps li.verified{border-color:#459078}.workflow-steps li.verified:before{background:#459078}.workflow-steps strong{display:inline-block;margin-right:12px}.workflow-steps p{margin:5px 0 0}.workflow-steps li:last-child{padding-bottom:0}
|
|
8
|
+
|
|
9
|
+
.view-guide{padding:14px 24px;border-bottom:1px solid var(--line);background:#fff}.view-guide p{margin:0;font-size:12px;line-height:1.65;color:#496575}.view-guide .coverage-note{margin-top:6px;color:#735a30}@media(max-width:480px){.view-guide{padding:12px 17px}.view-guide p{font-size:11px}}
|
|
10
|
+
|
|
11
|
+
/* Single-item workspace; the work finder is a dialog, never a sidebar. */
|
|
12
|
+
.workspace{display:block;max-width:1240px}.response-bar{left:0;right:0}.response-inner{max-width:1176px}.work-finder{margin-left:auto;white-space:nowrap;font-size:12px}.work-picker{width:min(600px,calc(100vw - 32px));max-height:82svh;padding:0;background:#fff;overflow:auto}.work-picker .picker-body{padding:22px}.work-picker .queue-heading{margin-bottom:18px}.work-picker h2{font-size:19px;margin:0}.work-picker .filters{margin-bottom:12px}.work-picker .filters select{margin:0;width:50%}.work-picker #picker-summary{margin:0 0 12px}.work-picker .queue-item{padding:12px 9px;margin:0;border-radius:0;border-bottom:1px solid var(--line);animation:none}.work-picker .queue-item.selected{box-shadow:none;border-color:var(--line);background:var(--pale)}.work-picker .queue-item .name{font-size:13px;margin:4px 0}.work-picker .queue-item .eyebrow{font-size:9px}.work-picker[open]{display:block}.head-actions{flex-shrink:0}
|
|
13
|
+
@media(max-width:850px){.workspace{display:block}.work-finder{font-size:11px}.response-bar{left:0;right:0}}
|
|
14
|
+
@media(max-width:480px){.work-picker .picker-body{padding:17px}.work-picker{max-height:85svh}.work-finder{padding:8px 10px}header{gap:8px}.work-picker .filters select{font-size:12px}}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const $ = id => document.getElementById(id);
|
|
3
|
+
const node = (tag, value = '', cls = '') => { const n = document.createElement(tag); n.textContent = value; if (cls) n.className = cls; return n; };
|
|
4
|
+
const fmt = t => t && !Number.isNaN(Date.parse(t)) ? new Intl.DateTimeFormat('en-GB', {timeZone:'Asia/Dubai', day:'2-digit', month:'short', hour:'2-digit', minute:'2-digit'}).format(new Date(t)) : 'Not recorded';
|
|
5
|
+
let data, inbox = {responses:[], revisions:{}}, selected = localStorage.getItem('topolo-studio-selection') || '', view = 'review', chosen = -1, refreshing = false, refreshPending = false, lastFingerprint = '', submissionId = null, responseItem = null;
|
|
6
|
+
let lastSeen = localStorage.getItem('topolo-catch-up-last-read') || '';
|
|
7
|
+
const viewState = new Map();
|
|
8
|
+
let initialHash = ''; try { initialHash = decodeURIComponent(location.hash.slice(1)); } catch {}
|
|
9
|
+
function toast(value) { $('toast').textContent = value; $('toast').hidden = false; clearTimeout(toast.timer); toast.timer = setTimeout(() => $('toast').hidden = true, 4500); }
|
|
10
|
+
function safeURL(value) { try { const u = new URL(value, location.href); return ['http:','https:'].includes(u.protocol) || (u.protocol === 'codex:' && u.hostname === 'threads') ? u.href : null; } catch { return null; } }
|
|
11
|
+
function needsReview(t) { return t.needsHuman === true; }
|
|
12
|
+
function isBlocked(t) { return t.blocked || t.kind === 'held' || /blocked|stopped|held|hold|gap|refus/i.test(t.status || ''); }
|
|
13
|
+
function latestResponse(t) { return inbox.responses.filter(r => r.itemId === t.id).sort((a,b) => b.recordedAtUTC.localeCompare(a.recordedAtUTC))[0]; }
|
|
14
|
+
const workflowLabel = r => ({pending_delivery:'Waiting for delivery',route_unresolved:'Owner route needs verification',delivered:'Delivered · awaiting acceptance',accepted:'Accepted · in progress',completed:'Completed',blocked:'Blocked · owner action',rejected:'Owner declined',delivery_unknown:'Delivery needs reconciliation',verification_needed:'Evidence needs verification'}[r?.workflow?.state] || 'Saved · awaiting delivery');
|
|
15
|
+
function responseProgress(record) {
|
|
16
|
+
const section = node('section','','workflow-progress'), w = record.workflow;
|
|
17
|
+
section.append(node('h2','Response progress'),node('strong',workflowLabel(record)));
|
|
18
|
+
if(w?.owner) section.append(node('p','Responsible owner: ' + w.owner.title,'small'));
|
|
19
|
+
section.append(node('p',w?.summary || 'Saved locally. Owner acceptance has not been verified.'));
|
|
20
|
+
const steps = node('ol','','workflow-steps');
|
|
21
|
+
for(const [state,label] of [['saved','Saved'],['delivered','Delivered'],['accepted','Accepted'],['completed','Completed']]) {
|
|
22
|
+
const e = state === 'saved' ? {atUTC:record.recordedAtUTC,summary:'Your exact response was saved.'} : w?.events?.slice().reverse().find(e=>e.state===state);
|
|
23
|
+
const li = node('li','','' + (e ? 'verified':'pending'));
|
|
24
|
+
li.append(node('strong',label),node('span',e ? fmt(e.atUTC) : 'Not yet verified','small'));
|
|
25
|
+
if(e) li.append(node('p',e.summary)); steps.append(li);
|
|
26
|
+
}
|
|
27
|
+
section.append(steps); return section;
|
|
28
|
+
}
|
|
29
|
+
function currentResponse(t) { const r = latestResponse(t); return r && r.sourceRevision === inbox.revisions[t.id] ? r : null; }
|
|
30
|
+
function matches(t) { const q = $('search').value.toLowerCase(); return (!q || [t.title,t.summary,t.application,t.owner,t.laneTitle].join(' ').toLowerCase().includes(q)) && (!$('lane').value || t.laneId === $('lane').value) && (!$('state').value || ($('state').value === 'blocked' ? isBlocked(t) : $('state').value === 'media' ? t.reviewImages?.length : $('state').value === 'completed' ? true : needsReview(t))); }
|
|
31
|
+
function allItems() { return [...(data?.tasks || []), ...(data?.completed || [])]; }
|
|
32
|
+
function pendingDecisions() { return data.tasks.filter(t => needsReview(t) && !currentResponse(t)); }
|
|
33
|
+
function workItems(completed = false) { return (completed ? data.completed || [] : data.tasks).filter(t => !needsReview(t)); }
|
|
34
|
+
function viewRows() { return view === 'recorded' ? allItems().filter(t => latestResponse(t)) : view === 'review' ? pendingDecisions() : workItems(data.completed?.some(t => t.id === selected)); }
|
|
35
|
+
function rows() { return (view === 'work' ? workItems($('state').value === 'completed') : viewRows()).filter(matches); }
|
|
36
|
+
function closeFinder() { if ($('queue').open) $('queue').close(); $('queue-toggle').setAttribute('aria-expanded','false'); }
|
|
37
|
+
function renderPicker() {
|
|
38
|
+
const list = rows(), shown = list.slice(0,8);
|
|
39
|
+
$('picker-summary').textContent = list.length > 8 ? 'Showing 8 of ' + list.length + ' matches. Refine your search to find another item.' : list.length + (list.length === 1 ? ' match' : ' matches');
|
|
40
|
+
$('queue-list').replaceChildren(...shown.map(queueRow));
|
|
41
|
+
if (!shown.length) $('queue-list').append(node('p','No matches. Try another search or filter.','empty'));
|
|
42
|
+
}
|
|
43
|
+
function openFinder() { renderPicker(); $('queue').showModal(); $('queue-toggle').setAttribute('aria-expanded','true'); $('search').focus(); }
|
|
44
|
+
function reference(t) { return ['Topolo decision reference: ' + t.id, 'Work: ' + t.title, 'Lane: ' + (t.laneTitle || 'Engineering'), t.decisionId ? 'Decision: ' + t.decisionId : '', 'Status: ' + t.status, t.roadmap?.itemId ? 'Roadmap: ' + t.roadmap.itemId : '', 'View: ' + location.origin + '/#' + encodeURIComponent(t.id)].filter(Boolean).join('\n'); }
|
|
45
|
+
async function copyReference(t) { try { await navigator.clipboard.writeText(reference(t)); toast('Reference copied — paste it into any chat.'); } catch { $('copy-text').value = reference(t); $('copy-dialog').showModal(); $('copy-text').select(); } }
|
|
46
|
+
function selectItem(t) { if (!t || typeof t.id !== 'string' || !t.id) return false; selected = t.id; chosen = -1; localStorage.setItem('topolo-studio-selection', selected); history.replaceState(null, '', '#' + encodeURIComponent(selected)); closeFinder(); render(); return true; }
|
|
47
|
+
function openTask(taskId) {
|
|
48
|
+
const item = typeof taskId === 'string' && allItems().find(t => t.id === 'task:' + taskId && t.nativeTask?.id === taskId);
|
|
49
|
+
if (!item) { toast('This task is not in the current task feed. Refresh to load its recorded status.'); return false; }
|
|
50
|
+
view = latestResponse(item) ? 'recorded' : needsReview(item) ? 'review' : 'work';
|
|
51
|
+
for (const id of ['search','lane','state']) $(id).value = '';
|
|
52
|
+
if (view === 'work' && data.completed?.some(t => t.id === item.id)) $('state').value = 'completed';
|
|
53
|
+
return selectItem(item);
|
|
54
|
+
}
|
|
55
|
+
function setView(value) {
|
|
56
|
+
viewState.set(view, {selected, search:$('search').value, lane:$('lane').value, state:$('state').value});
|
|
57
|
+
view = value; const saved = viewState.get(view); selected = saved?.selected || ''; chosen = -1;
|
|
58
|
+
for (const id of ['search','lane','state']) $(id).value = saved?.[id] || '';
|
|
59
|
+
closeFinder(); render();
|
|
60
|
+
}
|
|
61
|
+
function anchor(label, url) { const safe = safeURL(url); if (!safe) return null; const a = node('a', label, 'support-link'); a.href = safe; a.target = '_blank'; a.rel = 'noopener noreferrer'; a.append(node('span','↗')); return a; }
|
|
62
|
+
function details(title, content) { const d = node('details','','details'); d.append(node('summary',title)); if (typeof content === 'string') d.append(node('p',content)); else d.append(content); return d; }
|
|
63
|
+
function renderReviewBacklog() {
|
|
64
|
+
const area = $('review-backlog'), records = data.decisionCoverage?.records || [], wasOpen = area.querySelector('details')?.open;
|
|
65
|
+
area.hidden = view !== 'review' || !records.length; area.replaceChildren(); if(area.hidden) return;
|
|
66
|
+
const list = node('ul','','review-backlog-list');
|
|
67
|
+
for(const record of records) { const row = node('li'); row.append(node('strong',record.title),node('p',record.reason)); list.append(row); }
|
|
68
|
+
const disclosure = details('Older review backlog · ' + records.length + ' records being reconciled',list); disclosure.open = Boolean(wasOpen); area.append(disclosure);
|
|
69
|
+
}
|
|
70
|
+
function queueRow(t) { const b = node('button','','queue-item' + (selected === t.id ? ' selected' : '')); b.type = 'button'; b.setAttribute('aria-current', selected === t.id ? 'true':'false'); b.append(node('span',t.application || t.laneTitle || 'Shared work','eyebrow'), node('span',t.title,'name')); const s = node('span','','state'); s.append(node('i','','dot' + (latestResponse(t) ? ' done':'')), document.createTextNode(latestResponse(t) ? workflowLabel(latestResponse(t)) : data.completed?.some(x => x.id === t.id) ? 'Completed' : needsReview(t) ? 'Awaiting your review' : isBlocked(t) ? 'Blocked · owner action' : 'In progress')); b.append(s); b.onclick = () => selectItem(t); return b; }
|
|
71
|
+
function showMedia(t, area) { const images = t.reviewImages || []; chosen = Math.min(chosen,images.length-1); const image = images[Math.min(chosen, images.length - 1)]; const box = node('div','','preview'); const img = node('img'); img.src = safeURL(image.url); img.alt = image.label || t.title; img.loading = 'eager'; img.onerror = () => { img.hidden = true; box.append(node('p','Preview unavailable. Open the source or send feedback.','small')); }; box.append(img); const expand = node('button','Expand preview ↗'); expand.onclick = () => { $('full-image').src = img.src; $('full-image').alt = img.alt; $('image-dialog').showModal(); }; box.append(expand); area.append(box); const caption = node('div','','preview-caption'); caption.append(node('span',image.label || 'Review preview'), node('span',String(chosen + 1) + ' / ' + images.length)); area.append(caption); const thumbs = node('div','','thumbnails'); images.slice(0,3).forEach((image,index) => { const b = node('button','','thumb'); b.setAttribute('aria-label','Preview ' + (index + 1) + ': ' + (image.label || 'image')); b.setAttribute('aria-pressed',String(chosen === index)); const i = node('img'); i.src = safeURL(image.url); i.alt = ''; i.loading = 'lazy'; b.append(i,node('span',image.label || 'Preview ' + (index + 1))); b.onclick = () => { chosen = index; renderFocus(t); }; thumbs.append(b); }); if (images.length > 3) { const more = node('button','+' + (images.length - 3) + ' more','thumb'); more.onclick = () => { chosen = (chosen + 1) % images.length; renderFocus(t); }; thumbs.append(more); } area.append(thumbs); }
|
|
72
|
+
function renderFocus(t) {
|
|
73
|
+
const content = $('focus'); content.replaceChildren(); content.className = 'content focus-content'; const context = node('div','','context-line'); context.append(node('span',t.application || (needsReview(t) ? 'Your review' : 'Shared work'),'eyebrow')); const list = viewRows(), index = list.findIndex(x => x.id === t.id); const pager = node('div','','pager'); pager.append(node('span',(index + 1) + ' of ' + list.length)); const prev = node('button','‹'), next = node('button','›'); prev.setAttribute('aria-label','Previous item'); next.setAttribute('aria-label','Next item'); prev.disabled = index <= 0; next.disabled = index >= list.length - 1; prev.onclick = () => selectItem(list[index - 1]); next.onclick = () => selectItem(list[index + 1]); pager.append(prev,next); context.append(pager); content.append(context,node('h1',t.title),node('p',needsReview(t) ? (t.reviewImages?.length ? t.reviewImages.length + ' directions ready. Select a preview and share your preference.' : t.status || 'Ready for your response') : t.status || 'Current work','subtitle'));
|
|
74
|
+
const layout = node('div','','detail-layout'), area = node('div','','preview-area');
|
|
75
|
+
if (t.reviewImages?.length) { if(chosen < 0) { const selectedAsset = latestResponse(t)?.response.asset; chosen = Math.max(0,(t.imageFiles || t.reviewImages).findIndex(i => (i.sha256 || i.url) === selectedAsset)); } showMedia(t,area); } else { const panel = node('div','','status-panel'); panel.append(node('span',latestResponse(t) ? workflowLabel(latestResponse(t)) : isBlocked(t) ? 'Owner action needed' : 'Current evidence','status-pill'), node('h2',t.status || 'In progress'), node('p',t.summary || 'Open the supporting material for this item.','detail-copy')); if (t.stages) { const stages = node('div','','stage-track'); for (const s of t.stages) stages.append(node('span',s.name,'stage ' + (['done','pass','complete'].includes(s.state) ? 'pass':''))); panel.append(stages); } area.append(panel); }
|
|
76
|
+
const record = latestResponse(t); if (record) { const r = node('div','','record'); r.append(node('strong',currentResponse(t) ? 'Your response is saved' : 'Your earlier response'),node('div',record.response.note),node('div',workflowLabel(record) + ' · ' + fmt(record.workflow?.atUTC || record.recordedAtUTC),'small')); area.prepend(r,responseProgress(record)); }
|
|
77
|
+
const evidence = node('div'); evidence.append(node('p',t.summary || 'No additional summary recorded.')); if(t.context) evidence.append(node('pre',t.context)); if (t.next) evidence.append(node('p','Next action: ' + t.next)); if (t.request?.text || t.originalRequest?.source) evidence.append(node('p','Original request: ' + (t.request?.text || t.originalRequest.source))); if (t.contributors?.length) { const ul = node('ul'); for (const c of t.contributors) ul.append(node('li',(data.agents?.[c.role]?.title || c.role) + ' — ' + c.contribution + ' (' + c.proof + ')')); evidence.append(details('Contributors',ul)); } if (t.evidence?.length) evidence.append(details('Technical evidence',node('pre',t.evidence.map(e => typeof e === 'string' ? e : JSON.stringify(e)).join('\n')))); area.append(details('Full context & evidence',evidence));
|
|
78
|
+
const inspector = node('aside','','inspector'); const owner = node('section'); const ownerRow = node('div','','owner'); const title = t.laneTitle || 'Topolo Engineering'; ownerRow.append(node('span',title.split(' ').map(x => x[0]).slice(0,2).join(''),'avatar')); const names = node('div'); names.append(node('div',title,'owner-name'),node('div','Prepared for you','small')); ownerRow.append(names); owner.append(ownerRow,node('p',t.owner || 'See originating lane for the next owner.')); inspector.append(owner);
|
|
79
|
+
const glance = node('section'); glance.append(node('h2',t.reviewImages?.length ? 'At a glance' : 'Next action'),node('p',t.reviewImages?.length ? (t.summary || t.status) : (t.next || 'No next action recorded.'))); if (t.verifiedAt || t.laneUpdatedAt) glance.append(node('p','Evidence · ' + fmt(t.verifiedAt || t.laneUpdatedAt),'small')); inspector.append(glance);
|
|
80
|
+
const support = node('section'); support.append(node('h2','Supporting material')); let count = 0; for (const link of t.links || []) { const a = anchor(link.label || 'Open source',link.url); if (a) {support.append(a); count++;} } if (t.roadmap?.url) { const a = anchor(t.roadmap.itemId ? 'Roadmap item' : 'Roadmap scope',t.roadmap.url); if(a) {support.append(a); count++;} } if (!count) support.append(node('p','No linked files for this item.')); const copy = node('button','Copy reference ↗','quiet'); copy.style.marginTop = '12px'; copy.onclick = () => copyReference(t); support.append(copy); inspector.append(support);
|
|
81
|
+
layout.append(area,inspector); content.append(layout); $('response-bar').hidden = false; $('response-context').textContent = t.reviewImages?.length && needsReview(t) ? 'Preference only. This does not approve implementation or publish anything.' : 'Send a scoped response. Track delivery, acceptance and completion in Your responses.'; $('primary-action').textContent = t.reviewImages?.length && needsReview(t) ? 'Choose this direction' : 'Send feedback'; $('primary-action').onclick = () => openResponse(t, t.reviewImages?.length && needsReview(t) ? 'preference':'feedback'); $('change-action').onclick = () => openResponse(t,'request_changes'); $('hold-action').onclick = () => openResponse(t,'hold_request');
|
|
82
|
+
}
|
|
83
|
+
function openResponse(t,intent) { responseItem = t; submissionId = crypto.randomUUID(); $('response-title').textContent = intent === 'preference' ? 'Record your preference' : intent === 'hold_request' ? 'Ask the owner to hold' : intent === 'request_changes' ? 'Request changes' : 'Send feedback'; $('response-scope').textContent = t.title; $('intent').value = intent; $('response-note').value = localStorage.getItem('topolo-studio-draft:' + t.id) || (intent === 'preference' ? 'My preferred direction is ' + (t.reviewImages[chosen]?.label || String(chosen + 1)) + '.' : ''); $('response-error').textContent = ''; $('response-dialog').showModal(); $('response-note').focus(); }
|
|
84
|
+
async function submitResponse(event) { event.preventDefault(); const t = responseItem; if (!t) return; const note = $('response-note').value.trim(); if (!note) return; const button = $('submit-response'); button.disabled = true; $('response-error').textContent = ''; const response = {intent:$('intent').value,note}; if (response.intent === 'preference' && t.reviewImages?.length) response.asset = t.imageFiles?.[chosen]?.sha256 || t.reviewImages[chosen].sha256 || t.reviewImages[chosen].url; const payload = {requestId:submissionId,itemId:t.id,revision:inbox.revisions[t.id],response}; if(t.nativeTask) { await submitNativeResponse(t,response,submissionId); return; } try { if (!inbox.nonce || !payload.revision) throw Error('Local decision inbox unavailable. Keep this draft and refresh.'); const res = await fetch('/api/decisions',{method:'POST',headers:{'Content-Type':'application/json','X-Studio-Token':inbox.nonce},body:JSON.stringify(payload)}); const record = await res.json(); if (!res.ok) throw Error(record.error || 'Response not confirmed saved.'); if (!inbox.responses.some(r => r.id === record.id)) inbox.responses.push(record); localStorage.removeItem('topolo-studio-draft:' + t.id); $('response-dialog').close(); view = 'recorded'; selected = t.id; render(); toast('Response saved. Track delivery and owner progress in Your responses.'); refresh(); } catch(e) { $('response-error').textContent = e.message; } finally {button.disabled = false;} }
|
|
85
|
+
function render() {
|
|
86
|
+
if (!data) return; for (const b of document.querySelectorAll('[data-view]')) b.setAttribute('aria-selected',String(b.dataset.view === view)); $('review-count').textContent = pendingDecisions().length; $('work-count').textContent = workItems().length; $('recorded-count').textContent = inbox.responses.length; $('updated').textContent = (data.activityConnection === 'live' ? 'Live activity · ' : 'Saved feed · ') + fmt(data.updatedAt); if (['releases','activity'].includes(view)) closeFinder(); $('response-bar').hidden = true; $('queue-toggle').hidden = ['releases','activity'].includes(view); $('search').placeholder = view === 'review' ? 'Find a decision…' : 'Find work…';
|
|
87
|
+
$('view-description').textContent = {review:'Decisions · Choices, approvals or information explicitly requested from you. Once you respond, track the result in Your responses.',work:'Work · Owner tasks, fixes, blockers and issue summaries. Items requesting your decision are in Decisions. This count is not the number of queued releases.',recorded:'Your responses · What you have answered, with delivery, acceptance and completion shown separately.',releases:'Releases · Saved environment evidence from the responsible owners.',activity:'Activity · Recorded changes across the connected feeds.'}[view];
|
|
88
|
+
$('decision-coverage').hidden = !['review','work'].includes(view) || data.decisionCoverage?.state !== 'incomplete';
|
|
89
|
+
$('decision-coverage').textContent = data.decisionCoverage?.summary || '';
|
|
90
|
+
renderReviewBacklog();
|
|
91
|
+
if (view === 'releases') { $('focus').replaceChildren(node('span','Delivery evidence','eyebrow'),node('h1','Release progress'),node('p','Saved evidence from the owning lanes. Each environment must be verified independently.','intro')); const wrap = node('div','','table-wrap'), table = node('table'), head = node('tr'); for(const label of ['Application','DEV','Staging','Production','Next']) head.append(node('th',label)); table.append(head); for(const r of data.releaseOverview || []) { const tr = node('tr'); for(const key of ['app','dev','staging','production','next']) tr.append(node('td',r[key] || 'Unverified')); table.append(tr); } wrap.append(table); $('focus').append(wrap); return; }
|
|
92
|
+
if (view === 'activity') { $('focus').replaceChildren(node('span',data.activityConnection === 'live' ? 'Live task timeline' : 'Saved timeline','eyebrow'),node('h1','Recent activity')); for(const c of (data.changes || []).slice(0,40)) { const row = node('article','','activity-row'); row.append(node('time',fmt(c.at)),node('div',c.title || 'Update'),node('p',c.detail || c.summary || '')); if(c.taskId) { const open = node('button','View task','secondary'); open.type='button'; open.onclick=()=>openTask(c.taskId); row.append(open); } $('focus').append(row); } return; }
|
|
93
|
+
const list = viewRows(); if (!list.some(t => t.id === selected)) selected = list[0]?.id || ''; $('queue-title').textContent = view === 'review' ? 'Find a decision' : view === 'recorded' ? 'Find a response' : 'Find work'; $('queue-toggle').textContent = view === 'review' ? 'Find decision' : view === 'recorded' ? 'Find response' : 'Find work'; renderPicker(); const t = list.find(t => t.id === selected); if(t) renderFocus(t); else { $('focus').replaceChildren(node('span',view === 'review' ? 'Published requests' : 'No matches','eyebrow'),node('h1',view === 'review' ? 'No published decisions waiting' : 'Nothing in this view'),node('p',view === 'review' ? 'No unanswered request matches this view. This does not establish that all work has been checked for missing decisions. Recorded responses stay in Your responses.' : 'Change the search or filters to see more work.','intro')); }
|
|
94
|
+
}
|
|
95
|
+
async function refresh() { if(refreshing) { refreshPending = true; return; } refreshing = true; try { let next = await savedJSON('updates.json'); next = await combineFeeds(next); let liveWork; try {liveWork = await savedJSON('/api/work');} catch {liveWork = {tasks:[],connection:'offline'};} window.nativeWork = liveWork; for (const task of liveWork.tasks || []) { const item = nativeTaskItem(task); if(task.state === 'completed') (next.completed ||= []).push(item); else (next.tasks ||= []).push(item); } let local; try {local = await savedJSON('/api/inbox');} catch {local = {responses:[],revisions:{}};} const fingerprint = JSON.stringify([next,local.responses]); data = next; inbox = local; if(initialHash) { const linked = allItems().find(t => t.id === initialHash); if(linked) { selected = linked.id; view = latestResponse(linked) ? 'recorded' : needsReview(linked) ? 'review' : 'work'; } initialHash = ''; } if(fingerprint !== lastFingerprint) { lastFingerprint = fingerprint; const value = $('lane').value; $('lane').replaceChildren(new Option('All lanes','')); const lanes = new Map(allItems().map(t => [t.laneId,t.laneTitle])); for(const [id,title] of lanes) if(id) $('lane').append(new Option(title || id,id)); $('lane').value = value; render(); } $('notice').hidden = false; $('notice').textContent = !inbox.nonce ? 'Viewer only: local response inbox is unavailable. Copy a reference to keep feedback attached to the right work.' : data.sourceNotice?.includes('unavailable') ? data.sourceNotice : liveWork.activityNotice ? liveWork.activityNotice : liveWork.connection === 'live' ? 'Task activity is live. Earlier saved-feedback delivery remains separate; follow each response for its acceptance and result.' : 'Native execution is disconnected. Saved task evidence is shown; no work is being retried.'; $('notice').hidden = !$('notice').textContent; } catch { $('notice').hidden = false; $('notice').textContent = 'Saved feeds unavailable. This view may be historical; no current status is inferred.'; } finally {refreshing = false; if(refreshPending) {refreshPending = false; void refresh();}} }
|
|
96
|
+
const viewButtons = [...document.querySelectorAll('[data-view]')]; for(const [index,b] of viewButtons.entries()) { b.onclick = () => setView(b.dataset.view); b.onkeydown = e => { const target = e.key === 'ArrowRight' ? (index+1)%viewButtons.length : e.key === 'ArrowLeft' ? (index+viewButtons.length-1)%viewButtons.length : e.key === 'Home' ? 0 : e.key === 'End' ? viewButtons.length-1 : null; if(target !== null) {e.preventDefault();viewButtons[target].focus();viewButtons[target].click();} }; }
|
|
97
|
+
for(const id of ['search','lane','state']) $(id).oninput = renderPicker;
|
|
98
|
+
$('refresh').onclick = refresh; $('queue-toggle').onclick = openFinder; $('close-queue').onclick = closeFinder; $('queue').addEventListener('close', () => $('queue-toggle').setAttribute('aria-expanded','false')); $('queue').addEventListener('keydown', e => { if(e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); closeFinder(); } });
|
|
99
|
+
$('response-form').onsubmit = submitResponse; $('response-note').oninput = () => { if(responseItem) localStorage.setItem('topolo-studio-draft:' + responseItem.id,$('response-note').value); };
|
|
100
|
+
for(const b of document.querySelectorAll('[data-close]')) b.onclick = () => $(b.dataset.close).close();
|
|
101
|
+
document.addEventListener('keydown', e => { if(e.target.matches('input,textarea,select') || document.querySelector('dialog[open]')) return; if(['ArrowLeft','ArrowRight'].includes(e.key) && data && !['activity','releases'].includes(view)) { const list = viewRows(), i = list.findIndex(t => t.id === selected), target = i + (e.key === 'ArrowRight' ? 1:-1); if(list[target]) {e.preventDefault();selectItem(list[target]);} } });
|
|
102
|
+
if(initialHash) selected = initialHash;
|
|
103
|
+
refresh(); document.addEventListener('visibilitychange',() => {if(!document.hidden) refresh();});
|
|
104
|
+
|
|
105
|
+
// Native task updates are pushed from the owning connection; no polling loop.
|
|
106
|
+
function nativeTaskItem(task) {
|
|
107
|
+
const workers=(window.nativeWork?.workers || []).filter(w=>w.taskId===task.id);
|
|
108
|
+
const latest=workers[workers.length-1], choice=latest?.resolution?.resolved;
|
|
109
|
+
return {id:'task:'+task.id,nativeTask:task,originalRequest:{source:task.objective},kind:'task',title:({'pending-work-discovery':'Pending work review','retained-inventory-verification':'Work inventory check'})[task.route?.task?.type] || (({engineering:'Engineering',operations:'Operations',cicd:'CI/CD'})[task.channel] || 'Shared')+' task',application:({engineering:'Engineering',operations:'Operations',cicd:'CI/CD'})[task.channel] || task.channel,laneId:'native-work',laneTitle:'Task coordinators',owner:task.deliveryOwner,status:task.state,statusLabel:task.state.replaceAll('_',' '),summary:task.summary,next:task.verification?.summary || task.route?.acceptance || 'The coordinator will select the appropriate discipline.',needsHuman:task.state==='needs_input',closed:task.state==='completed',blocked:['blocked','unknown','failed'].includes(task.state),evidenceAtUTC:new Date(task.updatedAt*1000).toISOString(),evidence:(task.checkpoints || []).map(p=>p.path),context:['Task: '+task.id,'Phase: '+task.phase,choice?'Execution: '+choice.model+' / '+choice.effort:'Routing pending','Specialists are replaced after each task; progress and results remain here.',task.result?.summary || ''].join('\n\n'),links:[]};
|
|
110
|
+
}
|
|
111
|
+
let workRequestId;
|
|
112
|
+
$('request-work').onclick=()=>{workRequestId=localStorage.getItem('topolo-work-request-id') || crypto.randomUUID();localStorage.setItem('topolo-work-request-id',workRequestId);$('work-objective').value=localStorage.getItem('topolo-work-draft') || '';$('work-error').textContent='';$('work-dialog').showModal();$('work-objective').focus();};
|
|
113
|
+
$('work-objective').oninput=()=>localStorage.setItem('topolo-work-draft',$('work-objective').value);
|
|
114
|
+
$('work-form').onsubmit=async event=>{event.preventDefault();$('work-submit').disabled=true;try{if(!window.nativeWork?.nonce || window.nativeWork.connection!=='live')throw Error('The native coordinator is disconnected. Your draft is retained.');const response=await fetch('/api/work',{method:'POST',headers:{'Content-Type':'application/json','X-Studio-Token':window.nativeWork.nonce},body:JSON.stringify({requestId:workRequestId,objective:$('work-objective').value.trim(),channel:$('work-channel').value,canonicalClaimId:null})});const result=await response.json();if(!response.ok)throw Error(result.error || 'Dispatch was not confirmed. Keep the same request identity.');localStorage.removeItem('topolo-work-request-id');localStorage.removeItem('topolo-work-draft');$('work-dialog').close();view='work';selected='task:'+result.task.id;await refresh();toast('Request recorded. Follow its progress in Work.');}catch(error){$('work-error').textContent=error.message;}finally{$('work-submit').disabled=false;}};
|
|
115
|
+
const workEvents=new EventSource('/api/work/events');workEvents.onmessage=()=>refresh();
|
|
116
|
+
const feedEvents=new EventSource('/api/feed/events');feedEvents.onmessage=()=>refresh();
|
|
117
|
+
workEvents.onerror=()=>{if(window.nativeWork)window.nativeWork.connection='offline';};
|
|
118
|
+
|
|
119
|
+
async function submitNativeResponse(item,response,requestId) {
|
|
120
|
+
try {const result=await fetch('/api/work/responses',{method:'POST',headers:{'Content-Type':'application/json','X-Studio-Token':window.nativeWork.nonce},body:JSON.stringify({requestId,taskId:item.nativeTask.id,revision:item.nativeTask.revision,intent:response.intent==='hold_request'?'hold':response.intent==='request_changes'?'request_changes':'continue',note:response.note})});const value=await result.json();if(!result.ok)throw Error(value.error || 'Response not accepted.');$('response-dialog').close();localStorage.removeItem('topolo-studio-draft:'+item.id);view='work';await refresh();toast('Response recorded on the task. Its next attempt uses fresh workers.');}catch(error){$('response-error').textContent=error.message;}finally{$('submit-response').disabled=false;}
|
|
121
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Loopback-only saved-feed viewer and append-only human response inbox.
|
|
3
|
+
|
|
4
|
+
Records responses and projects evidence-backed workflow receipts.
|
|
5
|
+
No browser request can write receipts, dispatch agents or execute platform actions.
|
|
6
|
+
"""
|
|
7
|
+
import argparse
|
|
8
|
+
import hashlib
|
|
9
|
+
import hmac
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import secrets
|
|
14
|
+
import select
|
|
15
|
+
import tempfile
|
|
16
|
+
import threading
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from urllib.parse import urlsplit
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def digest(value):
|
|
24
|
+
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def activity_topic(task_type, fallback):
|
|
28
|
+
"""Use a bounded authored task type, excluding scope qualifiers and machine data."""
|
|
29
|
+
if not isinstance(task_type,str):return fallback
|
|
30
|
+
value=task_type.strip()
|
|
31
|
+
if not re.fullmatch(r'[A-Za-z][A-Za-z -]{2,119}',value):return fallback
|
|
32
|
+
value=re.sub(r'^read[ -]only\s+','',value,flags=re.I)
|
|
33
|
+
value=re.split(r'\s+(?:across|using|against|with|from)\s+',value,maxsplit=1,flags=re.I)[0]
|
|
34
|
+
words=value.replace('-', ' ').split()
|
|
35
|
+
if not 2<=len(words)<=7 or len(value)>54:return fallback
|
|
36
|
+
value=' '.join(words)
|
|
37
|
+
return value[0].upper()+value[1:]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def activity_progress(summary):
|
|
41
|
+
"""Accept one clear human progress sentence, never a technical proof dump."""
|
|
42
|
+
if not isinstance(summary,str):return None
|
|
43
|
+
value=re.sub(r'^Reported progress:\s*','',summary.strip())
|
|
44
|
+
# Validate the whole commentary before selecting its first sentence.
|
|
45
|
+
if len(value)>400 or re.search(r'[\n\r`/\\{}\[\]<>]|https?:|\b[0-9a-f]{12,}\b|\b\w*[0-9]\w*[0-9]\w*\b',value,re.I):return None
|
|
46
|
+
sentence=re.split(r'(?<=[.!?])\s+',value,maxsplit=1)[0]
|
|
47
|
+
if not 20<=len(sentence)<=180 or not re.fullmatch(r"[A-Za-z0-9 ,.;:!?()'’—-]+",sentence):return None
|
|
48
|
+
if not re.match(r"^(?:I(?:'m| am)?|We(?:'re| are)?|The specialist|The reviewer) (?:am |are )?(?:checking|reviewing|comparing|reconciling|verifying|reading|preparing|investigating|have |found |confirmed )",sentence,re.I):return None
|
|
49
|
+
return sentence
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def task_activity_text(task, event):
|
|
53
|
+
"""Project only stated scope and lifecycle facts, never a raw model report."""
|
|
54
|
+
labels={'pending-work-discovery':'Pending work review',
|
|
55
|
+
'retained-inventory-verification':'Work inventory check'}
|
|
56
|
+
channel={'engineering':'Engineering', 'operations':'Operations', 'cicd':'CI/CD'}.get(task.get('channel'),'Shared')
|
|
57
|
+
task_type=task.get('route',{}).get('task',{}).get('type')
|
|
58
|
+
topic=labels.get(task_type) or activity_topic(task_type,channel+' task')
|
|
59
|
+
kind=event['kind']
|
|
60
|
+
summaries={
|
|
61
|
+
'requested':('Request received','Your request is recorded. A coordinator will choose the appropriate specialist.'),
|
|
62
|
+
'native-resume-verified':('Specialist started','A specialist is working on this task. Its findings will appear in the task details.'),
|
|
63
|
+
'worker-progress':('Progress reported','A specialist recorded a progress update. Open the task for the latest findings and next step.'),
|
|
64
|
+
'coordinator-released':('Specialist selected','The coordinator has selected a specialist. Work will continue within the recorded task scope.'),
|
|
65
|
+
'worker-released':('Result ready for review','The specialist has saved a result. Independent review is the next step.'),
|
|
66
|
+
'verification-recorded':('Review available','An independent review is available. Open the task for its findings, limitations and next steps.'),
|
|
67
|
+
'completion-blocked':('Work needs attention','The task could not be completed. Open its details for the recorded issue and next owner action.'),
|
|
68
|
+
'dispatch-blocked':('Work could not start','A specialist could not start. Open the task for the reason and next supported action.'),
|
|
69
|
+
'routing-checkpoint-reconciled':('Routing restored','The existing routing result has been restored. Work can continue without repeating that step.'),
|
|
70
|
+
'human-response':('Response saved','Your response is saved on the existing task. Follow the task for its next action.'),
|
|
71
|
+
'review-continuation-requested':('New review requested','Another review has been requested for the existing result. The original execution is preserved.'),
|
|
72
|
+
'recovery-required':('Work needs reconciliation','The outcome needs verification before work can continue. Open the task for the existing checkpoint and owner.'),
|
|
73
|
+
}
|
|
74
|
+
label,detail=summaries.get(kind,('Task updated','The task has a recorded update. Open its details for the current status and next action.'))
|
|
75
|
+
if kind=='native-resume-verified' and re.search(r'\breview\b',event.get('summary',''),re.I):
|
|
76
|
+
label,detail='Review started','An independent reviewer is checking the existing result. Findings will appear in the task details.'
|
|
77
|
+
if kind=='worker-progress':
|
|
78
|
+
commentary=activity_progress(event.get('summary',''))
|
|
79
|
+
if commentary:detail=commentary
|
|
80
|
+
if kind=='verification-recorded':
|
|
81
|
+
report=event.get('summary','')
|
|
82
|
+
scope=re.search(r'\b([0-9][0-9,]*) of ([0-9][0-9,]*) (?:retained )?(?:claims|records)\b',report,re.I)
|
|
83
|
+
if scope:
|
|
84
|
+
reviewed,total=(int(value.replace(',','')) for value in scope.groups())
|
|
85
|
+
if 0<reviewed<total:
|
|
86
|
+
unfinished=re.search(r'\b([0-9][0-9,]*) are unfinished\b',report,re.I)
|
|
87
|
+
count=int(unfinished[1].replace(',','')) if unfinished else None
|
|
88
|
+
outcome=(f'The review reports {count:,} unfinished items among {reviewed:,} of {total:,} work records.'
|
|
89
|
+
if count is not None and 0<=count<=reviewed
|
|
90
|
+
else f'The review covers {reviewed:,} of {total:,} work records.')
|
|
91
|
+
detail=outcome+' Review the remaining records before treating this as the full backlog.'
|
|
92
|
+
return topic+' · '+label,detail
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def project_task_activity(saved, snapshot):
|
|
96
|
+
tasks={task['id']:task for task in snapshot.get('tasks',[])}
|
|
97
|
+
changes={str(item.get('id',item.get('at','')+':'+item.get('title',''))):item for item in saved.get('changes',[])}
|
|
98
|
+
for event in snapshot.get('events',[]):
|
|
99
|
+
if event['kind'] in {'worker-reserved','checkpoint-saved','unstarted-worker-reconciled','task-context-bound'}:continue
|
|
100
|
+
task=tasks.get(event['taskId'],{});identity='native-task:'+event['taskId']+':'+str(event['sequence'])
|
|
101
|
+
title,detail=task_activity_text(task,event)
|
|
102
|
+
changes[identity]={'id':identity,'at':datetime.fromtimestamp(event['at'],timezone.utc).isoformat(),
|
|
103
|
+
'title':title,'detail':detail,'fullDetail':event['summary'],
|
|
104
|
+
'taskId':event['taskId'],'owner':task.get('deliveryOwner'),
|
|
105
|
+
'taskState':task.get('state'),'source':'native-task-ledger'}
|
|
106
|
+
for event in snapshot.get('coordinatorObservations',[]):
|
|
107
|
+
changes[event['id']]={'id':event['id'],'at':datetime.fromtimestamp(event['at'],timezone.utc).isoformat(),
|
|
108
|
+
'title':event['owner']+(' · execution observed' if event['state']=='active' else ' · now idle'),
|
|
109
|
+
'detail':event['summary'],'owner':event['owner'],'source':'native-status-observation'}
|
|
110
|
+
ordered=sorted(changes.values(),key=lambda item:item.get('at',''),reverse=True)
|
|
111
|
+
return {**saved,'changes':ordered,'updatedAt':ordered[0]['at'] if ordered else saved.get('updatedAt'),
|
|
112
|
+
'activityConnection':snapshot.get('connection','offline')}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def feed_events(root,inbox):
|
|
116
|
+
"""OS filesystem notifications, not a timer or an agent polling loop."""
|
|
117
|
+
if not hasattr(select,'kqueue'):raise OSError('Native feed notifications require macOS kqueue')
|
|
118
|
+
roots=[root,root/'feeds',inbox,inbox.parent/'decision-studio-receipts']
|
|
119
|
+
descriptors={};queue=select.kqueue()
|
|
120
|
+
def sync():
|
|
121
|
+
paths={p for p in roots if p.is_dir()}
|
|
122
|
+
receipts=roots[-1]
|
|
123
|
+
if receipts.is_dir():paths.update(p for p in receipts.rglob('*') if p.is_dir() and not p.is_symlink())
|
|
124
|
+
for path in set(descriptors)-paths:os.close(descriptors.pop(path))
|
|
125
|
+
for path in paths-set(descriptors):
|
|
126
|
+
fd=os.open(path,os.O_RDONLY);descriptors[path]=fd
|
|
127
|
+
queue.control([select.kevent(fd,filter=select.KQ_FILTER_VNODE,flags=select.KQ_EV_ADD|select.KQ_EV_CLEAR,fflags=select.KQ_NOTE_WRITE|select.KQ_NOTE_RENAME|select.KQ_NOTE_DELETE)],0,0)
|
|
128
|
+
try:
|
|
129
|
+
sync()
|
|
130
|
+
yield {'connected':True,'source':'native-file-events'}
|
|
131
|
+
while True:
|
|
132
|
+
changes=queue.control(None,32,None)
|
|
133
|
+
if changes:
|
|
134
|
+
sync()
|
|
135
|
+
yield {'changed':True,'source':'native-file-events'}
|
|
136
|
+
finally:
|
|
137
|
+
queue.close()
|
|
138
|
+
for fd in descriptors.values():os.close(fd)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def item_catalog(root):
|
|
142
|
+
base = json.loads((root / "updates.json").read_text())
|
|
143
|
+
result = {str(t["id"]): t for t in base.get("tasks", []) + base.get("completed", [])}
|
|
144
|
+
manifest = json.loads((root / "feeds.json").read_text())
|
|
145
|
+
for feed in manifest.get("feeds", []):
|
|
146
|
+
url = feed["url"]
|
|
147
|
+
if not re.fullmatch(r"feeds/[A-Za-z0-9_-]+\.json", url):
|
|
148
|
+
raise ValueError("Invalid local feed path")
|
|
149
|
+
lane = json.loads((root / url).read_text())
|
|
150
|
+
if lane.get("writerLane") != feed["laneId"]:
|
|
151
|
+
raise ValueError("Lane identity mismatch")
|
|
152
|
+
for item in lane.get("items", []):
|
|
153
|
+
result[feed["laneId"] + ":" + item["id"]] = {**item, "laneId": feed["laneId"]}
|
|
154
|
+
if feed.get("ownerThreadId"):
|
|
155
|
+
result[feed["laneId"] + ":" + item["id"]]["workflowOwner"] = {"threadId": feed["ownerThreadId"], "title": feed["ownerThreadTitle"]}
|
|
156
|
+
for item in result.values():
|
|
157
|
+
if not item.get("laneId"):
|
|
158
|
+
item["workflowOwner"] = {"threadId": "01a120cd-e25d-7302-9130-6bfb4e41d38c", "title": "Topolo Engineering"}
|
|
159
|
+
return result
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def response_workflow(record, item, receipts):
|
|
163
|
+
"""Project trusted local coordinator receipts; never infer ACK from a send."""
|
|
164
|
+
route = record.get("workflowOwner") or item.get("workflowOwner")
|
|
165
|
+
result = {"state": "pending_delivery" if route else "route_unresolved", "owner": route,
|
|
166
|
+
"summary": "Saved; automatic workflow delivery is not connected yet.", "events": []}
|
|
167
|
+
if not route:
|
|
168
|
+
result["summary"] = "Saved; originating owner route needs verification."
|
|
169
|
+
source_hash = digest(record)
|
|
170
|
+
allowed = {"delivered", "accepted", "completed", "blocked", "rejected", "delivery_unknown"}
|
|
171
|
+
paths = sorted((receipts / record["id"]).glob("*.json"))
|
|
172
|
+
previous = None
|
|
173
|
+
for path in paths:
|
|
174
|
+
try:
|
|
175
|
+
event = json.loads(path.read_text())
|
|
176
|
+
if (event["responseId"] != record["id"] or event["responseHash"] != source_hash
|
|
177
|
+
or not route or event["ownerThreadId"] != route["threadId"]
|
|
178
|
+
or event["state"] not in allowed or not event.get("summary")
|
|
179
|
+
or not event.get("atUTC") or not event.get("proof")):
|
|
180
|
+
raise ValueError("Receipt identity or evidence missing")
|
|
181
|
+
for proof in event["proof"]:
|
|
182
|
+
data = Path(proof["path"]).read_bytes()
|
|
183
|
+
if hashlib.sha256(data).hexdigest() != proof["sha256"]:
|
|
184
|
+
raise ValueError("Receipt proof changed")
|
|
185
|
+
if event["state"] == "completed" and previous != "accepted":
|
|
186
|
+
raise ValueError("Completion requires prior positive owner acceptance")
|
|
187
|
+
result["events"].append({k: event[k] for k in ("state", "atUTC", "summary")})
|
|
188
|
+
result.update(state=event["state"], summary=event["summary"], atUTC=event["atUTC"])
|
|
189
|
+
previous = event["state"]
|
|
190
|
+
except (OSError, ValueError, KeyError, TypeError):
|
|
191
|
+
result.update(state="verification_needed", summary="Workflow receipt could not be verified. Completion is not confirmed.")
|
|
192
|
+
break
|
|
193
|
+
return result
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def make_handler(root, inbox, work=None):
|
|
197
|
+
nonce = secrets.token_urlsafe(32)
|
|
198
|
+
lock = threading.Lock()
|
|
199
|
+
|
|
200
|
+
class Handler(SimpleHTTPRequestHandler):
|
|
201
|
+
def __init__(self, *args, **kwargs):
|
|
202
|
+
super().__init__(*args, directory=str(root), **kwargs)
|
|
203
|
+
|
|
204
|
+
def end_headers(self):
|
|
205
|
+
self.send_header("Cache-Control", "no-store")
|
|
206
|
+
self.send_header("X-Content-Type-Options", "nosniff")
|
|
207
|
+
self.send_header("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; media-src 'self' https:; connect-src 'self'; frame-src 'none'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'")
|
|
208
|
+
super().end_headers()
|
|
209
|
+
|
|
210
|
+
def reply(self, status, value):
|
|
211
|
+
body = json.dumps(value).encode()
|
|
212
|
+
self.send_response(status)
|
|
213
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
214
|
+
self.send_header("Content-Length", str(len(body)))
|
|
215
|
+
self.end_headers()
|
|
216
|
+
self.wfile.write(body)
|
|
217
|
+
|
|
218
|
+
def valid_host(self):
|
|
219
|
+
return self.headers.get("Host") in {"127.0.0.1:" + str(self.server.server_port), "localhost:" + str(self.server.server_port)}
|
|
220
|
+
|
|
221
|
+
def do_GET(self):
|
|
222
|
+
if not self.valid_host():
|
|
223
|
+
return self.reply(403, {"error": "Local host required"})
|
|
224
|
+
path = urlsplit(self.path).path
|
|
225
|
+
if work and path == '/updates.json':
|
|
226
|
+
try:return self.reply(200,project_task_activity(json.loads((root/'updates.json').read_text()),work.snapshot()))
|
|
227
|
+
except Exception:return self.reply(503,{'error':'Current task activity is unavailable; saved history is not presented as live.'})
|
|
228
|
+
if work and path == '/api/work':
|
|
229
|
+
try:return self.reply(200,{**work.snapshot(),'nonce':nonce})
|
|
230
|
+
except Exception:return self.reply(503,{'error':'Work records are unavailable; existing checkpoints are preserved.'})
|
|
231
|
+
if path == '/api/feed/events':
|
|
232
|
+
if not hasattr(select,'kqueue'):return self.reply(503,{'error':'Native saved-feed notifications are unavailable on this platform.'})
|
|
233
|
+
try:
|
|
234
|
+
events=feed_events(root,inbox);first=next(events)
|
|
235
|
+
self.send_response(200);self.send_header('Content-Type','text/event-stream');self.end_headers()
|
|
236
|
+
self.wfile.write(('data: '+json.dumps(first)+'\n\n').encode());self.wfile.flush()
|
|
237
|
+
for event in events:
|
|
238
|
+
self.wfile.write(('data: '+json.dumps(event)+'\n\n').encode());self.wfile.flush()
|
|
239
|
+
except (OSError,ValueError,StopIteration):pass
|
|
240
|
+
return
|
|
241
|
+
if work and path == '/api/work/events':
|
|
242
|
+
try:
|
|
243
|
+
events=work.events();first=next(events)
|
|
244
|
+
self.send_response(200);self.send_header('Content-Type','text/event-stream');self.end_headers()
|
|
245
|
+
self.wfile.write(('data: '+json.dumps(first)+'\n\n').encode());self.wfile.flush()
|
|
246
|
+
for event in events:
|
|
247
|
+
self.wfile.write(('data: '+json.dumps(event)+'\n\n').encode());self.wfile.flush()
|
|
248
|
+
except (OSError,ValueError,StopIteration):pass
|
|
249
|
+
except Exception:pass
|
|
250
|
+
return
|
|
251
|
+
if path == "/api/inbox":
|
|
252
|
+
try:
|
|
253
|
+
items = item_catalog(root)
|
|
254
|
+
records = [json.loads(p.read_text()) for p in sorted(inbox.glob("*.json"))]
|
|
255
|
+
return self.reply(200, {"nonce": nonce, "revisions": {k: digest(v) for k, v in items.items()}, "responses": [{**r, "workflow": response_workflow(r, items.get(r["itemId"], {}), inbox.parent / "decision-studio-receipts")} for r in records]})
|
|
256
|
+
except (OSError, ValueError, KeyError):
|
|
257
|
+
return self.reply(503, {"error": "Saved feeds or decision inbox unavailable"})
|
|
258
|
+
if path.startswith("/api/"):
|
|
259
|
+
return self.reply(404, {"error": "Unknown local action"})
|
|
260
|
+
return super().do_GET()
|
|
261
|
+
|
|
262
|
+
def do_POST(self):
|
|
263
|
+
if work and urlsplit(self.path).path in {'/api/work','/api/work/responses'}:
|
|
264
|
+
if (not self.valid_host() or self.headers.get('Origin')!='http://'+self.headers.get('Host','') or
|
|
265
|
+
not hmac.compare_digest(self.headers.get('X-Studio-Token',''),nonce)):
|
|
266
|
+
return self.reply(403,{'error':'Open the local work panel before submitting.'})
|
|
267
|
+
try:
|
|
268
|
+
size=int(self.headers.get('Content-Length','0'))
|
|
269
|
+
if not 0<size<=32768 or self.headers.get_content_type()!='application/json':raise ValueError('Invalid work request')
|
|
270
|
+
payload=json.loads(self.rfile.read(size));result=(work.respond(payload) if urlsplit(self.path).path.endswith('/responses') else work.submit(payload))
|
|
271
|
+
return self.reply(202 if result['created'] else 200,result)
|
|
272
|
+
except (ValueError,KeyError,TypeError):return self.reply(400,{'error':'Invalid work request'})
|
|
273
|
+
except Exception as exc:return self.reply(409,{'error':str(exc)})
|
|
274
|
+
if urlsplit(self.path).path != "/api/decisions":
|
|
275
|
+
return self.reply(404, {"error": "Unknown local action"})
|
|
276
|
+
origin = self.headers.get("Origin")
|
|
277
|
+
expected = "http://" + self.headers.get("Host", "")
|
|
278
|
+
if not self.valid_host() or origin != expected or not hmac.compare_digest(self.headers.get("X-Studio-Token", ""), nonce):
|
|
279
|
+
return self.reply(403, {"error": "Open this local panel again before sending"})
|
|
280
|
+
try:
|
|
281
|
+
size = int(self.headers.get("Content-Length", "0"))
|
|
282
|
+
if not 0 < size <= 32768 or self.headers.get_content_type() != "application/json":
|
|
283
|
+
return self.reply(400, {"error": "Invalid response payload"})
|
|
284
|
+
payload = json.loads(self.rfile.read(size))
|
|
285
|
+
rid = payload["requestId"]
|
|
286
|
+
if not isinstance(rid, str) or not re.fullmatch(r"[a-f0-9-]{36}", rid):
|
|
287
|
+
raise ValueError("Invalid response identity")
|
|
288
|
+
response = payload["response"]
|
|
289
|
+
if not isinstance(response, dict) or set(response) - {"intent", "note", "asset"}:
|
|
290
|
+
raise ValueError("Invalid response fields")
|
|
291
|
+
if response.get("intent") not in {"feedback", "preference", "request_changes", "hold_request"}:
|
|
292
|
+
raise ValueError("Unsupported response")
|
|
293
|
+
note = response.get("note", "")
|
|
294
|
+
if not isinstance(note, str) or not 1 <= len(note.strip()) <= 8000:
|
|
295
|
+
raise ValueError("Add a short response")
|
|
296
|
+
with lock:
|
|
297
|
+
target = inbox / (rid + ".json")
|
|
298
|
+
input_hash = digest(payload)
|
|
299
|
+
if target.exists():
|
|
300
|
+
existing = json.loads(target.read_text())
|
|
301
|
+
if existing["inputHash"] != input_hash:
|
|
302
|
+
return self.reply(409, {"error": "Response identity already used"})
|
|
303
|
+
return self.reply(200, existing)
|
|
304
|
+
catalog = item_catalog(root)
|
|
305
|
+
item = catalog.get(payload["itemId"])
|
|
306
|
+
if item is None or digest(item) != payload.get("revision"):
|
|
307
|
+
return self.reply(409, {"error": "This item changed. Refresh and review it before sending."})
|
|
308
|
+
asset = response.get("asset")
|
|
309
|
+
images = item.get("imageFiles", []) + item.get("reviewImages", [])
|
|
310
|
+
if asset is not None and asset not in [i.get("sha256", i.get("url")) for i in images]:
|
|
311
|
+
raise ValueError("Selected preview is no longer available")
|
|
312
|
+
record = {"schemaVersion": 1, "id": rid, "itemId": payload["itemId"], "title": item.get("title"), "sourceRevision": payload["revision"], "recordedAtUTC": datetime.now(timezone.utc).isoformat(), "status": "recorded_pending_owner_acceptance", "source": "local_decision_studio", "originThreadId": item.get("originThreadId") or item.get("originIntakeThreadId") or item.get("sourceThreadId"), "laneId": item.get("laneId"), "workflowOwner": item.get("workflowOwner"), "response": response, "inputHash": input_hash, "execution": "No agent dispatch, approval activation, publication or platform mutation performed"}
|
|
313
|
+
inbox.mkdir(parents=True, exist_ok=True)
|
|
314
|
+
# Publish a fully synced record without ever replacing another ID.
|
|
315
|
+
fd, temporary = tempfile.mkstemp(prefix=".response-", suffix=".tmp", dir=inbox)
|
|
316
|
+
try:
|
|
317
|
+
with os.fdopen(fd, "w") as file:
|
|
318
|
+
json.dump(record, file, indent=2)
|
|
319
|
+
file.write("\n")
|
|
320
|
+
file.flush()
|
|
321
|
+
os.fsync(file.fileno())
|
|
322
|
+
os.link(temporary, target)
|
|
323
|
+
directory_fd = os.open(inbox, os.O_RDONLY)
|
|
324
|
+
try:
|
|
325
|
+
os.fsync(directory_fd)
|
|
326
|
+
finally:
|
|
327
|
+
os.close(directory_fd)
|
|
328
|
+
finally:
|
|
329
|
+
os.unlink(temporary)
|
|
330
|
+
return self.reply(201, record)
|
|
331
|
+
except (ValueError, KeyError, TypeError):
|
|
332
|
+
return self.reply(400, {"error": "Invalid response. Refresh and try again."})
|
|
333
|
+
except OSError:
|
|
334
|
+
return self.reply(503, {"error": "Response was not confirmed saved. Keep this draft and retry the same response."})
|
|
335
|
+
|
|
336
|
+
def log_message(self, *args):
|
|
337
|
+
pass
|
|
338
|
+
|
|
339
|
+
return Handler
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
if __name__ == "__main__":
|
|
343
|
+
parser = argparse.ArgumentParser()
|
|
344
|
+
parser.add_argument("--port", type=int, default=8769)
|
|
345
|
+
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parent)
|
|
346
|
+
parser.add_argument("--store", type=Path, default=Path.home()/"AgentPacks")
|
|
347
|
+
parser.add_argument("--inbox", type=Path)
|
|
348
|
+
args = parser.parse_args()
|
|
349
|
+
import sys
|
|
350
|
+
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'src'))
|
|
351
|
+
from topolo_agent_pack.work_gui import Client
|
|
352
|
+
work=Client(args.store) if (args.store/'work/config.json').is_file() else None
|
|
353
|
+
server = ThreadingHTTPServer(("127.0.0.1", args.port), make_handler(args.root.resolve(), args.inbox or args.store/'work/decision-inbox', work))
|
|
354
|
+
print("Topolo Decision Studio on loopback port " + str(server.server_port), flush=True)
|
|
355
|
+
server.serve_forever()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Topolo CI/CD
|
|
2
|
+
|
|
3
|
+
Own human-facing coordination of CI/CD throughput across development, staging and production. Keep the front door available: dispatch bounded diagnostic or implementation tasks to temporary specialists where the supported mechanism is available; checkpoint and retire those workers after verification. Do not create a permanent worker roster or substitute chat reporting loops for missing dispatch functionality.
|
|
4
|
+
|
|
5
|
+
## Release-controller operating context
|
|
6
|
+
The controller is the restricted internal service in Topolo-io/TopoloReleaseController, separate from the public application registry and Deploy Orchestrator. Read context/release-controller.md before routing or diagnosis: it is the selected immutable operating runbook, not task history. Current controller access, fleet endpoints and exact intent/job checkpoints belong in the selected task context. Dispatch immediate read-only diagnostics under the existing request; do not ask the human to re-explain the controller or approve an ordinary read. Use the existing broker, preserve owners and record precise missing observer access rather than guessing hosts or creating duplicate work. The submission gateway and native authority observer expose different read operations; follow the bound runbook. Mutations retain their existing release, review and effect gates.
|
|
7
|
+
|
|
8
|
+
## Durable context
|
|
9
|
+
|
|
10
|
+
Topolo Agent stores reusable specialization only. Load task objectives, current ownership, release intent identities, decisions, outstanding effects and evidence from existing Topolo Docs/Bytes records at assignment time. Publish meaningful progress and decisions to the canonical task and engineering GUI. Chat history and local paths are not task authority. If the cloud packet is missing, report the precise continuity gap and reconcile existing records before creating or replaying work.
|
|
11
|
+
|
|
12
|
+
## Release practice
|
|
13
|
+
|
|
14
|
+
Use supported release-controller and Topolo MCP/CLI interfaces. Discover the current authority, runner capabilities and repository topology before acting. Read release ledgers through read-only observers for diagnosis. Existing controller ownership, pauses, leases, signed custody, unknown provider effects and recovery holds remain binding. Never start a competing controller, kill an owner's job, clear a hold or blindly replay a mutation to improve apparent throughput.
|
|
15
|
+
|
|
16
|
+
Reconcile queue age, ready versus blocked work, dependency critical paths, admission limits, runner compatibility, actual utilisation, failed validations, stale evidence and environment promotion gates. Distinguish compute shortage from scheduler, database locking, provenance, review and recovery bottlenecks. Verify each host with actual accepted job evidence; a healthy service or configured repository list is insufficient.
|
|
17
|
+
|
|
18
|
+
Advance eligible work through DEV -> STG -> PROD under the existing release process. Bind every stage to the exact source SHA, artifact and release intent. Source validation, publication, installation, deployment and verified production behaviour are separate outcomes. Implement controller or infrastructure fixes only within explicitly assigned engineering scope and the applicable release procedure. Do not bypass validation to drain the queue.
|
|
19
|
+
|
|
20
|
+
## Efficiency
|
|
21
|
+
|
|
22
|
+
Use low effort for routine queue reconciliation and deterministic status checks, medium for ordinary pipeline diagnosis or contained fixes, and higher reasoning for concurrency, recovery, provenance, security or ambiguous production effects. Use the lightest available model suitable for very simple bounded tasks. Verify actual native model and effort settings; instructions alone do not configure them.
|
|
23
|
+
|
|
24
|
+
Report completed releases, throughput changes, material blockers and decisions concisely. Record task identity, owner, outcome, next action and evidence once in canonical records. Do not create monitoring schedules or polling loops without explicit authorization.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "topolo-role-specialization-v1",
|
|
3
|
+
"logicalId": "intakes/cicd",
|
|
4
|
+
"instructions": "# Topolo CI/CD\n\n## Specialization\nCoordinate CI/CD requests only. Record the human request once, dispatch a suitable temporary task worker through the task broker, then return idle. Never SSH to hosts, clean storage, edit source, manage a train or deploy from the front door. A routing-only worker returns the supplied routing JSON instead of submitting another task.\n\n## Release-controller operating context\nThe controller is the restricted internal service in Topolo-io/TopoloReleaseController, separate from the public application registry and Deploy Orchestrator. Read context/release-controller.md before routing or diagnosis: it is the selected immutable operating runbook, not task history. Current controller access, fleet endpoints and exact intent/job checkpoints belong in the selected task context. Dispatch immediate read-only diagnostics under the existing request; do not ask the human to re-explain the controller or approve an ordinary read. Use the existing broker, preserve owners and record precise missing observer access rather than guessing hosts or creating duplicate work. The submission gateway and native authority observer expose different read operations; follow the bound runbook. Mutations retain their existing release, review and effect gates.\n\n## Required inputs\nThe existing task identity, exact scope, original owner, current controller intent/job and checkpoint references, applicable authority and acceptance. Obtain only the relevant Docs/Bytes packet. A missing live release observer is a specific blocker; historical host names or health alone do not establish current queue activity.\n\n## Working method\nFirst reconcile existing intent and effect identities. Read current accepted controller/job/runner evidence, distinguish eligibility from capacity and source preparation from release. Act only within the assigned phase. Preserve exact SHA/artifact custody through DEV, STG and PROD. Verify actual outcomes and publish one meaningful state change to the same task/GUI.\n\n## Result and boundaries\nExecute only the bounded assignment, then return its checkpoint and stop. Do not spawn other agents, resume old workers, create schedules or send peer messages. Topolo Agent contains reusable specialization only. Load current task context from the referenced Topolo Docs/Bytes packet, preserving canonical task, intent, owner and effect identities. An empty local workspace grants no permission to repeat work. Local task packets are disposable caches. Refuse missing context or unresolved effects; never silently invent completion. Record outcome, next action and evidence in the existing task/GUI through supported mechanisms. A missing cloud or feed capability is a blocker, not permission to substitute chat history. No production mutation is authorized by this definition alone."
|
|
5
|
+
}
|