@loomhelm/multi-task-flow 0.2.8 → 0.2.10
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 +14 -14
- package/package.json +2 -2
- package/src/cli/flow.ts +13 -4
- package/src/cli/service.ts +26 -0
- package/src/cli.ts +3 -1
- package/toolkit/artifact-manifest.json +69 -73
- package/toolkit/package.json +1 -1
- package/toolkit/skills/bin/check_source_policy.ts +13 -7
- package/toolkit/skills/bin/multi_task_platform_gate.ts +2 -2
- package/toolkit/skills/bin/tests/core/package_scripts.test.ts +1 -1
- package/toolkit/skills/bin/tests/plan/multi-task-flow.test.ts +3 -3
- package/toolkit/skills/catalog.json +0 -1
- package/toolkit/skills/docs/vendor/dot-codex/design/multi-task-flow/data-and-scripts.md +8 -6
- package/toolkit/skills/docs/vendor/dot-codex/design/multi-task-flow/execution.md +1 -1
- package/toolkit/skills/docs/vendor/dot-codex/design/multi-task-flow/plan-revision-and-change-audit.md +1 -1
- package/toolkit/skills/docs/vendor/dot-codex/design/multi-task-flow/web-template.md +1 -1
- package/toolkit/skills/docs/vendor/dot-codex/specs/a4ge9-stable-preview-runtime/IMPLEMENTATION.md +0 -2
- package/toolkit/skills/docs/vendor/dot-codex/specs/a4ge9-stable-preview-runtime/SPEC.md +0 -1
- package/toolkit/skills/docs/vendor/dot-codex/specs/dev-plan-standalone-typescript-finalizer/IMPLEMENTATION.md +1 -1
- package/toolkit/skills/docs/vendor/dot-codex/specs/multi-task-flow/IMPLEMENTATION.md +3 -3
- package/toolkit/skills/docs/vendor/dot-codex/specs/multi-task-flow/SCHEMA.md +4 -4
- package/toolkit/skills/docs/vendor/dot-codex/specs/stable-preview-runtime/IMPLEMENTATION.md +0 -2
- package/toolkit/skills/docs/vendor/dot-codex/specs/stable-preview-runtime/SPEC.md +0 -1
- package/toolkit/skills/multi-task-flow/SKILL.md +65 -47
- package/toolkit/skills/multi-task-flow/dist/app.css +1 -1
- package/toolkit/skills/multi-task-flow/dist/app.js +74 -74
- package/toolkit/skills/multi-task-flow/dist/index.html +3 -3
- package/toolkit/skills/multi-task-flow/lib/atomic-orchestration.ts +1 -0
- package/toolkit/skills/multi-task-flow/lib/engine.ts +65 -7
- package/toolkit/skills/multi-task-flow/lib/package-source.ts +5 -5
- package/toolkit/skills/multi-task-flow/lib/schema.ts +82 -2
- package/toolkit/skills/multi-task-flow/lib/service-pages.ts +1 -26
- package/toolkit/skills/multi-task-flow/lib/view.ts +16 -0
- package/toolkit/skills/multi-task-flow/references/shared-viewer.md +1 -1
- package/toolkit/skills/multi-task-flow/scripts/check-client-skills.ts +7 -4
- package/toolkit/skills/multi-task-flow/scripts/check-skills-installation.ts +14 -8
- package/toolkit/skills/multi-task-flow/scripts/import-client-skills.ts +1 -1
- package/toolkit/skills/multi-task-flow/scripts/package-client-skills.ts +4 -4
- package/toolkit/skills/multi-task-flow/scripts/test-viewer-source.ts +1 -1
- package/toolkit/skills/multi-task-flow/tests/atomic-orchestration-schema.test.ts +5 -0
- package/toolkit/skills/multi-task-flow/tests/browser/README.md +1 -1
- package/toolkit/skills/multi-task-flow/tests/check-skills-installation.test.ts +12 -38
- package/toolkit/skills/multi-task-flow/tests/delivery-topology.test.ts +33 -0
- package/toolkit/skills/multi-task-flow/tests/orchestrate.test.ts +3 -5
- package/toolkit/skills/multi-task-flow/tests/service-fixture.ts +10 -2
- package/toolkit/skills/multi-task-flow/tests/shared-service.test.ts +13 -0
- package/toolkit/skills/multi-task-flow/tests/ui/details-content.test.ts +9 -0
- package/toolkit/skills/multi-task-flow/tests/ui/responsive-width-contract.test.ts +25 -1
- package/toolkit/skills/multi-task-flow/tests/ui/task-context-menu.test.ts +2 -1
- package/toolkit/skills/multi-task-flow/web/app.css +64 -9
- package/toolkit/skills/multi-task-flow/web/app.ts +33 -14
- package/toolkit/skills/multi-task-flow/web/atomic-graph-view.tsx +1 -1
- package/toolkit/skills/multi-task-flow/web/details-content.ts +49 -38
- package/toolkit/skills/multi-task-flow/web/graph-view.tsx +1 -1
- package/toolkit/skills/multi-task-flow/web/index.html +3 -3
- package/toolkit/skills/multi-task-flow/web/model.ts +1 -1
- package/toolkit/skills/multi-task-flow/web/timeline.ts +11 -8
- package/toolkit/skills/package.json +2 -2
- package/toolkit/skills/source-manifest.json +693 -400
- package/toolkit/skills/support-manifest.json +1 -0
- package/toolkit/skills/multi-task-flow/scripts/serve.ts +0 -218
- package/toolkit/skills/multi-task-flow/tests/serve.test.ts +0 -194
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
<button id="tab-tasks" type="button" role="tab" aria-selected="false" aria-controls="tasks-view" data-tab="tasks">任务</button>
|
|
51
51
|
</div>
|
|
52
52
|
<label class="search input"><i data-lucide="search" aria-hidden="true"></i><span class="sr-only">搜索任务</span><input id="search" type="search" placeholder="搜索任务"></label>
|
|
53
|
+
<div id="task-view-tools" class="task-view-tools" aria-label="任务视图工具"><span id="task-count" class="section-count"></span><div id="task-layout-switch" class="layout-switch" role="group" aria-label="对象布局"><button id="task-layout-board" type="button" class="icon-button layout-option" aria-label="看板布局" aria-describedby="task-layout-board-tooltip" aria-pressed="true"><i data-lucide="columns-3"></i><span id="task-layout-board-tooltip" class="layout-tooltip" role="tooltip">看板布局:按交付单元分列展示任务</span></button><button id="task-layout-grid" type="button" class="icon-button layout-option" aria-label="网格布局" aria-describedby="task-layout-grid-tooltip" aria-pressed="false"><i data-lucide="layout-grid"></i><span id="task-layout-grid-tooltip" class="layout-tooltip" role="tooltip">网格布局:平铺展示任务</span></button></div></div>
|
|
53
54
|
</div>
|
|
54
55
|
<div class="filters">
|
|
55
56
|
<span class="filter-caption">筛选视图</span>
|
|
@@ -70,7 +71,6 @@
|
|
|
70
71
|
<div id="connection-notice" class="notice" role="status" hidden></div>
|
|
71
72
|
<div id="phase-notice" class="notice notice-neutral" role="status" hidden><span id="phase-notice-text"></span><button type="button" id="phase-jump" class="text-button">查看实际进度</button></div>
|
|
72
73
|
<div id="operation-warning" class="notice" role="status" hidden></div>
|
|
73
|
-
<div id="plan-state" class="notice plan-state" aria-label="计划与运行状态" hidden></div>
|
|
74
74
|
<div id="plan-warning" class="notice notice-warning" role="status" hidden></div>
|
|
75
75
|
<div id="load-error" class="notice notice-error" role="alert" hidden></div>
|
|
76
76
|
<div id="graph-view" class="canvas-view" role="tabpanel" aria-labelledby="tab-graph"><div id="graph-projection-warning" class="notice notice-warning" role="status" hidden></div><div id="graph" aria-label="任务依赖图"></div><div id="atomic-graph" aria-label="原子任务依赖图" hidden></div><div id="graph-a11y" class="graph-a11y" role="list" aria-label="依赖图节点列表"></div><div id="graph-empty" class="empty" hidden>暂无任务</div></div>
|
|
@@ -79,11 +79,11 @@
|
|
|
79
79
|
</div>
|
|
80
80
|
<div id="tasks-view" class="workspace-tasks" role="tabpanel" aria-labelledby="tab-tasks" hidden>
|
|
81
81
|
<section id="group-section" class="task-list-section" aria-labelledby="group-heading"><div class="section-heading"><h2 id="group-heading">交付单元</h2></div><div id="group-list" class="task-list" role="list"></div></section>
|
|
82
|
-
<section id="task-section" class="task-list-section"
|
|
82
|
+
<section id="task-section" class="task-list-section"><div id="task-list" class="task-list" role="list"></div></section>
|
|
83
83
|
</div>
|
|
84
84
|
</div>
|
|
85
85
|
<div id="details-backdrop" class="details-backdrop" hidden></div>
|
|
86
|
-
<div class="details-slot" hidden><aside id="details" class="details" aria-label="详情" aria-hidden="true" tabindex="-1"><div class="details-heading"><button type="button" id="close-details" class="icon-button mobile-close" data-tooltip="关闭详情" aria-label="关闭详情"><i data-lucide="x"></i></button><div id="details-summary"></div></div><div id="details-content" class="details-empty"><span class="empty-rule" aria-hidden="true"></span><strong>选择交付单元或任务</strong><span>查看依赖、运行时间和原生会话链接</span></div></aside></div>
|
|
86
|
+
<div class="details-slot" hidden><div id="details-resize" class="details-resize" role="separator" aria-orientation="horizontal" aria-label="调整详情高度" tabindex="0"></div><aside id="details" class="details" aria-label="详情" aria-hidden="true" tabindex="-1"><div class="details-heading"><button type="button" id="close-details" class="icon-button mobile-close" data-tooltip="关闭详情" aria-label="关闭详情"><i data-lucide="x"></i></button><div id="details-summary"></div></div><div id="details-content" class="details-empty"><span class="empty-rule" aria-hidden="true"></span><strong>选择交付单元或任务</strong><span>查看依赖、运行时间和原生会话链接</span></div></aside></div>
|
|
87
87
|
</section>
|
|
88
88
|
</main>
|
|
89
89
|
</div>
|
|
@@ -77,6 +77,7 @@ export const atomicEdge = z
|
|
|
77
77
|
|
|
78
78
|
export const taskPrBinding = z.discriminatedUnion("state", [
|
|
79
79
|
z.object({ state: z.literal("unbound") }).strict(),
|
|
80
|
+
z.object({ state: z.literal("shared_delivery_unit"), delivery_unit_id: id }).strict(),
|
|
80
81
|
z.object({ state: z.literal("open"), pr: atomicExternalRef, branch: text, base_branch: text }).strict(),
|
|
81
82
|
z.object({ state: z.literal("merged"), pr: atomicExternalRef, branch: text, merge_sha: text }).strict(),
|
|
82
83
|
z.object({ state: z.literal("closed"), pr: atomicExternalRef, branch: text, closed_at: timestamp, closed_reason: z.enum(["abandoned", "superseded", "provider_closed", "replaced"]) }).strict(),
|
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
validateLedger,
|
|
33
33
|
validateSchedulablePlan,
|
|
34
34
|
validatePlan,
|
|
35
|
+
deliveryTopologyForGroup,
|
|
36
|
+
validateDeliveryTopologies,
|
|
35
37
|
defaultGroupAtoms,
|
|
36
38
|
executionProfile,
|
|
37
39
|
recommendedExecutionProfile,
|
|
@@ -192,6 +194,15 @@ function taskPrRef(task: Ledger["tasks"][number]): ExternalRef | null {
|
|
|
192
194
|
if (task.pr && "state" in task.pr) return "pr" in task.pr ? task.pr.pr : null;
|
|
193
195
|
return task.pr;
|
|
194
196
|
}
|
|
197
|
+
function isSharedDeliveryTask(
|
|
198
|
+
ledger: Ledger,
|
|
199
|
+
task: Ledger["tasks"][number],
|
|
200
|
+
): boolean {
|
|
201
|
+
return (
|
|
202
|
+
isContributionKind(task.kind) &&
|
|
203
|
+
deliveryTopologyForGroup(ledger.tasks, task.group_id) === "shared_delivery"
|
|
204
|
+
);
|
|
205
|
+
}
|
|
195
206
|
function requireContributionDispatchGate(
|
|
196
207
|
ledger: Ledger,
|
|
197
208
|
owner: Ledger["groups"][number],
|
|
@@ -240,6 +251,8 @@ function ensureSchedulable(ledger: Ledger, candidate: Plan): void {
|
|
|
240
251
|
const contributionTasks = ledger.tasks.filter(
|
|
241
252
|
(item) => item.disposition === "active" && isContributionKind(item.kind),
|
|
242
253
|
);
|
|
254
|
+
if (ledger.contract_version >= 3)
|
|
255
|
+
validateDeliveryTopologies(contributionTasks, true);
|
|
243
256
|
const groupIds = new Set(contributionTasks.map((item) => item.group_id));
|
|
244
257
|
const missing: string[] = [];
|
|
245
258
|
for (const groupId of groupIds) {
|
|
@@ -254,9 +267,19 @@ function ensureSchedulable(ledger: Ledger, candidate: Plan): void {
|
|
|
254
267
|
}
|
|
255
268
|
}
|
|
256
269
|
for (const task of contributionTasks) {
|
|
270
|
+
const topology = deliveryTopologyForGroup(contributionTasks, task.group_id);
|
|
257
271
|
const hasTaskPublication = candidate.actions.some(
|
|
258
272
|
(action) => action.task_id === task.id,
|
|
259
273
|
);
|
|
274
|
+
if (topology === "shared_delivery") {
|
|
275
|
+
requireCondition(
|
|
276
|
+
!hasTaskPublication,
|
|
277
|
+
"PLAN_NOT_DISPATCHABLE",
|
|
278
|
+
`Shared delivery task ${task.id} must not declare a task-level PR action`,
|
|
279
|
+
3,
|
|
280
|
+
);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
260
283
|
if (!hasTaskPublication) continue;
|
|
261
284
|
for (const kind of ["publish_branch", "publish_pr"] as const) {
|
|
262
285
|
const planned = candidate.actions.some(
|
|
@@ -1595,6 +1618,15 @@ export function execute(
|
|
|
1595
1618
|
(item) => item.id === target.group_id,
|
|
1596
1619
|
"Group",
|
|
1597
1620
|
);
|
|
1621
|
+
requireCondition(
|
|
1622
|
+
!isSharedDeliveryTask(ledger, target),
|
|
1623
|
+
"SHARED_DELIVERY_UNIT_DISPATCH",
|
|
1624
|
+
"A single-contribution delivery unit is dispatched through its delivery-unit conversation; do not create a task conversation",
|
|
1625
|
+
3,
|
|
1626
|
+
{
|
|
1627
|
+
recovery: ["dispatch-group-claim", "dispatch-resolve", "task-update"],
|
|
1628
|
+
},
|
|
1629
|
+
);
|
|
1598
1630
|
requireCondition(
|
|
1599
1631
|
groupOwnerThread(ledger, owner),
|
|
1600
1632
|
"GROUP_NOT_READY",
|
|
@@ -2401,6 +2433,15 @@ export function execute(
|
|
|
2401
2433
|
const targetId = parse(id, payload.target_id);
|
|
2402
2434
|
requireOwner(ledger, request, targetType, targetId);
|
|
2403
2435
|
requireDevelopmentTarget(ledger, targetType, targetId);
|
|
2436
|
+
if (targetType === "task") {
|
|
2437
|
+
const taskTarget = find(ledger.tasks, (item) => item.id === targetId, "Task");
|
|
2438
|
+
requireCondition(
|
|
2439
|
+
!isSharedDeliveryTask(ledger, taskTarget),
|
|
2440
|
+
"SHARED_DELIVERY_UNIT_PR",
|
|
2441
|
+
"A shared delivery task reuses the delivery-unit PR and cannot publish a task PR",
|
|
2442
|
+
3,
|
|
2443
|
+
);
|
|
2444
|
+
}
|
|
2404
2445
|
requireLifecycle(ledger, targetType, targetId, "workspace_ready");
|
|
2405
2446
|
const target = lifecycleTarget(ledger, targetType, targetId) as any;
|
|
2406
2447
|
const branch = string(payload.branch);
|
|
@@ -2816,7 +2857,10 @@ export function execute(
|
|
|
2816
2857
|
case "task-runtime-observe": {
|
|
2817
2858
|
onlyMain(request.actor);
|
|
2818
2859
|
const target = find(ledger.tasks, item => item.id === parse(id, payload.task_id), "Task");
|
|
2819
|
-
|
|
2860
|
+
const observedThread = isSharedDeliveryTask(ledger, target)
|
|
2861
|
+
? ledger.groups.find((item) => item.id === target.group_id)?.agent_thread_id
|
|
2862
|
+
: target.thread_id;
|
|
2863
|
+
requireCondition(observedThread === payload.thread_id, "RESOURCE_MISMATCH", "Runtime observation thread does not match task", 4);
|
|
2820
2864
|
const runtime = z.enum(["active", "idle", "unknown"]).parse(payload.runtime);
|
|
2821
2865
|
const observedAt = z.iso.datetime().parse(payload.observed_at);
|
|
2822
2866
|
if (runtime === "idle") requireCondition(typeof payload.turn_id === "string" && payload.turn_id.length > 0, "EVIDENCE_REQUIRED", "Idle observation requires a confirmed terminal turn identity", 4);
|
|
@@ -2829,7 +2873,7 @@ export function execute(
|
|
|
2829
2873
|
target.state = target.agent_resume_state ?? "running";
|
|
2830
2874
|
delete target.agent_resume_state;
|
|
2831
2875
|
}
|
|
2832
|
-
if (previous !== target.state) addEvent(ledger, request, "task_runtime_observed", target.id, { task_id: target.id, thread_id:
|
|
2876
|
+
if (previous !== target.state) addEvent(ledger, request, "task_runtime_observed", target.id, { task_id: target.id, thread_id: observedThread, turn_id: payload.turn_id ?? null, observed_at: observedAt, runtime, state: target.state, previous_state: previous });
|
|
2833
2877
|
return { state: target.state };
|
|
2834
2878
|
}
|
|
2835
2879
|
case "task-update": {
|
|
@@ -2840,11 +2884,16 @@ export function execute(
|
|
|
2840
2884
|
target.conversation_policy === "main_owned" &&
|
|
2841
2885
|
request.actor.role === "main" &&
|
|
2842
2886
|
request.actor.thread_id === ledger.control.owner_thread_id;
|
|
2887
|
+
const sharedOwned =
|
|
2888
|
+
isSharedDeliveryTask(ledger, target) &&
|
|
2889
|
+
request.actor.role === "group" &&
|
|
2890
|
+
ledger.groups.find((item) => item.id === target.group_id)?.agent_thread_id ===
|
|
2891
|
+
request.actor.thread_id;
|
|
2843
2892
|
const state = z
|
|
2844
2893
|
.enum(["running", "waiting", "blocked", "completed", "closed"])
|
|
2845
2894
|
.parse(payload.state);
|
|
2846
2895
|
const allowed: Record<string, string[]> = {
|
|
2847
|
-
planned: mainOwned ? ["running", "waiting", "blocked"] : [],
|
|
2896
|
+
planned: mainOwned || sharedOwned ? ["running", "waiting", "blocked"] : [],
|
|
2848
2897
|
starting: ["running", "waiting", "blocked"],
|
|
2849
2898
|
running: ["waiting", "blocked"],
|
|
2850
2899
|
agent_idle: ["running", "waiting", "blocked"],
|
|
@@ -2926,9 +2975,9 @@ export function execute(
|
|
|
2926
2975
|
}
|
|
2927
2976
|
if (state === "running" && previousState === "planned")
|
|
2928
2977
|
requireCondition(
|
|
2929
|
-
mainOwned && ready(ledger, target.id),
|
|
2978
|
+
(mainOwned || sharedOwned) && ready(ledger, target.id),
|
|
2930
2979
|
"MAIN_TASK_REQUIRED",
|
|
2931
|
-
"Only the
|
|
2980
|
+
"Only the task owner can start a planned task directly",
|
|
2932
2981
|
3,
|
|
2933
2982
|
);
|
|
2934
2983
|
if (state === "completed")
|
|
@@ -2956,7 +3005,10 @@ export function execute(
|
|
|
2956
3005
|
);
|
|
2957
3006
|
if (isContributionKind(target.kind))
|
|
2958
3007
|
requireCondition(
|
|
2959
|
-
legacyLedger(ledger) ||
|
|
3008
|
+
legacyLedger(ledger) ||
|
|
3009
|
+
target.lifecycle.pr_merged === "passed" ||
|
|
3010
|
+
(isSharedDeliveryTask(ledger, target) &&
|
|
3011
|
+
ledger.groups.find((item) => item.id === target.group_id)?.lifecycle.pr_merged === "passed"),
|
|
2960
3012
|
"PR_NOT_MERGED",
|
|
2961
3013
|
"Contribution task PR must merge before closeout",
|
|
2962
3014
|
4,
|
|
@@ -3044,6 +3096,7 @@ export function execute(
|
|
|
3044
3096
|
const target = find(ledger.tasks, (item) => item.id === taskId, "Task");
|
|
3045
3097
|
const owner = find(ledger.groups, (item) => item.id === target.group_id, "Group");
|
|
3046
3098
|
requireCondition(isContributionKind(target.kind), "TASK_MISMATCH", "Only contribution tasks can be equivalently integrated", 3);
|
|
3099
|
+
requireCondition(!isSharedDeliveryTask(ledger, target), "SHARED_DELIVERY_UNIT_PR", "Shared delivery tasks do not have a child PR to integrate", 3);
|
|
3047
3100
|
requireCondition(["blocked", "agent_idle", "running"].includes(target.state), "INVALID_TRANSITION", "Task is not awaiting integration", 3);
|
|
3048
3101
|
requireCondition(target.atoms.every((atom) => atom.status === "completed"), "ATOMS_PENDING", "Complete the task atoms before integration", 4);
|
|
3049
3102
|
const taskPr = parse(externalRef, payload.task_pr);
|
|
@@ -3079,8 +3132,13 @@ export function execute(
|
|
|
3079
3132
|
target.conversation_policy === "main_owned" &&
|
|
3080
3133
|
request.actor.role === "main" &&
|
|
3081
3134
|
request.actor.thread_id === ledger.control.owner_thread_id;
|
|
3135
|
+
const sharedOwned =
|
|
3136
|
+
isSharedDeliveryTask(ledger, target) &&
|
|
3137
|
+
request.actor.role === "group" &&
|
|
3138
|
+
ledger.groups.find((item) => item.id === target.group_id)?.agent_thread_id ===
|
|
3139
|
+
request.actor.thread_id;
|
|
3082
3140
|
requireCondition(
|
|
3083
|
-
(request.actor.role === "task" || mainOwned) && taskExecutionState(target) === "running",
|
|
3141
|
+
(request.actor.role === "task" || mainOwned || sharedOwned) && taskExecutionState(target) === "running",
|
|
3084
3142
|
"FORBIDDEN",
|
|
3085
3143
|
"The running task owner updates its atoms",
|
|
3086
3144
|
3,
|
|
@@ -5,8 +5,8 @@ export const digest = value => createHash('sha256').update(value).digest('hex');
|
|
|
5
5
|
export function copySkillSources(root, output, { dependencies = false } = {}) {
|
|
6
6
|
const manifest = JSON.parse(readFileSync(join(root, 'skills/source-manifest.json'), 'utf8'));
|
|
7
7
|
const files = new Map();
|
|
8
|
-
const copy = (path, expected) => {
|
|
9
|
-
const source = resolve(root,
|
|
8
|
+
const copy = (path, expected, sourcePath = path) => {
|
|
9
|
+
const source = resolve(root, sourcePath);
|
|
10
10
|
if (!path.startsWith('skills/') || !source.startsWith(resolve(root, 'skills') + '/')) throw new Error('Unsafe source resource: ' + path);
|
|
11
11
|
const stat = lstatSync(source);
|
|
12
12
|
if (!stat.isFile() || stat.isSymbolicLink() || realpathSync(source) !== source || (!path.includes('/node_modules/') && stat.nlink !== 1)) throw new Error('Unsafe source resource: ' + path);
|
|
@@ -15,9 +15,9 @@ export function copySkillSources(root, output, { dependencies = false } = {}) {
|
|
|
15
15
|
const target = join(output, path); mkdirSync(dirname(target), { recursive: true, mode: 0o700 }); cpSync(source, target);
|
|
16
16
|
files.set(path, sha256);
|
|
17
17
|
};
|
|
18
|
-
for (const file of [...manifest.files, ...(manifest.additions ?? [])]) copy(file.path, file.installedSha256);
|
|
18
|
+
for (const file of [...manifest.files, ...(manifest.additions ?? [])]) copy(file.path, file.installedSha256, file.sourcePath ?? file.path);
|
|
19
19
|
for (const path of ['skills/source-manifest.json', 'skills/catalog.json']) copy(path);
|
|
20
|
-
for (const name of ['index.html','app.js','app.css']) copy('skills/multi-task-flow/dist/' + name);
|
|
20
|
+
for (const name of ['index.html','app.js','app.css']) copy('skills/multi-task-flow/dist/' + name, undefined, 'skills/multi-task-flow-runtime/dist/' + name);
|
|
21
21
|
if (dependencies) {
|
|
22
22
|
const walk = path => {
|
|
23
23
|
for (const entry of readdirSync(join(root, path), { withFileTypes: true })) {
|
|
@@ -26,7 +26,7 @@ export function copySkillSources(root, output, { dependencies = false } = {}) {
|
|
|
26
26
|
if (entry.isDirectory()) walk(child); else copy(child);
|
|
27
27
|
}
|
|
28
28
|
};
|
|
29
|
-
for (const path of ['skills/node_modules', 'skills/multi-task-flow/node_modules']) {
|
|
29
|
+
for (const path of ['skills/node_modules', 'skills/multi-task-flow-runtime/node_modules']) {
|
|
30
30
|
if (!existsSync(join(root, path))) throw new Error('Prepared dependency directory missing: ' + path);
|
|
31
31
|
walk(path);
|
|
32
32
|
}
|
|
@@ -384,7 +384,7 @@ export const task = z
|
|
|
384
384
|
.nullable()
|
|
385
385
|
.default(null),
|
|
386
386
|
delivery_policy: z
|
|
387
|
-
.enum(["parent_pr", "child_pr", "evidence", "none"])
|
|
387
|
+
.enum(["parent_pr", "child_pr", "shared_delivery_unit", "evidence", "none"])
|
|
388
388
|
.nullable()
|
|
389
389
|
.default(null),
|
|
390
390
|
superseded_by_task_id: nullableText.default(null),
|
|
@@ -772,7 +772,7 @@ const taskExecutionPolicyDeclaration = z
|
|
|
772
772
|
.enum(["main_owned", "delivery_owned", "task_agent", "none"])
|
|
773
773
|
.nullable(),
|
|
774
774
|
delivery_policy: z
|
|
775
|
-
.enum(["parent_pr", "child_pr", "evidence", "none"])
|
|
775
|
+
.enum(["parent_pr", "child_pr", "shared_delivery_unit", "evidence", "none"])
|
|
776
776
|
.nullable(),
|
|
777
777
|
})
|
|
778
778
|
.strict();
|
|
@@ -820,6 +820,8 @@ export function taskExecutionPolicyIssues(
|
|
|
820
820
|
if (owner === "main" && delivery === "child_pr") issues.push({ code: "TASK_EXECUTION_POLICY_CONFLICT", message: "Main Agent 不得拥有 child PR" });
|
|
821
821
|
if (owner === "delivery_agent" && delivery === "child_pr") issues.push({ code: "TASK_EXECUTION_POLICY_CONFLICT", message: "交付 Agent 不得拥有 child PR" });
|
|
822
822
|
if (owner === "task_agent" && delivery === "parent_pr") issues.push({ code: "TASK_EXECUTION_POLICY_CONFLICT", message: "Task Agent 不得拥有 parent PR" });
|
|
823
|
+
if (delivery === "shared_delivery_unit" && owner !== "delivery_agent") issues.push({ code: "TASK_EXECUTION_POLICY_CONFLICT", message: "共享交付任务必须由交付 Agent 持有" });
|
|
824
|
+
if (delivery === "shared_delivery_unit" && conversation !== "delivery_owned") issues.push({ code: "TASK_EXECUTION_POLICY_CONFLICT", message: "共享交付任务必须复用交付单元对话" });
|
|
823
825
|
if (conversation === "none" && ["child_pr", "evidence"].includes(delivery)) issues.push({ code: "TASK_EXECUTION_POLICY_CONFLICT", message: "无对话任务不能声明 child PR 或验证证据" });
|
|
824
826
|
return issues;
|
|
825
827
|
}
|
|
@@ -846,6 +848,82 @@ export function validateTaskExecutionPolicies(tasks: readonly Task[], requireCom
|
|
|
846
848
|
export function isContributionKind(kind: Task["kind"]): boolean {
|
|
847
849
|
return kind === "contribution" || kind === "development";
|
|
848
850
|
}
|
|
851
|
+
|
|
852
|
+
export type DeliveryTopology = "shared_delivery" | "multi_task_integration" | "none";
|
|
853
|
+
|
|
854
|
+
export function deliveryTopologyForGroup(
|
|
855
|
+
tasks: readonly Pick<Task, "group_id" | "kind" | "disposition">[],
|
|
856
|
+
groupId: string,
|
|
857
|
+
): DeliveryTopology {
|
|
858
|
+
const count = tasks.filter(
|
|
859
|
+
(task) =>
|
|
860
|
+
task.group_id === groupId &&
|
|
861
|
+
task.disposition === "active" &&
|
|
862
|
+
isContributionKind(task.kind),
|
|
863
|
+
).length;
|
|
864
|
+
return count === 0
|
|
865
|
+
? "none"
|
|
866
|
+
: count === 1
|
|
867
|
+
? "shared_delivery"
|
|
868
|
+
: "multi_task_integration";
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Validate the execution policy derived from the main Agent's approved grouping.
|
|
873
|
+
* The grouping itself is a planning decision; this function only checks that the
|
|
874
|
+
* resulting one-task/multi-task topology is represented consistently at runtime.
|
|
875
|
+
*/
|
|
876
|
+
export function validateDeliveryTopologies(
|
|
877
|
+
tasks: readonly Task[],
|
|
878
|
+
requireComplete = false,
|
|
879
|
+
): void {
|
|
880
|
+
const groupIds = new Set(
|
|
881
|
+
tasks
|
|
882
|
+
.filter((task) => task.disposition === "active" && isContributionKind(task.kind))
|
|
883
|
+
.map((task) => task.group_id),
|
|
884
|
+
);
|
|
885
|
+
for (const groupId of groupIds) {
|
|
886
|
+
const topology = deliveryTopologyForGroup(tasks, groupId);
|
|
887
|
+
const contributionTasks = tasks.filter(
|
|
888
|
+
(task) =>
|
|
889
|
+
task.group_id === groupId &&
|
|
890
|
+
task.disposition === "active" &&
|
|
891
|
+
isContributionKind(task.kind),
|
|
892
|
+
);
|
|
893
|
+
if (!requireComplete) {
|
|
894
|
+
if (contributionTasks.some((task) => task.delivery_policy === "shared_delivery_unit"))
|
|
895
|
+
requireCondition(
|
|
896
|
+
topology === "shared_delivery",
|
|
897
|
+
"DELIVERY_TOPOLOGY_CONFLICT",
|
|
898
|
+
`Delivery unit ${groupId} can use shared delivery only with one contribution task`,
|
|
899
|
+
3,
|
|
900
|
+
);
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
for (const task of contributionTasks) {
|
|
904
|
+
if (topology === "shared_delivery") {
|
|
905
|
+
requireCondition(
|
|
906
|
+
task.execution_owner === "delivery_agent" &&
|
|
907
|
+
task.conversation_policy === "delivery_owned" &&
|
|
908
|
+
task.delivery_policy === "shared_delivery_unit",
|
|
909
|
+
"DELIVERY_TOPOLOGY_CONFLICT",
|
|
910
|
+
`Task ${task.id} must use the delivery-unit conversation and shared PR topology`,
|
|
911
|
+
3,
|
|
912
|
+
);
|
|
913
|
+
} else {
|
|
914
|
+
requireCondition(
|
|
915
|
+
task.execution_owner === "task_agent" &&
|
|
916
|
+
task.conversation_policy === "task_agent" &&
|
|
917
|
+
task.delivery_policy === "child_pr",
|
|
918
|
+
"DELIVERY_TOPOLOGY_CONFLICT",
|
|
919
|
+
`Task ${task.id} must use an independent Task Agent and child PR in a multi-task delivery unit`,
|
|
920
|
+
3,
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
849
927
|
export function isCanonicalTaskKind(kind: Task["kind"]): boolean {
|
|
850
928
|
return ["contribution", "release", "deployment", "verification"].includes(kind);
|
|
851
929
|
}
|
|
@@ -2121,6 +2199,8 @@ export function validateLedger(value: unknown): Ledger {
|
|
|
2121
2199
|
}
|
|
2122
2200
|
for (const task of parsed.tasks.filter((item) => isContributionKind(item.kind))) {
|
|
2123
2201
|
requireCondition(taskPrBinding.safeParse(task.pr).success, "ATOMIC_CONTRACT_REQUIRED", "Contract-3 contribution Task PR must use TaskPrBinding", 3);
|
|
2202
|
+
if (task.pr?.state === "shared_delivery_unit")
|
|
2203
|
+
requireCondition(task.pr.delivery_unit_id === task.group_id, "DELIVERY_TOPOLOGY_CONFLICT", "Shared task PR binding must reference its delivery unit", 3);
|
|
2124
2204
|
}
|
|
2125
2205
|
}
|
|
2126
2206
|
for (const collection of [
|
|
@@ -8,26 +8,6 @@ export function viewerPage(
|
|
|
8
8
|
|
|
9
9
|
const runPathPattern = /^\/runs\/[A-Za-z0-9][A-Za-z0-9_.-]{0,127}\/$/;
|
|
10
10
|
const viewIdPattern = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
|
|
11
|
-
const viewParamValidators: Record<string, (value: string) => boolean> = {
|
|
12
|
-
tab: (value) => /^(graph|atomic|planned|actual)$/.test(value),
|
|
13
|
-
graph: (value) => /^(aggregate|atomic)$/.test(value),
|
|
14
|
-
layout: (value) => /^(board|grid)$/.test(value),
|
|
15
|
-
q: (value) => value.length <= 200 && !/[\u0000-\u001f\u007f]/.test(value),
|
|
16
|
-
group: (value) => viewIdPattern.test(value),
|
|
17
|
-
state: (value) => viewIdPattern.test(value),
|
|
18
|
-
kind: (value) => viewIdPattern.test(value),
|
|
19
|
-
plan: (value) => viewIdPattern.test(value),
|
|
20
|
-
selected: (value) => /^(task|group|milestone):[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(value),
|
|
21
|
-
detail: (value) => value === "1" || value === "true",
|
|
22
|
-
zoom: (value) => {
|
|
23
|
-
const zoom = Number(value);
|
|
24
|
-
return Number.isFinite(zoom) && zoom >= 0.2 && zoom <= 2.5;
|
|
25
|
-
},
|
|
26
|
-
plannedStart: (value) => value.length <= 64 && !Number.isNaN(Date.parse(value)),
|
|
27
|
-
plannedEnd: (value) => value.length <= 64 && !Number.isNaN(Date.parse(value)),
|
|
28
|
-
actualStart: (value) => value.length <= 64 && !Number.isNaN(Date.parse(value)),
|
|
29
|
-
actualEnd: (value) => value.length <= 64 && !Number.isNaN(Date.parse(value)),
|
|
30
|
-
};
|
|
31
11
|
const maxTimelineSpanMs = 10_000 * 365.25 * 24 * 60 * 60 * 1000;
|
|
32
12
|
|
|
33
13
|
export type ViewerTarget = { target: string; ticket: string | null };
|
|
@@ -50,15 +30,10 @@ export function parseViewerTarget(
|
|
|
50
30
|
if (ticketValues.length > 1 || (ticketValues.length > 0 && !allowTicket)) return null;
|
|
51
31
|
const ticket = ticketValues[0] ?? null;
|
|
52
32
|
if (ticket !== null && (ticket.length > 100 || /[\u0000-\u001f\u007f]/.test(ticket))) return null;
|
|
53
|
-
let selected = false;
|
|
54
|
-
let detail = false;
|
|
55
33
|
for (const [key, parameter] of url.searchParams) {
|
|
56
34
|
if (key === "ticket") continue;
|
|
57
|
-
if (home || /[\u0000-\u001f\u007f]/.test(
|
|
58
|
-
if (key === "selected") selected = true;
|
|
59
|
-
if (key === "detail") detail = true;
|
|
35
|
+
if (home || /[\u0000-\u001f\u007f]/.test(key) || /[\u0000-\u001f\u007f]/.test(parameter)) return null;
|
|
60
36
|
}
|
|
61
|
-
if (detail && !selected) return null;
|
|
62
37
|
for (const [start, end] of [["plannedStart", "plannedEnd"], ["actualStart", "actualEnd"]] as const) {
|
|
63
38
|
const startValue = url.searchParams.get(start);
|
|
64
39
|
const endValue = url.searchParams.get(end);
|
|
@@ -141,6 +141,21 @@ function deliveryGateDiagnostics(
|
|
|
141
141
|
return diagnostics;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
function taskPrProjection(
|
|
145
|
+
task: Ledger["tasks"][number],
|
|
146
|
+
owner: Ledger["groups"][number] | undefined,
|
|
147
|
+
): { url: string | null } | null {
|
|
148
|
+
if (!task.pr || typeof task.pr !== "object" || !("state" in task.pr))
|
|
149
|
+
return task.pr && "url" in task.pr ? { url: task.pr.url ?? null } : null;
|
|
150
|
+
if (task.pr.state === "shared_delivery_unit") {
|
|
151
|
+
const deliveryPr = owner?.pr;
|
|
152
|
+
if (deliveryPr && typeof deliveryPr === "object" && "state" in deliveryPr && "pr" in deliveryPr)
|
|
153
|
+
return { url: deliveryPr.pr.url ?? null };
|
|
154
|
+
return owner?.parent_pr ? { url: owner.parent_pr.url ?? null } : null;
|
|
155
|
+
}
|
|
156
|
+
return "pr" in task.pr ? { url: task.pr.pr.url ?? null } : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
144
159
|
function formalExecutionWarnings(ledger: Ledger): Record<string, unknown>[] {
|
|
145
160
|
return ledger.groups.flatMap((group) => {
|
|
146
161
|
if (group.state !== "active" || group.coordination_execution.state !== "running")
|
|
@@ -898,6 +913,7 @@ export function buildView(
|
|
|
898
913
|
const derivedStatus = deriveBusinessStatus(canonicalAtomicNodes, { kind: "task", task_id: task.id }, milestoneEvaluations);
|
|
899
914
|
return {
|
|
900
915
|
...task,
|
|
916
|
+
pr: taskPrProjection(task, owner),
|
|
901
917
|
...(() => {
|
|
902
918
|
const gateDiagnostics = owner && isContributionKind(task.kind) ? deliveryGateDiagnostics(ledger, owner, task) : [];
|
|
903
919
|
return { gateDiagnostics };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The formal root is `~/.local/share/multi-task-flow/`: `config/`, `data/runs/`, `releases/`, `state/`, `logs/`. `CODEX_HOME` and the invoking worktree do not select a formal instance. `MULTI_TASK_FLOW_PROFILE=test|development` requires an absolute `MULTI_TASK_FLOW_ROOT`, distinct from the formal root. Isolated profiles derive separate launchd labels, port scopes and registry roots. `TASK_ORCHESTRATION_DATA_ROOT` is an explicit ledger-only fixture override; never use it to create a second formal authority.
|
|
4
4
|
|
|
5
|
-
All commands use `service
|
|
5
|
+
All commands use `multi-task-flow service <command> --input -`, consuming one JSON object. Normal usage:
|
|
6
6
|
|
|
7
7
|
- `ensure`, `renew`, `release`: `{"run_id":"example","thread_id":"main-thread","api_version":1}`.
|
|
8
8
|
- `status`, `doctor`: `{}`. Inspect configured identity and runtime before remediation.
|
|
@@ -8,8 +8,9 @@ const path = resolve(root, 'skills/source-manifest.json');
|
|
|
8
8
|
const manifest = JSON.parse(readFileSync(path, 'utf8'));
|
|
9
9
|
const hash = data => createHash('sha256').update(data).digest('hex');
|
|
10
10
|
const refresh = process.argv.includes('--write');
|
|
11
|
-
const additional = [...JSON.parse(readFileSync(resolve(root, 'skills/support-manifest.json'), 'utf8')), 'skills/bin/modules/core/runtime.ts', 'skills/bin/package.json', 'skills/multi-task-flow/tests/service-release.test.ts', 'skills/multi-task-flow/lib/notification-delivery.ts', 'skills/multi-task-flow/tests/notification-delivery.test.ts', 'skills/multi-task-flow/lib/conversation-archive.ts', 'skills/multi-task-flow/lib/task-runtime.ts', 'skills/multi-task-flow/web/task-state.ts', 'skills/multi-task-flow/web/conversation-control.ts', 'skills/multi-task-flow/tests/ui/conversation-control.test.ts', 'skills/multi-task-flow/tests/task-runtime.test.ts'];
|
|
12
|
-
|
|
11
|
+
const additional = [...JSON.parse(readFileSync(resolve(root, 'skills/support-manifest.json'), 'utf8')), 'skills/bin/modules/core/runtime.ts', 'skills/bin/package.json', 'skills/multi-task-flow/tests/service-release.test.ts', 'skills/multi-task-flow/lib/notification-delivery.ts', 'skills/multi-task-flow/tests/notification-delivery.test.ts', 'skills/multi-task-flow/lib/conversation-archive.ts', 'skills/multi-task-flow/lib/task-runtime.ts', 'skills/multi-task-flow/web/task-state.ts', 'skills/multi-task-flow/web/conversation-control.ts', 'skills/multi-task-flow/tests/ui/conversation-control.test.ts', 'skills/multi-task-flow/tests/task-runtime.test.ts', 'skills/multi-task-flow/lib/atomic-orchestration.ts', 'skills/multi-task-flow/tests/atomic-orchestration-schema.test.ts', 'skills/multi-task-flow/tests/task-kind.test.ts', 'skills/multi-task-flow/web/atomic-graph-view.tsx'];
|
|
12
|
+
const sourcePath = file => file.sourcePath ?? file.path;
|
|
13
|
+
if (refresh) manifest.additions = additional.map(path => { const source = path.startsWith('skills/multi-task-flow/') ? 'skills/multi-task-flow-runtime/' + path.slice('skills/multi-task-flow/'.length) : path; return { path, sourcePath: source, installedSha256: hash(readFileSync(resolve(root, source))) }; });
|
|
13
14
|
const findings = [];
|
|
14
15
|
const gitRoot = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd:root, encoding:'utf8' });
|
|
15
16
|
if (gitRoot.status === 0 && gitRoot.stdout.trim() === root) {
|
|
@@ -18,6 +19,7 @@ if (gitRoot.status === 0 && gitRoot.stdout.trim() === root) {
|
|
|
18
19
|
const covered = new Set([...manifest.files,...(manifest.additions ?? [])].map(file => file.path));
|
|
19
20
|
for (const file of tracked.stdout.split('\0').filter(Boolean)) {
|
|
20
21
|
if (/\.(?:js|mjs|cjs)$/.test(file)) findings.push('owned-source-must-be-typescript:'+file);
|
|
22
|
+
if (file.startsWith('skills/multi-task-flow-runtime/')) continue;
|
|
21
23
|
if (file.startsWith('skills/') && !covered.has(file) && !['skills/source-manifest.json','skills/catalog.json'].includes(file)) findings.push('unlisted:'+file);
|
|
22
24
|
}
|
|
23
25
|
}
|
|
@@ -27,8 +29,9 @@ for (const excluded of manifest.exclusions ?? []) {
|
|
|
27
29
|
if (manifest.files.some(file => file.source.startsWith(excluded.sourcePrefix)) || existsSync(resolve(root, installed))) findings.push(`excluded-resource:${installed}`);
|
|
28
30
|
}
|
|
29
31
|
for (const file of [...manifest.files, ...(manifest.additions ?? [])]) {
|
|
30
|
-
const
|
|
31
|
-
if (!file.path.startsWith('skills/') || !
|
|
32
|
+
const source = resolve(root, sourcePath(file));
|
|
33
|
+
if (!file.path.startsWith('skills/') || !source.startsWith(root + '/') || !existsSync(source)) { findings.push(`missing:${file.path}`); continue; }
|
|
34
|
+
const target = source;
|
|
32
35
|
const stat = lstatSync(target);
|
|
33
36
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) { findings.push(`unsafe:${file.path}`); continue; }
|
|
34
37
|
const digest = hash(readFileSync(target));
|
|
@@ -12,6 +12,11 @@ export function checkSkillsInstallation(root, { probe = false } = {}) {
|
|
|
12
12
|
const inventory = resolve(root, 'skills/source-manifest.json');
|
|
13
13
|
if (!existsSync(inventory)) return { ok: false, code: 'SKILLS_INVENTORY_MISSING', root, missing: ['skills/source-manifest.json'] };
|
|
14
14
|
const manifest = JSON.parse(readFileSync(inventory, 'utf8'));
|
|
15
|
+
const localPath = resource => {
|
|
16
|
+
const target = resolve(root, resource.path);
|
|
17
|
+
if (existsSync(target)) return resource.path;
|
|
18
|
+
return resource.sourcePath ?? resource.path;
|
|
19
|
+
};
|
|
15
20
|
const contained = path => {
|
|
16
21
|
const file = resolve(root, path);
|
|
17
22
|
if (!existsSync(file)) return false;
|
|
@@ -19,14 +24,15 @@ export function checkSkillsInstallation(root, { probe = false } = {}) {
|
|
|
19
24
|
return stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1 && realpathSync(file) === file && realpathSync(file).startsWith(realpathSync(resolve(root, 'skills')) + '/');
|
|
20
25
|
};
|
|
21
26
|
const missing = [...manifest.files, ...(manifest.additions ?? [])]
|
|
22
|
-
.map(file => file
|
|
23
|
-
const modified = [...manifest.files, ...(manifest.additions ?? [])].filter(file => !missing.includes(
|
|
27
|
+
.map(file => localPath(file)).filter(path => !path.startsWith('skills/') || path.split('/').includes('..') || !contained(path));
|
|
28
|
+
const modified = [...manifest.files, ...(manifest.additions ?? [])].filter(file => { const path = localPath(file); return !missing.includes(path) && createHash("sha256").update(readFileSync(resolve(root,path))).digest("hex") !== file.installedSha256; }).map(file => file.path);
|
|
24
29
|
const escapedImports = [];
|
|
25
30
|
for (const file of [...manifest.files, ...(manifest.additions ?? [])]) {
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
const path = localPath(file);
|
|
32
|
+
if (!/\.(ts|mjs)$/.test(path) || !path.startsWith('skills/') || path.split('/').includes('..') || !contained(path)) continue;
|
|
33
|
+
const text = readFileSync(resolve(root, path), 'utf8');
|
|
28
34
|
for (const match of text.matchAll(/(?:from\s*|import\s*\(?\s*)["'](\.[^"']+)["']/g)) {
|
|
29
|
-
const target = resolve(root, dirname(
|
|
35
|
+
const target = resolve(root, dirname(path), match[1]);
|
|
30
36
|
if (!target.startsWith(resolve(root, 'skills') + '/')) escapedImports.push({ file: file.path, import: match[1] });
|
|
31
37
|
}
|
|
32
38
|
}
|
|
@@ -46,7 +52,7 @@ export function checkSkillsInstallation(root, { probe = false } = {}) {
|
|
|
46
52
|
const dependencies = { status:!sourceComplete ? 'not_checked' : dependencyMissing.length ? 'missing' : 'prepared', missing:dependencyMissing };
|
|
47
53
|
const entrypoints = { status: dependencyMissing.length ? 'blocked' : 'not_checked', results: [] };
|
|
48
54
|
if (probe && sourceComplete && runtime.available && dependencyMissing.length === 0) {
|
|
49
|
-
for (const [script, args] of [['skills/bin/plan-cli.sh',['--help']], ['skills/dot-codex-development/scripts/context.ts',['--help']], ['skills/multi-task-flow/scripts/orchestrate.ts',['view','--input','-']]]) {
|
|
55
|
+
for (const [script, args] of [['skills/bin/plan-cli.sh',['--help']], ['skills/dot-codex-development/scripts/context.ts',['--help']], ['skills/multi-task-flow-runtime/scripts/orchestrate.ts',['view','--input','-']]]) {
|
|
50
56
|
const result = spawnSync(script.endsWith('.sh') ? '/bin/bash' : process.execPath, [...(script.endsWith('.sh') ? ['-p'] : ['--no-install']), resolve(root,script), ...args], { cwd:root, env: { ...process.env, BUN_RUNTIME_TRANSPILER_CACHE_PATH:'0' }, input:'{}', encoding:'utf8', timeout:30000 });
|
|
51
57
|
const expected = script.endsWith('orchestrate.ts') ? 2 : 0;
|
|
52
58
|
entrypoints.results.push({script, code:result.status, ok:!result.error && result.status === expected, error: result.error ? String(result.error) : result.status === expected ? undefined : result.stderr});
|
|
@@ -69,11 +75,11 @@ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1]
|
|
|
69
75
|
const probe = process.argv.includes('--probe');
|
|
70
76
|
const args = process.argv.slice(2).filter(arg => arg !== '--probe');
|
|
71
77
|
if (args.length !== 0 && (args.length !== 2 || args[0] !== '--root')) {
|
|
72
|
-
console.error('Usage: bun skills/multi-task-flow/scripts/check-skills-installation.ts [--root PATH] [--probe]');
|
|
78
|
+
console.error('Usage: bun skills/multi-task-flow-runtime/scripts/check-skills-installation.ts [--root PATH] [--probe]');
|
|
73
79
|
process.exitCode = 2;
|
|
74
80
|
} else {
|
|
75
81
|
try {
|
|
76
|
-
const report = checkSkillsInstallation(args[1] ?? resolve(import.meta.dirname, '
|
|
82
|
+
const report = checkSkillsInstallation(args[1] ?? resolve(import.meta.dirname, '../../..'), {probe});
|
|
77
83
|
console.log(JSON.stringify(report));
|
|
78
84
|
process.exitCode = report.ok ? 0 : 1;
|
|
79
85
|
} catch (error) {
|
|
@@ -7,7 +7,7 @@ import { dirname, resolve } from 'node:path';
|
|
|
7
7
|
|
|
8
8
|
const source = process.argv[2];
|
|
9
9
|
const commit = process.argv[3] ?? 'HEAD';
|
|
10
|
-
if (!source) throw new Error('Usage: bun skills/multi-task-flow/scripts/import-client-skills.ts SOURCE_REPOSITORY [SOURCE_REF]');
|
|
10
|
+
if (!source) throw new Error('Usage: bun skills/multi-task-flow-runtime/scripts/import-client-skills.ts SOURCE_REPOSITORY [SOURCE_REF]');
|
|
11
11
|
const git = (...args) => execFileSync('git', ['-C', source, ...args], { maxBuffer: 32 * 1024 * 1024 });
|
|
12
12
|
const tree = git('ls-tree', '-rz', commit, 'skills', 'bin', 'docs').toString().split('\0').filter(Boolean);
|
|
13
13
|
const files = [];
|
|
@@ -60,8 +60,8 @@ const outputs = new Map<string, { path: string; sha256: string }>();
|
|
|
60
60
|
function record(path: string, contents: Uint8Array): void {
|
|
61
61
|
outputs.set(path, { path, sha256: createHash('sha256').update(contents).digest('hex') });
|
|
62
62
|
}
|
|
63
|
-
function copy(path: string, expected: string | undefined, dependency = false): void {
|
|
64
|
-
const source = resolve(root,
|
|
63
|
+
function copy(path: string, expected: string | undefined, dependency = false, sourcePath = path): void {
|
|
64
|
+
const source = resolve(root, sourcePath);
|
|
65
65
|
if (!path.startsWith('skills/') || !source.startsWith(resolve(root, 'skills') + '/')) throw new Error('Unsafe toolkit resource path');
|
|
66
66
|
const stat = lstatSync(source);
|
|
67
67
|
if (!stat.isFile() || stat.isSymbolicLink() || (!dependency && stat.nlink !== 1) || realpathSync(source) !== source) throw new Error('Unsafe toolkit resource: ' + path);
|
|
@@ -104,10 +104,10 @@ function copyPackageManifest(): void {
|
|
|
104
104
|
const packageManifest = { ...manifest, files, additions, packageOmissions };
|
|
105
105
|
writeGenerated('skills/source-manifest.json', Buffer.from(JSON.stringify(packageManifest, null, 2) + '\n'));
|
|
106
106
|
}
|
|
107
|
-
for (const file of [...manifest.files, ...(manifest.additions ?? [])]) if (!packageOmissionPaths.has(file.path)) copy(file.path, file.installedSha256);
|
|
107
|
+
for (const file of [...manifest.files, ...(manifest.additions ?? [])]) if (!packageOmissionPaths.has(file.path)) copy(file.path, file.installedSha256, false, file.sourcePath ?? file.path);
|
|
108
108
|
copyPackageManifest();
|
|
109
109
|
copy('skills/catalog.json');
|
|
110
|
-
for (const name of ['index.html','app.js','app.css']) copy(`skills/multi-task-flow/dist/${name}`);
|
|
110
|
+
for (const name of ['index.html','app.js','app.css']) copy(`skills/multi-task-flow/dist/${name}`, undefined, false, `skills/multi-task-flow-runtime/dist/${name}`);
|
|
111
111
|
if (requireDependencies) validateInstalledDependencies();
|
|
112
112
|
copyRootFile('AGENTS.md');
|
|
113
113
|
const packageIdentityBase = {
|
|
@@ -10,7 +10,7 @@ const temporary = realpathSync(mkdtempSync(join(tmpdir(), 'viewer-source-test-')
|
|
|
10
10
|
const output = join(temporary, 'package');
|
|
11
11
|
const env = { ...process.env, MULTI_TASK_FLOW_PROFILE:'test', MULTI_TASK_FLOW_ROOT:join(temporary,'state') };
|
|
12
12
|
try {
|
|
13
|
-
const build=spawnSync(process.execPath,[join(root,'skills/multi-task-flow/scripts/build-service.ts'),'--output',output,'--service-version','0.0.0-test'],{cwd:temporary,env,encoding:'utf8',timeout:120000});
|
|
13
|
+
const build=spawnSync(process.execPath,[join(root,'skills/multi-task-flow-runtime/scripts/build-service.ts'),'--output',output,'--service-version','0.0.0-test'],{cwd:temporary,env,encoding:'utf8',timeout:120000});
|
|
14
14
|
assert.equal(build.status,0,build.stderr);
|
|
15
15
|
const manifest=JSON.parse(readFileSync(join(output,'manifest.json'),'utf8'));
|
|
16
16
|
assert.equal(manifest.schema_version,2);
|
|
@@ -25,6 +25,11 @@ test("represents closed PRs without treating them as success", () => {
|
|
|
25
25
|
expect(parsed.state).toBe("closed");
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
+
test("represents a singleton task by the delivery-unit PR binding", () => {
|
|
29
|
+
const parsed = taskPrBinding.parse({ state: "shared_delivery_unit", delivery_unit_id: "du-1" });
|
|
30
|
+
expect(parsed).toEqual({ state: "shared_delivery_unit", delivery_unit_id: "du-1" });
|
|
31
|
+
});
|
|
32
|
+
|
|
28
33
|
test("rejects cyclic atomic graphs and empty planned milestones", () => {
|
|
29
34
|
const nodes = [
|
|
30
35
|
{ kind: "milestone", id: "m-1", title: "M1", state: "planned", scope: { kind: "delivery_unit", delivery_unit_id: "du-1" }, acceptance: [{ kind: "node_satisfied", node_id: "w-1" }] },
|
|
@@ -5,7 +5,7 @@ The agreed seam is the rendered mock example: click a task to open details and c
|
|
|
5
5
|
Build the existing `demo:static` surface and use its current owned preview server. In the Codex in-app browser REPL, bind the preview tab and viewport capability, import the checked-in runner, then run it:
|
|
6
6
|
|
|
7
7
|
```js
|
|
8
|
-
var regression = await import("file:///absolute/repo/skills/multi-task-flow/tests/browser/card-width-regression.ts");
|
|
8
|
+
var regression = await import("file:///absolute/repo/skills/multi-task-flow-runtime/tests/browser/card-width-regression.ts");
|
|
9
9
|
await regression.checkCardWidths(tab, viewport);
|
|
10
10
|
```
|
|
11
11
|
|