@viccydev/pi-fpa 0.7.2 → 0.8.1
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 +15 -4
- package/extensions/fpa-artifacts/index.ts +22 -0
- package/extensions/fpa-dashboard/compat-publisher.ts +45 -0
- package/extensions/fpa-dashboard/coordinator.ts +3 -2
- package/extensions/fpa-dashboard/index.ts +237 -3
- package/extensions/fpa-dashboard/module-publisher.ts +390 -0
- package/extensions/fpa-dashboard/stage-projector.ts +304 -0
- package/extensions/fpa-dashboard/strategy-decision.ts +192 -0
- package/extensions/fpa-routing-guard/graph-installer.ts +234 -0
- package/extensions/fpa-routing-guard/index.ts +131 -11
- package/graphs/fpa-forecast-freeze.json +95 -0
- package/graphs/fpa-strategy-planning.json +159 -0
- package/package.json +4 -3
- package/prompts/fpa-plan-cycle.md +18 -5
- package/skills/fpa-apply-core-rules/SKILL.md +9 -6
- package/skills/fpa-apply-core-rules/references/core-rules.md +21 -10
- package/skills/fpa-forecast-approved-strategy/SKILL.md +11 -8
- package/skills/fpa-recommend-strategy/SKILL.md +1 -1
- package/skills/fpa-recommend-strategy/references/artifact-contract.md +2 -1
- package/skills/fpa-refresh-dashboard/SKILL.md +47 -4
- package/skills/fpa-review-strategy/references/artifact-contract.md +22 -0
package/README.md
CHANGED
|
@@ -33,7 +33,9 @@ Skill:
|
|
|
33
33
|
阶段的请求都必须先查询 Graph catalog,再运行当前第一个满足入口条件的
|
|
34
34
|
Graph;在此之前不得调用 `fpa_*` 工具或写 Artifact。四个工作流的边界为:
|
|
35
35
|
|
|
36
|
-
- `fpa-strategy-planning
|
|
36
|
+
- `fpa-strategy-planning`:在一个组合 Graph 中完成规划至
|
|
37
|
+
`reviewed_strategy_handoff` 后停止。数据/驱动审计与三类策略场景分别并行;
|
|
38
|
+
并行子节点只读,正式 Artifact 由后续汇总节点顺序写入。
|
|
37
39
|
- `fpa-forecast-freeze`:只接受精确 handoff,经人工批准后冻结 Forecast。
|
|
38
40
|
- `fpa-strategy-execution`:只接受精确 committed Forecast ref 和单独执行授权。
|
|
39
41
|
- `fpa-cycle-review`:只接受精确 Forecast/Execution refs 与新 Actuals。
|
|
@@ -47,6 +49,11 @@ Graph 的业务阻塞不能触发父 Agent 降级执行。Graph 工具不可用
|
|
|
47
49
|
它从展开后的请求识别多阶段 FP&A 意图,要求目标 Graph 与请求阶段一致,并
|
|
48
50
|
阻断父 Agent 的提前 `fpa_*` 调用和 `artifacts/` 写入。Graph worker 没有
|
|
49
51
|
`graph_list`/`graph_run` 时不会激活该护栏,因此节点仍可执行被分配的单一阶段。
|
|
52
|
+
首次在可信项目进入 FP&A 工作流时,护栏会把 package 自带的 Graph 模板安装到
|
|
53
|
+
`.agent-graph/graphs`;较旧的同名模板和已经退役的拆分 Graph 会先移入
|
|
54
|
+
`.agent-graph/graphs_backup_v*` 再更新,因此 `graph_list` 能直接发现当前组合
|
|
55
|
+
Graph,同时保留可恢复的旧定义。非可信项目、符号链接目录和更高版本的项目
|
|
56
|
+
Graph 均不会被覆盖。
|
|
50
57
|
|
|
51
58
|
## 数据 Extension(fpa-data)
|
|
52
59
|
|
|
@@ -75,10 +82,14 @@ Extension 内置的关键防护:
|
|
|
75
82
|
|
|
76
83
|
`fpa-artifacts` 提供 `fpa_artifact_commit`,对 `approved_cycle_forecast` 和 `execution_receipt` 做严格字段校验、对账、稳定指纹和原子落盘。只有工具返回成功后的 JSON 才是冻结产物,Markdown 不是正式数据源。
|
|
77
84
|
|
|
78
|
-
`fpa-dashboard`
|
|
85
|
+
`fpa-dashboard` 提供分模块发布工具和兼容的闭环刷新工具:
|
|
79
86
|
|
|
80
87
|
| 工具 | 作用 |
|
|
81
88
|
| --- | --- |
|
|
89
|
+
| `fpa_dashboard_module_status` | 只读检查各独立模块及当前 Dashboard revision |
|
|
90
|
+
| `fpa_dashboard_publish_review` | 只发布 `period-review`,保留其他模块 |
|
|
91
|
+
| `fpa_dashboard_publish_strategy` | 只发布带确认/修改动作的 `next-strategy`,并绑定当前主会话 |
|
|
92
|
+
| `fpa_dashboard_publish_forecast` | 确认策略并冻结预测后,只发布 `next-forecast` |
|
|
82
93
|
| `fpa_dashboard_status` | 只读检查当前 manifest、构建回执和各数据集是否可读 |
|
|
83
94
|
| `fpa_dashboard_refresh` | 从冻结预测、可选执行回执和实时 Actuals 生成固定的闭环看板;先 preview,再携带相同指纹原子 publish |
|
|
84
95
|
| `fpa_dashboard_refresh_queue` | 检查或处理持久刷新队列;主 Agent 用 `enqueue_artifact` 显式交接 Forecast/Execution,Actuals watermark 按 SLA 轮询并幂等发布 |
|
|
@@ -148,12 +159,12 @@ pi list
|
|
|
148
159
|
团队分发建议使用固定 Git tag:
|
|
149
160
|
|
|
150
161
|
```bash
|
|
151
|
-
pi install git:github.com/linyqh/pi-fpa@v0.
|
|
162
|
+
pi install git:github.com/linyqh/pi-fpa@v0.8.1
|
|
152
163
|
```
|
|
153
164
|
|
|
154
165
|
## 发布到 npm
|
|
155
166
|
|
|
156
|
-
发布动作由 GitHub Release 触发。Release 标签必须严格使用 `v<package.json version>`,例如版本 `0.
|
|
167
|
+
发布动作由 GitHub Release 触发。Release 标签必须严格使用 `v<package.json version>`,例如版本 `0.8.1` 对应 `v0.8.1`。工作流会检出该标签,执行 `npm ci`、`npm test` 和包内容预检,全部通过后发布公开包 `@viccydev/pi-fpa`。普通 Release 发布到 `latest`,Prerelease 发布到 `next`。
|
|
157
168
|
|
|
158
169
|
发布认证使用 npm Trusted Publishing / OIDC,不使用长期 npm Token。npm 包后台的 Trusted Publisher 配置为:
|
|
159
170
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
1
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
4
|
import { Type } from "typebox";
|
|
3
5
|
|
|
@@ -7,6 +9,7 @@ import {
|
|
|
7
9
|
commitArtifactFromPath,
|
|
8
10
|
readArtifactByRef,
|
|
9
11
|
readProjectJsonFile,
|
|
12
|
+
stableJson,
|
|
10
13
|
type ArtifactRefV2,
|
|
11
14
|
} from "./store.ts";
|
|
12
15
|
|
|
@@ -115,6 +118,25 @@ export default function fpaArtifactsExtension(pi: ExtensionAPI): void {
|
|
|
115
118
|
},
|
|
116
119
|
});
|
|
117
120
|
|
|
121
|
+
pi.registerTool({
|
|
122
|
+
name: "fpa_json_fingerprint",
|
|
123
|
+
label: "Fingerprint FP&A JSON Artifacts",
|
|
124
|
+
description: "Read project-local JSON artifacts and return deterministic SHA-256 fingerprints over their canonical JSON form. Use this to bind reviewed handoffs to exact proposal and review bodies.",
|
|
125
|
+
promptSnippet: "Fingerprint exact FP&A JSON artifacts for a version-bound handoff",
|
|
126
|
+
parameters: Type.Object({
|
|
127
|
+
paths: Type.Array(Type.String({ minLength: 1 }), { minItems: 1, maxItems: 8 }),
|
|
128
|
+
}, { additionalProperties: false }),
|
|
129
|
+
executionMode: "parallel",
|
|
130
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
131
|
+
if (new Set(params.paths).size !== params.paths.length) throw new Error("Fingerprint paths must be unique.");
|
|
132
|
+
const entries = await Promise.all(params.paths.map(async (path) => {
|
|
133
|
+
const value = await readProjectJsonFile(ctx.cwd, path, "path");
|
|
134
|
+
return [path, createHash("sha256").update(stableJson(value)).digest("hex")] as const;
|
|
135
|
+
}));
|
|
136
|
+
return toolResult({ status: "fingerprinted", fingerprints: Object.fromEntries(entries) });
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
118
140
|
pi.registerTool({
|
|
119
141
|
name: "fpa_forecast_compose",
|
|
120
142
|
label: "Compose Approved Forecast",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { DashboardBuild } from "./projector.ts";
|
|
5
|
+
import type { DashboardProjectorProvenance, DashboardProjectorRuntime } from "./provenance.ts";
|
|
6
|
+
import { publishDashboard, resolveDashboardDir } from "./publisher.ts";
|
|
7
|
+
import { publishDashboardModule, readDashboardModuleManifest } from "./module-publisher.ts";
|
|
8
|
+
import type { DashboardActualsSnapshot } from "./source.ts";
|
|
9
|
+
|
|
10
|
+
interface Options {
|
|
11
|
+
cwd: string;
|
|
12
|
+
build: DashboardBuild;
|
|
13
|
+
projector: DashboardProjectorProvenance;
|
|
14
|
+
runtime: DashboardProjectorRuntime;
|
|
15
|
+
actualsSnapshot?: DashboardActualsSnapshot;
|
|
16
|
+
allowBacktestDisplay?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function publishDashboardProjection(options: Options): Promise<{ dashboardDir: string; fingerprint: string; publishedDatasets: number }> {
|
|
20
|
+
const dashboardDir = await resolveDashboardDir(options.cwd);
|
|
21
|
+
let schemaVersion: number | undefined;
|
|
22
|
+
try {
|
|
23
|
+
schemaVersion = (JSON.parse(await readFile(join(dashboardDir, "manifest.json"), "utf8")) as { schemaVersion?: number }).schemaVersion;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
26
|
+
}
|
|
27
|
+
if (schemaVersion !== 2) return publishDashboard(options);
|
|
28
|
+
if (options.build.source.forecast_role === "backtest" && options.allowBacktestDisplay !== true) {
|
|
29
|
+
throw new Error("A backtest cannot replace dashboard execution evidence without explicit user authorization.");
|
|
30
|
+
}
|
|
31
|
+
const current = await readDashboardModuleManifest(options.cwd);
|
|
32
|
+
const published = await publishDashboardModule({
|
|
33
|
+
cwd: options.cwd,
|
|
34
|
+
dashboardTitle: options.build.title,
|
|
35
|
+
expectedDashboardRevision: current?.dashboardRevision ?? null,
|
|
36
|
+
module: {
|
|
37
|
+
id: "execution-evidence",
|
|
38
|
+
title: "经营执行与校准证据",
|
|
39
|
+
status: "published",
|
|
40
|
+
source: { ...options.build.source, warnings: options.build.warnings, projector: options.projector, runtime: options.runtime },
|
|
41
|
+
widgets: options.build.widgets,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
return { dashboardDir: published.dashboardDir, fingerprint: published.moduleRevision, publishedDatasets: published.publishedDatasets };
|
|
45
|
+
}
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
type ArtifactRefV2,
|
|
10
10
|
type ForecastRole,
|
|
11
11
|
} from "../fpa-artifacts/store.ts";
|
|
12
|
-
import { dashboardBuildFingerprint,
|
|
12
|
+
import { dashboardBuildFingerprint, resolveDashboardDir } from "./publisher.ts";
|
|
13
|
+
import { publishDashboardProjection } from "./compat-publisher.ts";
|
|
13
14
|
import { buildDashboardProjection } from "./service.ts";
|
|
14
15
|
|
|
15
16
|
const EVENT_MAX_BYTES = 64 * 1024;
|
|
@@ -723,7 +724,7 @@ async function defaultWorker(event: DashboardRefreshEvent, projectRoot: string,
|
|
|
723
724
|
if (event.desired_generation_id && generationId !== event.desired_generation_id) {
|
|
724
725
|
throw new ObsoleteDashboardRefreshError("Dashboard inputs changed after polling; the stale desired generation must be replanned.");
|
|
725
726
|
}
|
|
726
|
-
await
|
|
727
|
+
await publishDashboardProjection({ cwd: projectRoot, build: result.build, projector: result.projector, runtime: result.runtime, actualsSnapshot: result.actuals });
|
|
727
728
|
return {
|
|
728
729
|
generation_id: generationId,
|
|
729
730
|
data_as_of: result.build.source.data_as_of,
|
|
@@ -2,8 +2,19 @@ import { StringEnum } from "@earendil-works/pi-ai";
|
|
|
2
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
|
|
5
|
-
import { readArtifactByRef, type ArtifactRefV2 } from "../fpa-artifacts/store.ts";
|
|
6
|
-
import { dashboardBuildFingerprint
|
|
5
|
+
import { readArtifactByRef, readProjectJsonFile, type ArtifactRefV2 } from "../fpa-artifacts/store.ts";
|
|
6
|
+
import { dashboardBuildFingerprint } from "./publisher.ts";
|
|
7
|
+
import { publishDashboardProjection } from "./compat-publisher.ts";
|
|
8
|
+
import {
|
|
9
|
+
dashboardModuleBuildFingerprint,
|
|
10
|
+
publishDashboardModule,
|
|
11
|
+
readDashboardModuleManifest,
|
|
12
|
+
readDashboardModuleSource,
|
|
13
|
+
transitionStrategyModule,
|
|
14
|
+
} from "./module-publisher.ts";
|
|
15
|
+
import { projectForecastModule, projectReviewModule, projectStrategyModule } from "./stage-projector.ts";
|
|
16
|
+
import type { ApprovedCycleForecast } from "../fpa-artifacts/contracts.ts";
|
|
17
|
+
import { commitStrategyDecision, createStrategyDecisionRequest, readCommittedStrategyDecision } from "./strategy-decision.ts";
|
|
7
18
|
import {
|
|
8
19
|
enqueueDashboardRefresh,
|
|
9
20
|
enqueueDashboardRefreshForArtifact,
|
|
@@ -28,6 +39,229 @@ export default function fpaDashboardExtension(pi: ExtensionAPI): void {
|
|
|
28
39
|
entry_id: Type.String({ pattern: "^[a-f0-9]{64}$" }),
|
|
29
40
|
body_fingerprint: Type.String({ pattern: "^[a-f0-9]{64}$" }),
|
|
30
41
|
}, { additionalProperties: false });
|
|
42
|
+
pi.registerTool({
|
|
43
|
+
name: "fpa_dashboard_module_status",
|
|
44
|
+
label: "FP&A Dashboard Module Status",
|
|
45
|
+
description: "Inspect independently published FP&A dashboard modules and their current dashboard revision without changing files.",
|
|
46
|
+
promptSnippet: "Inspect the independently published FP&A dashboard modules",
|
|
47
|
+
parameters: Type.Object({}, { additionalProperties: false }),
|
|
48
|
+
executionMode: "parallel",
|
|
49
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
50
|
+
const manifest = await readDashboardModuleManifest(ctx.cwd);
|
|
51
|
+
return toolResult(manifest ? {
|
|
52
|
+
status: "available",
|
|
53
|
+
dashboard_revision: manifest.dashboardRevision,
|
|
54
|
+
module_order: manifest.moduleOrder,
|
|
55
|
+
modules: Object.fromEntries(Object.entries(manifest.modules).map(([id, module]) => [id, module ? {
|
|
56
|
+
status: module.status,
|
|
57
|
+
revision: module.revision,
|
|
58
|
+
updated_at: module.updatedAt,
|
|
59
|
+
interactive: module.interaction?.kind === "strategy-decision",
|
|
60
|
+
} : null])),
|
|
61
|
+
} : { status: "missing" });
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
pi.registerTool({
|
|
66
|
+
name: "fpa_dashboard_publish_review",
|
|
67
|
+
label: "Publish FP&A Period Review Module",
|
|
68
|
+
description: "Preview or publish only the period-review dashboard module from a project driver_analysis JSON artifact. Other dashboard modules are preserved.",
|
|
69
|
+
promptSnippet: "Publish the completed period analysis as its own dashboard module",
|
|
70
|
+
promptGuidelines: [
|
|
71
|
+
"This tool is called by the main Agent after the analysis Graph returns; never add it to a Graph tool allowlist.",
|
|
72
|
+
"Preview first, then publish with the exact preview fingerprint and dashboard revision.",
|
|
73
|
+
],
|
|
74
|
+
parameters: Type.Object({
|
|
75
|
+
mode: StringEnum(["preview", "publish"] as const),
|
|
76
|
+
analysis_path: Type.String({ minLength: 1 }),
|
|
77
|
+
expected_preview_fingerprint: Type.Optional(Type.String({ pattern: "^[a-f0-9]{64}$" })),
|
|
78
|
+
expected_dashboard_revision: Type.Optional(Type.Union([Type.String({ pattern: "^[a-f0-9]{64}$" }), Type.Null()])),
|
|
79
|
+
dashboard_title: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
|
|
80
|
+
}, { additionalProperties: false }),
|
|
81
|
+
executionMode: "sequential",
|
|
82
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
83
|
+
const module = projectReviewModule(await readProjectJsonFile(ctx.cwd, params.analysis_path, "analysis_path"));
|
|
84
|
+
const previewFingerprint = dashboardModuleBuildFingerprint(module);
|
|
85
|
+
const current = await readDashboardModuleManifest(ctx.cwd);
|
|
86
|
+
if (params.mode === "preview") return toolResult({
|
|
87
|
+
status: "ready",
|
|
88
|
+
module_id: module.id,
|
|
89
|
+
preview_fingerprint: previewFingerprint,
|
|
90
|
+
dashboard_revision: current?.dashboardRevision ?? null,
|
|
91
|
+
widget_count: module.widgets.length,
|
|
92
|
+
});
|
|
93
|
+
if (params.expected_preview_fingerprint !== previewFingerprint) throw new Error("Review module inputs changed after preview; preview again before publishing.");
|
|
94
|
+
if (params.expected_dashboard_revision === undefined) throw new Error("Publish requires expected_dashboard_revision from preview, including null for a new dashboard.");
|
|
95
|
+
const published = await publishDashboardModule({
|
|
96
|
+
cwd: ctx.cwd,
|
|
97
|
+
module,
|
|
98
|
+
dashboardTitle: params.dashboard_title,
|
|
99
|
+
expectedDashboardRevision: params.expected_dashboard_revision,
|
|
100
|
+
});
|
|
101
|
+
return toolResult({ status: "published", module_id: module.id, preview_fingerprint: previewFingerprint, dashboard_revision: published.dashboardRevision, module_revision: published.moduleRevision });
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
pi.registerTool({
|
|
106
|
+
name: "fpa_dashboard_publish_strategy",
|
|
107
|
+
label: "Publish FP&A Strategy Module",
|
|
108
|
+
description: "Preview or publish only the reviewed next-strategy module and create a server-side decision action bound to the calling main session. Other modules are preserved.",
|
|
109
|
+
promptSnippet: "Publish the reviewed strategy for human confirmation in the dashboard",
|
|
110
|
+
promptGuidelines: [
|
|
111
|
+
"Call this from the main Agent after the strategy Graph returns; Graph nodes must never publish dashboard modules.",
|
|
112
|
+
"Preview first. Publishing binds the confirmation action to this exact main session and reviewed handoff fingerprint.",
|
|
113
|
+
],
|
|
114
|
+
parameters: Type.Object({
|
|
115
|
+
mode: StringEnum(["preview", "publish"] as const),
|
|
116
|
+
proposal_path: Type.String({ minLength: 1 }),
|
|
117
|
+
review_path: Type.String({ minLength: 1 }),
|
|
118
|
+
handoff_path: Type.String({ minLength: 1 }),
|
|
119
|
+
scope_id: Type.String({ minLength: 1, maxLength: 256 }),
|
|
120
|
+
cycle_id: Type.String({ minLength: 1, maxLength: 256 }),
|
|
121
|
+
forecast_role: Type.String({ minLength: 1, maxLength: 64 }),
|
|
122
|
+
expected_preview_fingerprint: Type.Optional(Type.String({ pattern: "^[a-f0-9]{64}$" })),
|
|
123
|
+
expected_dashboard_revision: Type.Optional(Type.Union([Type.String({ pattern: "^[a-f0-9]{64}$" }), Type.Null()])),
|
|
124
|
+
dashboard_title: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
|
|
125
|
+
}, { additionalProperties: false }),
|
|
126
|
+
executionMode: "sequential",
|
|
127
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
128
|
+
const proposal = await readProjectJsonFile(ctx.cwd, params.proposal_path, "proposal_path");
|
|
129
|
+
const review = await readProjectJsonFile(ctx.cwd, params.review_path, "review_path");
|
|
130
|
+
const handoff = await readProjectJsonFile(ctx.cwd, params.handoff_path, "handoff_path");
|
|
131
|
+
const module = projectStrategyModule(proposal, handoff, review, {
|
|
132
|
+
scope_id: params.scope_id,
|
|
133
|
+
cycle_id: params.cycle_id,
|
|
134
|
+
forecast_role: params.forecast_role,
|
|
135
|
+
});
|
|
136
|
+
const previewFingerprint = dashboardModuleBuildFingerprint(module);
|
|
137
|
+
const current = await readDashboardModuleManifest(ctx.cwd);
|
|
138
|
+
if (params.mode === "preview") return toolResult({
|
|
139
|
+
status: "ready",
|
|
140
|
+
module_id: module.id,
|
|
141
|
+
preview_fingerprint: previewFingerprint,
|
|
142
|
+
dashboard_revision: current?.dashboardRevision ?? null,
|
|
143
|
+
strategy_version: module.source.strategy_version,
|
|
144
|
+
widget_count: module.widgets.length,
|
|
145
|
+
});
|
|
146
|
+
if (params.expected_preview_fingerprint !== previewFingerprint) throw new Error("Strategy module inputs changed after preview; preview again before publishing.");
|
|
147
|
+
if (params.expected_dashboard_revision === undefined) throw new Error("Publish requires expected_dashboard_revision from preview, including null for a new dashboard.");
|
|
148
|
+
const handoffFingerprint = module.source.handoff_fingerprint as string;
|
|
149
|
+
const request = await createStrategyDecisionRequest(ctx.cwd, {
|
|
150
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
151
|
+
strategyVersion: module.source.strategy_version as string,
|
|
152
|
+
handoffFingerprint,
|
|
153
|
+
});
|
|
154
|
+
module.interaction = { kind: "strategy-decision", action_id: request.actionId };
|
|
155
|
+
const published = await publishDashboardModule({
|
|
156
|
+
cwd: ctx.cwd,
|
|
157
|
+
module,
|
|
158
|
+
dashboardTitle: params.dashboard_title,
|
|
159
|
+
expectedDashboardRevision: params.expected_dashboard_revision,
|
|
160
|
+
});
|
|
161
|
+
return toolResult({
|
|
162
|
+
status: "awaiting_decision",
|
|
163
|
+
module_id: module.id,
|
|
164
|
+
preview_fingerprint: previewFingerprint,
|
|
165
|
+
dashboard_revision: published.dashboardRevision,
|
|
166
|
+
module_revision: published.moduleRevision,
|
|
167
|
+
action_id: request.actionId,
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
pi.registerTool({
|
|
173
|
+
name: "fpa_dashboard_publish_forecast",
|
|
174
|
+
label: "Publish FP&A Forecast Module",
|
|
175
|
+
description: "Preview or publish only the next-forecast dashboard module from an exact committed approved forecast. Other dashboard modules are preserved.",
|
|
176
|
+
promptSnippet: "Publish the confirmed strategy forecast as its own dashboard module",
|
|
177
|
+
promptGuidelines: [
|
|
178
|
+
"Call this from the main Agent only after fpa_strategy_decision_commit confirmed the exact strategy and the forecast Graph returned an immutable forecast ref.",
|
|
179
|
+
"Never add this tool to a Graph tool allowlist. Preview first and publish with the exact preview fingerprint and dashboard revision.",
|
|
180
|
+
],
|
|
181
|
+
parameters: Type.Object({
|
|
182
|
+
mode: StringEnum(["preview", "publish"] as const),
|
|
183
|
+
forecast_ref: artifactRefSchema,
|
|
184
|
+
strategy_decision: Type.Object({
|
|
185
|
+
action_id: Type.String({ pattern: "^[a-f0-9]{64}$" }),
|
|
186
|
+
decision_fingerprint: Type.String({ pattern: "^[a-f0-9]{64}$" }),
|
|
187
|
+
}, { additionalProperties: false }),
|
|
188
|
+
expected_preview_fingerprint: Type.Optional(Type.String({ pattern: "^[a-f0-9]{64}$" })),
|
|
189
|
+
expected_dashboard_revision: Type.Optional(Type.Union([Type.String({ pattern: "^[a-f0-9]{64}$" }), Type.Null()])),
|
|
190
|
+
dashboard_title: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
|
|
191
|
+
}, { additionalProperties: false }),
|
|
192
|
+
executionMode: "sequential",
|
|
193
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
194
|
+
const decision = await readCommittedStrategyDecision(ctx.cwd, params.strategy_decision.action_id, params.strategy_decision.decision_fingerprint);
|
|
195
|
+
if (decision.decision !== "confirm") throw new Error("Forecast publication requires a confirmed strategy decision.");
|
|
196
|
+
const read = await readArtifactByRef(ctx.cwd, params.forecast_ref as ArtifactRefV2);
|
|
197
|
+
if (read.artifact.artifact_type !== "approved_cycle_forecast") throw new Error("Forecast module requires an approved_cycle_forecast artifact ref.");
|
|
198
|
+
const forecast = read.artifact as ApprovedCycleForecast;
|
|
199
|
+
if (forecast.status === "blocked" || !forecast.approval_conditions_satisfied) throw new Error("Forecast module requires a completed forecast whose approval conditions are satisfied.");
|
|
200
|
+
if (forecast.strategy_version !== decision.strategy_version) throw new Error("Forecast strategy_version does not match the confirmed strategy decision.");
|
|
201
|
+
const currentStrategy = (await readDashboardModuleManifest(ctx.cwd))?.modules["next-strategy"];
|
|
202
|
+
if (!currentStrategy || currentStrategy.status !== "approved") throw new Error("Forecast publication requires the current strategy dashboard module to be approved.");
|
|
203
|
+
const strategySource = await readDashboardModuleSource(ctx.cwd, "next-strategy");
|
|
204
|
+
if (strategySource?.strategy_version !== decision.strategy_version || strategySource?.handoff_fingerprint !== decision.handoff_fingerprint) {
|
|
205
|
+
throw new Error("Approved strategy module does not match the confirmed decision lineage.");
|
|
206
|
+
}
|
|
207
|
+
const module = projectForecastModule(forecast, params.forecast_ref as ArtifactRefV2);
|
|
208
|
+
const previewFingerprint = dashboardModuleBuildFingerprint(module);
|
|
209
|
+
const current = await readDashboardModuleManifest(ctx.cwd);
|
|
210
|
+
if (params.mode === "preview") return toolResult({
|
|
211
|
+
status: "ready",
|
|
212
|
+
module_id: module.id,
|
|
213
|
+
preview_fingerprint: previewFingerprint,
|
|
214
|
+
dashboard_revision: current?.dashboardRevision ?? null,
|
|
215
|
+
forecast_version: forecast.forecast_version,
|
|
216
|
+
widget_count: module.widgets.length,
|
|
217
|
+
});
|
|
218
|
+
if (params.expected_preview_fingerprint !== previewFingerprint) throw new Error("Forecast module inputs changed after preview; preview again before publishing.");
|
|
219
|
+
if (params.expected_dashboard_revision === undefined) throw new Error("Publish requires expected_dashboard_revision from preview, including null for a new dashboard.");
|
|
220
|
+
const published = await publishDashboardModule({
|
|
221
|
+
cwd: ctx.cwd,
|
|
222
|
+
module,
|
|
223
|
+
dashboardTitle: params.dashboard_title,
|
|
224
|
+
expectedDashboardRevision: params.expected_dashboard_revision,
|
|
225
|
+
});
|
|
226
|
+
return toolResult({ status: "published", module_id: module.id, preview_fingerprint: previewFingerprint, dashboard_revision: published.dashboardRevision, module_revision: published.moduleRevision });
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
pi.registerTool({
|
|
231
|
+
name: "fpa_strategy_decision_commit",
|
|
232
|
+
label: "Commit FP&A Strategy Decision",
|
|
233
|
+
description: "Commit an idempotent human strategy confirmation or change request from the original main session, then transition the strategy dashboard module.",
|
|
234
|
+
promptSnippet: "Commit the dashboard strategy decision in the original main session",
|
|
235
|
+
parameters: Type.Object({
|
|
236
|
+
action_id: Type.String({ pattern: "^[a-f0-9]{64}$" }),
|
|
237
|
+
decision: StringEnum(["confirm", "request_changes"] as const),
|
|
238
|
+
feedback: Type.Optional(Type.String({ minLength: 1, maxLength: 8000 })),
|
|
239
|
+
}, { additionalProperties: false }),
|
|
240
|
+
executionMode: "sequential",
|
|
241
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
242
|
+
const committed = await commitStrategyDecision(ctx.cwd, {
|
|
243
|
+
actionId: params.action_id,
|
|
244
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
245
|
+
decision: params.decision,
|
|
246
|
+
feedback: params.feedback,
|
|
247
|
+
});
|
|
248
|
+
const transitioned = await transitionStrategyModule(ctx.cwd, {
|
|
249
|
+
actionId: params.action_id,
|
|
250
|
+
status: params.decision === "confirm" ? "approved" : "changes_requested",
|
|
251
|
+
});
|
|
252
|
+
return toolResult({
|
|
253
|
+
status: "committed",
|
|
254
|
+
decision: committed.decision,
|
|
255
|
+
decision_fingerprint: committed.decisionFingerprint,
|
|
256
|
+
decision_path: committed.path,
|
|
257
|
+
strategy_version: committed.strategyVersion,
|
|
258
|
+
handoff_fingerprint: committed.handoffFingerprint,
|
|
259
|
+
dashboard_revision: transitioned.dashboardRevision,
|
|
260
|
+
next_stage: params.decision === "confirm" ? "run fpa-forecast-freeze Graph, then publish next-forecast module" : "rerun strategy planning with the exact feedback, then republish next-strategy module",
|
|
261
|
+
});
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
|
|
31
265
|
pi.registerTool({
|
|
32
266
|
name: "fpa_dashboard_status",
|
|
33
267
|
label: "FP&A Dashboard Status",
|
|
@@ -157,7 +391,7 @@ export default function fpaDashboardExtension(pi: ExtensionAPI): void {
|
|
|
157
391
|
if (params.expected_preview_fingerprint !== previewFingerprint) {
|
|
158
392
|
throw new Error("Dashboard inputs changed after preview; run preview again before publishing.");
|
|
159
393
|
}
|
|
160
|
-
const published = await
|
|
394
|
+
const published = await publishDashboardProjection({
|
|
161
395
|
cwd: ctx.cwd,
|
|
162
396
|
build,
|
|
163
397
|
projector,
|