@zq-silk/yui 2.1.0 → 2.2.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/README.md +4 -0
- package/dist/cli/commandCatalog.js +24 -3
- package/dist/cli/commandDiscovery.js +4 -1
- package/dist/cli.js +23 -3078
- package/dist/commands/taskCommands.js +79 -21
- package/dist/commands/taskIntegrationCommands.js +3 -1
- package/dist/commands/taskUpstreamCommands.js +3 -1
- package/dist/context/runContextPack.js +132 -5
- package/dist/context/sourceRunContext.js +4 -2
- package/dist/context/taskContext.js +6 -1
- package/dist/controlPlaneCli.js +3092 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +19 -6
- package/dist/controller/jobControl.js +21 -0
- package/dist/executor/agentExecutor.js +1 -1
- package/dist/executor/effectiveLaunch.js +18 -5
- package/dist/executor/fileRoleLaunchPlanner.js +15 -19
- package/dist/integration/gitIntegrationService.js +50 -15
- package/dist/message/messageContinuation.js +7 -2
- package/dist/nativeAgent/agent.js +176 -74
- package/dist/nativeAgent/cliDemo.js +37 -0
- package/dist/nativeAgent/codingTools.js +11 -0
- package/dist/nativeAgent/commandTool.js +215 -0
- package/dist/nativeAgent/compactionDemo.js +158 -0
- package/dist/nativeAgent/composition.js +71 -0
- package/dist/nativeAgent/context/budget.js +44 -0
- package/dist/nativeAgent/context/index.js +339 -0
- package/dist/nativeAgent/context/providerCompressor.js +103 -0
- package/dist/nativeAgent/demo.js +12 -1
- package/dist/nativeAgent/evaluation/cases.js +38 -0
- package/dist/nativeAgent/evaluation/checks.js +91 -0
- package/dist/nativeAgent/evaluation/demo.js +19 -0
- package/dist/nativeAgent/evaluation/files.js +54 -0
- package/dist/nativeAgent/evaluation/fixture.js +36 -0
- package/dist/nativeAgent/evaluation/index.js +239 -0
- package/dist/nativeAgent/executionOwner.js +209 -0
- package/dist/nativeAgent/filePatterns.js +170 -0
- package/dist/nativeAgent/fileToolsSupport.js +202 -0
- package/dist/nativeAgent/index.js +13 -0
- package/dist/nativeAgent/interaction/cli.js +358 -0
- package/dist/nativeAgent/interaction/contracts.js +1 -0
- package/dist/nativeAgent/interaction/index.js +3 -0
- package/dist/nativeAgent/interaction/memoryDemo.js +115 -0
- package/dist/nativeAgent/interaction/renderer.js +34 -0
- package/dist/nativeAgent/localSafety.js +258 -0
- package/dist/nativeAgent/model/anthropicMessages.js +204 -0
- package/dist/nativeAgent/model/chatCompletions.js +210 -0
- package/dist/nativeAgent/model/errors.js +47 -0
- package/dist/nativeAgent/model/gateway.js +423 -0
- package/dist/nativeAgent/model/index.js +7 -0
- package/dist/nativeAgent/model/observationAdapter.js +21 -0
- package/dist/nativeAgent/model/protocols.js +19 -0
- package/dist/nativeAgent/model/responses.js +263 -0
- package/dist/nativeAgent/model/types.js +1 -0
- package/dist/nativeAgent/model/wire.js +73 -0
- package/dist/nativeAgent/observability/index.js +220 -0
- package/dist/nativeAgent/product/catalog.js +82 -0
- package/dist/nativeAgent/product/config.js +295 -0
- package/dist/nativeAgent/product/facts.js +30 -0
- package/dist/nativeAgent/product/index.js +62 -0
- package/dist/nativeAgent/product/location.js +44 -0
- package/dist/nativeAgent/product/runtime.js +276 -0
- package/dist/nativeAgent/product/storage.js +49 -0
- package/dist/nativeAgent/product/tools.js +47 -0
- package/dist/nativeAgent/product/transport.js +54 -0
- package/dist/nativeAgent/projectGuidance/index.js +425 -0
- package/dist/nativeAgent/searchTools.js +305 -0
- package/dist/nativeAgent/session/backends.js +293 -0
- package/dist/nativeAgent/session/catalog.js +97 -0
- package/dist/nativeAgent/session/catalogDemo.js +87 -0
- package/dist/nativeAgent/session/contracts.js +1 -0
- package/dist/nativeAgent/session/format.js +269 -0
- package/dist/nativeAgent/session/index.js +5 -0
- package/dist/nativeAgent/session/location.js +36 -0
- package/dist/nativeAgent/session/sqliteFormat.js +134 -0
- package/dist/nativeAgent/session/store.js +248 -0
- package/dist/nativeAgent/textTools.js +270 -133
- package/dist/nativeAgent/toolManager/executor.js +290 -0
- package/dist/nativeAgent/toolManager/index.js +4 -0
- package/dist/nativeAgent/validation.js +2 -2
- package/dist/task/taskAuthority.js +56 -0
- package/dist/web/assets/client/app.js +64 -3
- package/dist/web/assets/client/detail.js +6 -6
- package/dist/web/assets/client/i18n.js +4 -0
- package/dist/web/assets/client/overview.js +12 -9
- package/dist/web/assets/client/sidebar.js +31 -2
- package/dist/web/assets/shell.js +2 -1
- package/dist/web/assets/styles/components.js +2 -0
- package/dist/web/assets/styles/layout.js +11 -4
- package/dist/web/assets/styles/responsive.js +12 -4
- package/dist/web/assets/styles/views.js +16 -13
- package/docs/agent-result-consumption.md +16 -0
- package/docs/agent-result-consumption.zh-CN.md +13 -0
- package/docs/examples/agent-offline.mjs +194 -0
- package/docs/native-agent.md +283 -0
- package/docs/release-workflow.md +47 -9
- package/docs/release-workflow.zh-CN.md +36 -6
- package/docs/roles-and-configuration.md +32 -0
- package/docs/roles-and-configuration.zh-CN.md +26 -0
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +9 -0
- package/skills/yui-reviewer/SKILL.md +5 -0
- package/skills/yui-runtime/SKILL.md +7 -0
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* LAYOUT — the three-region workspace.
|
|
3
3
|
* sidebar | center (task details) | divider | dock (session)
|
|
4
4
|
* body.dock-open shows the dock; body.dock-center swaps the dock into the
|
|
5
|
-
* middle so the order becomes sidebar | dock | divider | center.
|
|
5
|
+
* middle so the order becomes sidebar | dock | divider | center. The sidebar's
|
|
6
|
+
* own divider overlays its right edge, so it adds no grid column; --sidebar-w
|
|
7
|
+
* is the default width and a stored preference overrides it inline.
|
|
6
8
|
*/
|
|
7
9
|
export const LAYOUT_STYLES = `
|
|
8
10
|
.app{display:grid;height:100vh;height:100dvh;grid-template-rows:minmax(0,1fr);
|
|
@@ -12,16 +14,21 @@ body.dock-open .app{grid-template-columns:var(--sidebar-w) minmax(0,1fr) var(--d
|
|
|
12
14
|
body.dock-open.dock-center .app{grid-template-areas:"sidebar dock divider center";grid-template-columns:var(--sidebar-w) var(--dock-w) var(--divider-w) minmax(0,1fr)}
|
|
13
15
|
|
|
14
16
|
/* Sidebar */
|
|
15
|
-
.sidebar{grid-area:sidebar;display:flex;flex-direction:column;min-width:0;min-height:0;background:var(--surface);border-right:1px solid var(--line)}
|
|
17
|
+
.sidebar{grid-area:sidebar;display:flex;flex-direction:column;min-width:0;min-height:0;background:var(--surface);border-right:1px solid var(--line);container:sidebar/inline-size}
|
|
18
|
+
.sidebar-divider{grid-area:sidebar;justify-self:end;position:relative;z-index:20;width:9px;margin-right:-5px;cursor:col-resize;touch-action:none}
|
|
19
|
+
.sidebar-divider span{position:absolute;top:0;bottom:0;left:3px;width:1px;background:transparent;transition:background var(--t-fast),box-shadow var(--t-fast)}
|
|
20
|
+
.sidebar-divider:hover span,.sidebar-divider:focus-visible span,body.sidebar-resizing .sidebar-divider span{background:var(--accent);box-shadow:0 0 0 1px var(--accent)}
|
|
21
|
+
.sidebar-divider:focus-visible{box-shadow:none}
|
|
22
|
+
body.sidebar-resizing{cursor:col-resize;user-select:none}
|
|
16
23
|
.side-head{flex:none;display:flex;align-items:center;gap:10px;height:var(--header-h);padding:0 12px 0 16px;border-bottom:1px solid var(--line)}
|
|
17
24
|
.brand{display:flex;align-items:center;gap:10px;flex:1;min-width:0}
|
|
18
25
|
.search-field{flex:none;margin:12px 12px 0}
|
|
19
26
|
.attention-bar{flex:none;margin:10px 12px 0}
|
|
20
27
|
.status-tabs{flex:none;margin:10px 0 0;padding:0 12px}
|
|
21
|
-
.task-list{flex:1;min-height:0;overflow-y:auto;padding:
|
|
28
|
+
.task-list{flex:1;min-height:0;overflow-y:auto;padding:4px 8px 12px;scrollbar-gutter:stable}
|
|
22
29
|
.pager{flex:none;display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:6px;padding:8px 12px;border-top:1px solid var(--line)}
|
|
23
30
|
.pager .pager-clear{grid-column:1/-1}
|
|
24
|
-
.side-foot{flex:none;display:flex;align-items:center;gap:
|
|
31
|
+
.side-foot{flex:none;display:flex;align-items:center;gap:4px;padding:10px;border-top:1px solid var(--line)}
|
|
25
32
|
|
|
26
33
|
/* Center */
|
|
27
34
|
.center{grid-area:center;min-width:0;min-height:0;overflow-y:auto;scrollbar-gutter:stable;background:var(--canvas)}
|
|
@@ -3,15 +3,23 @@
|
|
|
3
3
|
* ≤1200px narrower sidebar and dock defaults
|
|
4
4
|
* ≤900px master–detail: task list OR task page; the dock becomes a
|
|
5
5
|
* full-screen sheet over the task page
|
|
6
|
+
* Sidebar container queries adapt its footer to the width the user chose.
|
|
6
7
|
*/
|
|
7
8
|
export const RESPONSIVE_STYLES = `
|
|
9
|
+
@container sidebar (max-width:299px){
|
|
10
|
+
.foot-btn kbd{display:none}
|
|
11
|
+
}
|
|
12
|
+
@container sidebar (max-width:259px){
|
|
13
|
+
.foot-btn{gap:0;padding:0 9px}
|
|
14
|
+
.foot-btn span{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
15
|
+
}
|
|
8
16
|
@media(max-width:1200px){
|
|
9
17
|
:root{--sidebar-w:264px}
|
|
10
|
-
.card-grid{grid-template-columns:minmax(0,1fr)}
|
|
18
|
+
.card-grid,.card-columns{grid-template-columns:minmax(0,1fr)}
|
|
11
19
|
.metric-strip{grid-template-columns:repeat(3,minmax(0,1fr))}
|
|
12
20
|
}
|
|
13
21
|
@media(max-width:1040px){
|
|
14
|
-
body.dock-open .overview-
|
|
22
|
+
body.dock-open .overview-layout{grid-template-columns:minmax(0,1fr)}
|
|
15
23
|
}
|
|
16
24
|
@media(max-width:900px){
|
|
17
25
|
.app,body.dock-open .app,body.dock-open.dock-center .app{grid-template-areas:"main";grid-template-columns:minmax(0,1fr)}
|
|
@@ -21,10 +29,10 @@ export const RESPONSIVE_STYLES = `
|
|
|
21
29
|
body.task-open .center{display:block}
|
|
22
30
|
.back-btn{display:inline-grid}
|
|
23
31
|
.crumbs .crumb,.crumbs .crumb-sep{display:none}
|
|
24
|
-
.dock-divider{display:none}
|
|
32
|
+
.dock-divider,.sidebar-divider{display:none}
|
|
25
33
|
.dock{position:fixed;inset:0;z-index:60;padding-top:env(safe-area-inset-top,0px);padding-bottom:env(safe-area-inset-bottom,0px)}
|
|
26
34
|
#dock-swap{display:none}
|
|
27
|
-
.overview-
|
|
35
|
+
.overview-layout{grid-template-columns:minmax(0,1fr)}
|
|
28
36
|
.dock-toggle span{display:none}
|
|
29
37
|
.dock-toggle{width:32px;padding:0}
|
|
30
38
|
}
|
|
@@ -25,29 +25,32 @@ export const VIEW_STYLES = `
|
|
|
25
25
|
.attention-pill[aria-pressed="true"]{background:var(--tone);color:var(--canvas)}
|
|
26
26
|
.attention-clear{display:flex;align-items:center;gap:6px;color:var(--ink-4);font-size:var(--fs-xs)}
|
|
27
27
|
.attention-clear .icon{color:var(--ok)}
|
|
28
|
-
.tabs-row{display:flex;gap:2px;overflow-x:auto;scrollbar-width:none;padding-bottom:2px;border-bottom:1px solid var(--line)}
|
|
28
|
+
.tabs-row{position:relative;display:flex;gap:2px;overflow-x:auto;scrollbar-width:none;padding-bottom:2px;border-bottom:1px solid var(--line)}
|
|
29
|
+
.tabs-row[data-fade="end"]{-webkit-mask-image:linear-gradient(90deg,#000 calc(100% - 28px),transparent);mask-image:linear-gradient(90deg,#000 calc(100% - 28px),transparent)}
|
|
30
|
+
.tabs-row[data-fade="start"]{-webkit-mask-image:linear-gradient(90deg,transparent,#000 28px);mask-image:linear-gradient(90deg,transparent,#000 28px)}
|
|
31
|
+
.tabs-row[data-fade="both"]{-webkit-mask-image:linear-gradient(90deg,transparent,#000 28px,#000 calc(100% - 28px),transparent);mask-image:linear-gradient(90deg,transparent,#000 28px,#000 calc(100% - 28px),transparent)}
|
|
29
32
|
.tabs-row::-webkit-scrollbar{display:none}
|
|
30
33
|
.status-tab{position:relative;display:inline-flex;align-items:center;gap:5px;flex:none;height:32px;padding:0 8px;border:0;background:transparent;color:var(--ink-3);font-size:var(--fs-xs);font-weight:500;white-space:nowrap}
|
|
31
34
|
.status-tab:hover{color:var(--ink)}
|
|
32
35
|
.status-tab[aria-pressed="true"]{color:var(--ink)}
|
|
33
36
|
.status-tab[aria-pressed="true"]::after{content:"";position:absolute;left:6px;right:6px;bottom:-3px;height:2px;border-radius:2px;background:var(--accent)}
|
|
34
37
|
.status-tab-count{color:var(--ink-4);font-size:var(--fs-2xs);font-variant-numeric:tabular-nums}
|
|
35
|
-
.task-group{margin-top:
|
|
36
|
-
.task-group-head{display:flex;align-items:center;gap:6px;padding:4px 8px
|
|
38
|
+
.task-group{margin-top:8px}
|
|
39
|
+
.task-group-head{display:flex;align-items:center;gap:6px;padding:4px 8px 4px;color:var(--ink-4);font-size:var(--fs-2xs);font-weight:600;text-transform:uppercase;letter-spacing:.07em}
|
|
37
40
|
.task-group-head .count{background:transparent;padding:0;min-width:0}
|
|
38
|
-
.task-row{position:relative;display:flex;align-items:flex-start;gap:
|
|
39
|
-
.task-row>.dot{margin-top:
|
|
41
|
+
.task-row{position:relative;display:flex;align-items:flex-start;gap:9px;width:100%;padding:7px 10px;border:0;border-radius:var(--r-md);background:transparent;text-align:left;transition:background var(--t-fast)}
|
|
42
|
+
.task-row>.dot{margin-top:5px}
|
|
40
43
|
.task-row:hover{background:var(--surface-3)}
|
|
41
44
|
.task-row[aria-current="true"]{background:var(--surface-3);box-shadow:inset 0 0 0 1px var(--line-2)}
|
|
42
|
-
.task-row[aria-current="true"]::before{content:"";position:absolute;left
|
|
43
|
-
.task-body{display:grid;gap:
|
|
45
|
+
.task-row[aria-current="true"]::before{content:"";position:absolute;left:0;top:9px;bottom:9px;width:3px;border-radius:0 3px 3px 0;background:var(--accent)}
|
|
46
|
+
.task-body{display:grid;gap:1px;flex:1;min-width:0}
|
|
44
47
|
.task-line{display:flex;align-items:center;gap:8px;min-width:0}
|
|
45
|
-
.task-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ink);font-size:var(--fs-md);font-weight:500}
|
|
48
|
+
.task-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ink);font-size:var(--fs-md);font-weight:500;line-height:1.35}
|
|
46
49
|
.task-time{flex:none;color:var(--ink-4);font-size:var(--fs-2xs);font-variant-numeric:tabular-nums}
|
|
47
50
|
.task-sub{gap:8px;height:18px;color:var(--ink-4);font-size:var(--fs-2xs)}
|
|
48
51
|
.task-id{font-size:var(--fs-2xs);line-height:1;color:var(--ink-3)}
|
|
49
|
-
.task-
|
|
50
|
-
.task-
|
|
52
|
+
.task-count{display:inline-flex;align-items:center;gap:3px;line-height:1;white-space:nowrap}
|
|
53
|
+
.task-count .icon{width:11px;height:11px}
|
|
51
54
|
.task-signals{display:inline-flex;gap:4px;margin-left:auto}
|
|
52
55
|
.signal{display:inline-flex;align-items:center;gap:3px;height:18px;padding:0 6px;border-radius:var(--r-pill);background:var(--tone-soft);color:var(--tone);font-size:10.5px;font-weight:600;font-variant-numeric:tabular-nums}
|
|
53
56
|
.signal .icon{width:11px;height:11px;stroke-width:2}
|
|
@@ -59,12 +62,11 @@ export const VIEW_STYLES = `
|
|
|
59
62
|
.pager-count{grid-column:2;text-align:center;color:var(--ink-4);font-size:var(--fs-2xs);font-variant-numeric:tabular-nums}
|
|
60
63
|
.pager-clear{height:28px;border:1px dashed var(--line-3);border-radius:var(--r-sm);background:transparent;color:var(--ink-3);font-size:var(--fs-xs)}
|
|
61
64
|
.pager-clear:hover{color:var(--accent);border-color:var(--accent-line)}
|
|
62
|
-
.foot-btn{display:inline-flex;align-items:center;gap:
|
|
65
|
+
.foot-btn{display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 8px;border:1px solid var(--line-2);border-radius:var(--r-sm);background:transparent;color:var(--ink-2);font-size:var(--fs-xs);font-weight:500;min-width:0}
|
|
63
66
|
.foot-btn span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
64
67
|
.foot-btn:hover{color:var(--ink);border-color:var(--line-3);background:var(--surface-3)}
|
|
65
68
|
.foot-btn[aria-pressed="true"]{color:var(--accent);border-color:var(--accent-line);background:var(--accent-soft)}
|
|
66
69
|
.foot-btn kbd{margin-left:2px}
|
|
67
|
-
.side-foot .foot-btn:first-child{flex:1}
|
|
68
70
|
.side-foot .icon-btn{margin-left:auto}
|
|
69
71
|
|
|
70
72
|
/* ---------- Page scaffolding ---------- */
|
|
@@ -83,7 +85,8 @@ export const VIEW_STYLES = `
|
|
|
83
85
|
.metric-strip{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px;margin-bottom:14px}
|
|
84
86
|
.metric-strip .metric{padding:14px 16px;border-radius:var(--r-lg)}
|
|
85
87
|
.metric-strip .metric-value{font-size:var(--fs-2xl)}
|
|
86
|
-
.overview-
|
|
88
|
+
.overview-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(300px,380px);gap:14px;align-items:start}
|
|
89
|
+
.overview-col{display:flex;flex-direction:column;gap:14px;min-width:0}
|
|
87
90
|
.overview>.card{margin-bottom:14px}
|
|
88
91
|
.row-list{display:grid;gap:2px;margin:-4px -8px}
|
|
89
92
|
.list-row{display:flex;align-items:center;gap:10px;width:100%;min-height:38px;padding:7px 8px;border:0;border-radius:var(--r-sm);background:transparent;color:var(--ink);text-align:left;font-size:var(--fs-sm)}
|
|
@@ -65,6 +65,22 @@ silently rerun successful Producers.
|
|
|
65
65
|
|
|
66
66
|
## Review
|
|
67
67
|
|
|
68
|
+
Capturing a Run-backed WorkItem Candidate freezes its exact source report and
|
|
69
|
+
Candidate identity in the existing immutable Context Snapshot store. Review
|
|
70
|
+
receives a `source-run` pointer with `evidenceOf=<work-item>/<candidate>` and
|
|
71
|
+
expands the original report through `task run context expand`, including all
|
|
72
|
+
content pages. Source, Candidate or report drift refuses a new dispatch; already
|
|
73
|
+
frozen Run Context remains readable. Task-final producer evidence requires an
|
|
74
|
+
exact Candidate head or a committed Integration at the fixed head and Task-main
|
|
75
|
+
target. Earlier attempts sharing a target are not ancestry proof. A later direct
|
|
76
|
+
Task-main repair remains reviewable without a matching producer report: the
|
|
77
|
+
ReviewRound pointer identifies the evidence gap. Attached reports describe their
|
|
78
|
+
named Candidate, not validation of the entire Task head; a Reviewer assesses
|
|
79
|
+
the fixed code and reports missing validation rather than borrowing old success.
|
|
80
|
+
Historical Candidates without this frozen proof remain inspectable, but need
|
|
81
|
+
newly captured delivery evidence before a new Candidate Review; old Snapshots
|
|
82
|
+
are never backfilled.
|
|
83
|
+
|
|
68
84
|
ReviewRound owns frozen Candidate or Task-head identity, workspace provenance,
|
|
69
85
|
execution topology, exact main Reviewer Run, lifecycle and Core diagnostics.
|
|
70
86
|
Its completed state is structural execution evidence, not a “pass” extracted
|
|
@@ -52,6 +52,19 @@ Review 结果。Producer 从不直接进入 Integration 或验收。重试针对
|
|
|
52
52
|
|
|
53
53
|
## Review
|
|
54
54
|
|
|
55
|
+
捕获 Run 来源的 WorkItem Candidate 时,会在现有不可变 Context Snapshot 存储中
|
|
56
|
+
冻结其精确原始报告与 Candidate 身份。Review 获得带
|
|
57
|
+
`evidenceOf=<work-item>/<candidate>` 的 `source-run` 指针,通过
|
|
58
|
+
`task run context expand` 读取原文与所有内容分页。来源、Candidate 或报告漂移会
|
|
59
|
+
拒绝新的派发,但已经冻结的 Run Context 仍可读取。Task-final 来源证据需要精确
|
|
60
|
+
匹配 Candidate 提交,或匹配固定 head 与 Task-main 目标的已提交 Integration。
|
|
61
|
+
仅共享目标的历史 attempts 不是血缘证明。Task-main 后续直接修复没有匹配报告时,
|
|
62
|
+
仍可审查,ReviewRound 指针明确提示证据缺口。附带报告只描述其命名的 Candidate,
|
|
63
|
+
不是整个 Task head 已通过验证的证明;Reviewer 应审查固定代码、报告验证缺口,
|
|
64
|
+
不能借用旧成功结论。
|
|
65
|
+
缺少冻结证据的历史 Candidate 仍可诊断;新的 Candidate Review 需要新捕获的交付
|
|
66
|
+
证据,不回填旧 Snapshot。
|
|
67
|
+
|
|
55
68
|
ReviewRound 拥有冻结的 Candidate 或 Task-head 身份、工作区来源、执行拓扑、精确
|
|
56
69
|
的 main Reviewer Run、生命周期和 Core 诊断。它的 completed 状态是结构性执行证据,
|
|
57
70
|
而不是从报告里提取出来的“通过”。
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises';
|
|
5
|
+
import { createServer } from 'node:http';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const packageRoot = fileURLToPath(new URL('../../', import.meta.url));
|
|
11
|
+
const localLauncher = join(packageRoot, 'output/dev/bin/yui');
|
|
12
|
+
const directory = await mkdtemp(join(tmpdir(), 'native-agent-offline-'));
|
|
13
|
+
const workingDirectory = join(directory, 'child');
|
|
14
|
+
const launchDirectory = join(directory, 'different-launch');
|
|
15
|
+
const secret = 'dummy-offline-example-token';
|
|
16
|
+
const executable = realpathSync(process.execPath);
|
|
17
|
+
// Trusted fixture code reviewed here, not arbitrary text supplied by the model.
|
|
18
|
+
const argv = ['-e',
|
|
19
|
+
'const fs=require("node:fs");if(fs.readFileSync("../input.txt","utf8")!=="after\\n"||process.env.EXAMPLE_CREDENTIAL)process.exit(1);console.log("local check passed");'];
|
|
20
|
+
const commandConfig = JSON.stringify({ env: {}, specs: [{ executable, argv, effect: 'read' }] });
|
|
21
|
+
let phase = 'read', step = 0, active;
|
|
22
|
+
let initialSkillBody = false, loadedSkillBody = false, commandFromCurrentFacts = false;
|
|
23
|
+
const server = createServer(async (request, response) => {
|
|
24
|
+
let source = '';
|
|
25
|
+
for await (const part of request) source += part;
|
|
26
|
+
const input = JSON.parse(source);
|
|
27
|
+
assert.equal(request.headers.authorization, `Bearer ${secret}`);
|
|
28
|
+
const call = (id, name, args) => ({ id, type: 'function',
|
|
29
|
+
function: { name, arguments: JSON.stringify(args) } });
|
|
30
|
+
let calls;
|
|
31
|
+
assert.equal(input.messages.filter(message => message.role === 'system').length, 1);
|
|
32
|
+
const material = input.messages.filter(message => message.role === 'user')
|
|
33
|
+
.map(message => { try { return JSON.parse(message.content).contextMaterial; } catch { return undefined; } })
|
|
34
|
+
.find(material => material?.loader === 'product-runtime');
|
|
35
|
+
assert.ok(material, 'The actual model request must include current runtime facts');
|
|
36
|
+
const currentFacts = JSON.parse(material.content);
|
|
37
|
+
assert.deepEqual({ root: currentFacts.root, cwd: currentFacts.cwd },
|
|
38
|
+
{ root: directory, cwd: workingDirectory });
|
|
39
|
+
assert.ok(!source.includes(secret));
|
|
40
|
+
const configuredCheck = id => {
|
|
41
|
+
const spec = currentFacts.commands[0];
|
|
42
|
+
assert.ok(spec, 'Choose the configured check from the actual model request');
|
|
43
|
+
commandFromCurrentFacts = true;
|
|
44
|
+
return call(id, 'command', { command: spec.executable, argv: spec.argv, cwd: currentFacts.cwd });
|
|
45
|
+
};
|
|
46
|
+
if (phase === 'read' && step++ === 0) calls = [call('read-example', 'read', { path: 'input.txt' })];
|
|
47
|
+
else if (phase === 'edit' && step++ === 0) {
|
|
48
|
+
const previousRead = input.messages.find(message => message.role === 'tool');
|
|
49
|
+
const sha256 = JSON.parse(JSON.parse(previousRead.content).content).sha256;
|
|
50
|
+
calls = [call('edit-example', 'edit',
|
|
51
|
+
{ path: 'input.txt', expectedSha256: sha256, oldText: 'before', newText: 'after' })];
|
|
52
|
+
} else if (phase === 'edit' && step === 2) calls = [configuredCheck('check-example')];
|
|
53
|
+
else if (phase === 'reopen' && step++ === 0) {
|
|
54
|
+
assert.deepEqual(currentFacts.commands, []);
|
|
55
|
+
assert.equal(currentFacts.grants.allowCommand, false);
|
|
56
|
+
// Deliberately replay the old command as a negative authorization probe.
|
|
57
|
+
calls = [call('reopen-check', 'command', { command: executable, cwd: workingDirectory, argv })];
|
|
58
|
+
}
|
|
59
|
+
else if (phase === 'restored' && step++ === 0) calls = [call('restored-read', 'read', { path: 'input.txt' })];
|
|
60
|
+
else if (phase === 'restored' && step === 2) calls = [configuredCheck('restored-check')];
|
|
61
|
+
else if (phase === 'skill') {
|
|
62
|
+
const loaded = input.messages.some(message => message.content.includes('COMPLETE_OFFLINE_SKILL_BODY'));
|
|
63
|
+
if (step++ === 0) {
|
|
64
|
+
initialSkillBody = loaded;
|
|
65
|
+
calls = [call('skill-example', 'project_context', {
|
|
66
|
+
action: 'load_skill', locator: '.agents/skills/check/SKILL.md' })];
|
|
67
|
+
} else loadedSkillBody = loaded;
|
|
68
|
+
assert.ok(input.messages.filter(message => message.content.includes('COMPLETE_OFFLINE_SKILL_BODY'))
|
|
69
|
+
.every(message => message.role === 'user'));
|
|
70
|
+
} else if (['memory-denied', 'memory-write', 'memory-reopen'].includes(phase) && step++ === 0)
|
|
71
|
+
calls = [call(phase, 'project_memory', {
|
|
72
|
+
action: 'replace', expectedSha256: null, content: 'offline confirmed experience' })];
|
|
73
|
+
response.setHeader('content-type', 'application/json');
|
|
74
|
+
response.end(JSON.stringify({ choices: [{ index: 0, message: { role: 'assistant', content: 'Offline facts saved',
|
|
75
|
+
...(calls ? { tool_calls: calls } : {}) }, finish_reason: calls ? 'tool_calls' : 'stop' }] }));
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
await mkdir(workingDirectory);
|
|
79
|
+
await mkdir(launchDirectory);
|
|
80
|
+
await writeFile(join(directory, 'input.txt'), 'before\n');
|
|
81
|
+
await mkdir(join(directory, '.agents/skills/check'), { recursive: true });
|
|
82
|
+
await writeFile(join(directory, '.agents/skills/check/SKILL.md'),
|
|
83
|
+
'---\nname: check\ndescription: Offline check.\nallowed-tools: command\n---\nCOMPLETE_OFFLINE_SKILL_BODY');
|
|
84
|
+
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
85
|
+
const shared = ['--allow-http', '--endpoint', `http://127.0.0.1:${server.address().port}/chat`,
|
|
86
|
+
'--model', 'offline-fixture', '--credential-ref', 'env:EXAMPLE_CREDENTIAL',
|
|
87
|
+
'--state-dir', join(directory, 'state')];
|
|
88
|
+
const execute = (args, expectedCode = 0, command = 'run') => new Promise((resolve, reject) => {
|
|
89
|
+
// Installed package uses its public bin; source checkout uses the required absolute local launcher.
|
|
90
|
+
active = spawn(existsSync(localLauncher) ? localLauncher : process.execPath,
|
|
91
|
+
[...(existsSync(localLauncher) ? [] : [join(packageRoot, 'dist/cli.js')]), 'agent', command,
|
|
92
|
+
...(command === 'run' ? [...shared,
|
|
93
|
+
...(args.includes('--session') ? [] : ['--root', directory, '--cwd', workingDirectory])]
|
|
94
|
+
: ['--state-dir', join(directory, 'state'), '--credential-ref', 'env:EXAMPLE_CREDENTIAL']),
|
|
95
|
+
...args],
|
|
96
|
+
{ env: { PATH: `${dirname(process.execPath)}:/usr/bin:/bin`, TMPDIR: tmpdir(),
|
|
97
|
+
YUI_HOME: join(directory, 'unused-control-home'), EXAMPLE_CREDENTIAL: secret },
|
|
98
|
+
cwd: launchDirectory, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
99
|
+
let output = '', error = '';
|
|
100
|
+
active.stdout.on('data', data => { output += data; });
|
|
101
|
+
active.stderr.on('data', data => { error += data; });
|
|
102
|
+
active.on('error', reject);
|
|
103
|
+
active.on('close', code => {
|
|
104
|
+
active = undefined;
|
|
105
|
+
if (code !== expectedCode) reject(new Error(`Offline entry failed (${code}): ${error}`));
|
|
106
|
+
else resolve(JSON.parse(output || error));
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
const first = await execute(['--input', 'Read input.txt']);
|
|
110
|
+
phase = 'edit'; step = 0;
|
|
111
|
+
const second = await execute(['--session', first.receipt.sessionId,
|
|
112
|
+
'--tools', 'read,edit,command', '--allow-write', '--allow-command', '--command-config', commandConfig,
|
|
113
|
+
'--input', 'Edit and check']);
|
|
114
|
+
assert.equal(await readFile(join(directory, 'input.txt'), 'utf8'), 'after\n');
|
|
115
|
+
assert.equal(second.receipt.sessionId, first.receipt.sessionId);
|
|
116
|
+
assert.equal(second.receipt.source.durability, 'persistent');
|
|
117
|
+
const checked = second.result.messages.find(message => message.role === 'tool' && message.name === 'command');
|
|
118
|
+
assert.equal(JSON.parse(checked.outcome.content).exitCode, 0);
|
|
119
|
+
assert.equal(JSON.parse(checked.outcome.content).processGroup, 'absent');
|
|
120
|
+
assert.ok(second.receipt.revision > first.receipt.revision);
|
|
121
|
+
assert.ok(!JSON.stringify(second).includes(secret));
|
|
122
|
+
assert.ok(!(await readFile(join(directory, 'state', 'sessions.sqlite'))).includes(Buffer.from(secret)));
|
|
123
|
+
assert.ok(!existsSync(join(directory, 'unused-control-home')));
|
|
124
|
+
phase = 'reopen'; step = 0;
|
|
125
|
+
const third = await execute(['--session', first.receipt.sessionId, '--input', 'Reopen without granting command'], 1);
|
|
126
|
+
assert.equal(third.result.reason, 'error');
|
|
127
|
+
assert.equal(third.binding.allowCommand, false);
|
|
128
|
+
assert.equal(third.binding.allowWrite, false);
|
|
129
|
+
assert.equal(third.receipt.sessionId, first.receipt.sessionId);
|
|
130
|
+
assert.ok(third.receipt.revision > second.receipt.revision);
|
|
131
|
+
const resume = ['--session', first.receipt.sessionId];
|
|
132
|
+
phase = 'skill'; step = 0;
|
|
133
|
+
await execute([...resume, '--input', 'Load the relevant check Skill completely']);
|
|
134
|
+
assert.equal(initialSkillBody, false);
|
|
135
|
+
assert.equal(loadedSkillBody, true);
|
|
136
|
+
phase = 'memory-denied'; step = 0;
|
|
137
|
+
const denied = await execute([...resume, '--allow-write', '--input', 'Try memory without its grant']);
|
|
138
|
+
assert.equal(denied.result.messages.filter(message => message.role === 'tool'
|
|
139
|
+
&& message.name === 'project_memory').at(-1).outcome.error.code, 'permission_denied');
|
|
140
|
+
assert.ok(!existsSync(join(directory, '.agents/MEMORY.md')));
|
|
141
|
+
phase = 'memory-write'; step = 0;
|
|
142
|
+
await execute([...resume, '--allow-memory-write', '--input', 'Save confirmed experience']);
|
|
143
|
+
assert.equal(await readFile(join(directory, '.agents/MEMORY.md'), 'utf8'), 'offline confirmed experience');
|
|
144
|
+
phase = 'memory-reopen'; step = 0;
|
|
145
|
+
const ungranted = await execute([...resume, '--input', 'Reopen without memory grant']);
|
|
146
|
+
assert.equal(ungranted.result.messages.filter(message => message.role === 'tool'
|
|
147
|
+
&& message.name === 'project_memory').at(-1).outcome.error.code, 'permission_denied');
|
|
148
|
+
assert.equal(ungranted.projectAuthority.allowMemoryWrite, false);
|
|
149
|
+
phase = 'read'; step = 0;
|
|
150
|
+
const another = await execute(['--input', 'Create another explicitly separate Session']);
|
|
151
|
+
for (const saved of [first, another]) await execute(['--session', saved.receipt.sessionId,
|
|
152
|
+
'--title', '"Same title"', '--expected-metadata-revision', '0'], 0, 'rename');
|
|
153
|
+
const page = await execute(['--limit', '1'], 0, 'sessions');
|
|
154
|
+
const next = await execute(['--limit', '1', '--cursor', page.nextCursor], 0, 'sessions');
|
|
155
|
+
assert.notEqual(page.items[0].sessionId, next.items[0].sessionId);
|
|
156
|
+
assert.equal(page.items[0].title, next.items[0].title);
|
|
157
|
+
assert.ok([page.items[0], next.items[0]].some(item => item.sessionId === first.receipt.sessionId));
|
|
158
|
+
const facts = await execute([...resume, '--limit', '2'], 0, 'history');
|
|
159
|
+
assert.equal(facts.records.length, 2);
|
|
160
|
+
const detail = await execute(resume, 0, 'session-info');
|
|
161
|
+
assert.equal(detail.sessionId, first.receipt.sessionId);
|
|
162
|
+
assert.deepEqual(detail.location, { root: directory, cwd: workingDirectory });
|
|
163
|
+
await execute([...resume, '--title', '"Chosen exact ID"', '--expected-metadata-revision', '1'], 0, 'rename');
|
|
164
|
+
const stale = await execute(['--limit', '1', '--cursor', page.nextCursor], 1, 'sessions');
|
|
165
|
+
assert.equal(stale.field, 'cursor_stale');
|
|
166
|
+
await execute(['--limit', '1'], 0, 'sessions'); // Explicit refresh, no automatic cursor fallback.
|
|
167
|
+
await execute([...resume, '--limit', '2', '--cursor', facts.nextCursor], 0, 'history');
|
|
168
|
+
phase = 'restored'; step = 0;
|
|
169
|
+
const restored = await execute([...resume, '--tools', 'read,command', '--allow-command',
|
|
170
|
+
'--command-config', commandConfig, '--input', 'Continue by exact catalog ID with fresh command authority']);
|
|
171
|
+
assert.equal(restored.binding.root, directory);
|
|
172
|
+
assert.equal(restored.binding.cwd, workingDirectory);
|
|
173
|
+
assert.equal(restored.receipt.sessionId, detail.sessionId);
|
|
174
|
+
assert.ok(restored.receipt.revision > detail.revision);
|
|
175
|
+
const restoredCheck = restored.result.messages.filter(message => message.role === 'tool' && message.name === 'command').at(-1);
|
|
176
|
+
assert.equal(JSON.parse(restoredCheck.outcome.content).exitCode, 0);
|
|
177
|
+
console.log(JSON.stringify({ offline: true, editedAndChecked: true, resumedExactId: true,
|
|
178
|
+
persistentReceiptVerified: true, exactCommandAuthority: true, reopenDefaultsReadOnly: true,
|
|
179
|
+
progressiveCompleteSkill: true, memoryRequiresSeparateInvocationGrant: true,
|
|
180
|
+
persistentCatalogDiscovered: true, duplicateTitlesSelectedById: true, boundedHistoryRead: true,
|
|
181
|
+
explicitStaleCursorRefresh: true, controlHomeCreated: false,
|
|
182
|
+
originalRootCwdRestoredAcrossLaunchDirectories: true, rootDiffersFromCwd: true,
|
|
183
|
+
freshCommandAuthorizationAfterCatalogSelection: true,
|
|
184
|
+
commandChosenFromCurrentModelRequest: commandFromCurrentFacts }));
|
|
185
|
+
} finally {
|
|
186
|
+
if (active) {
|
|
187
|
+
const stopped = new Promise(resolve => active.once('close', resolve));
|
|
188
|
+
active.kill('SIGTERM');
|
|
189
|
+
await stopped;
|
|
190
|
+
}
|
|
191
|
+
server.closeAllConnections();
|
|
192
|
+
await new Promise(resolve => server.close(resolve));
|
|
193
|
+
await rm(directory, { recursive: true, force: true });
|
|
194
|
+
}
|