@guanlin4924/devflow-pi 0.1.56 → 0.1.59
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/package.json +1 -1
- package/plugins/devflow-core/extensions/devflow-core.ts +2 -0
- package/plugins/devflow-core/skills/wireframe-proposal/SKILL.md +8 -0
- package/plugins/devflow-core/skills/wireframe-validate/SKILL.md +17 -1
- package/plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_validate.py +32 -3
- package/src/constants.ts +1 -1
- package/src/runtime.ts +33 -5
package/package.json
CHANGED
|
@@ -3722,6 +3722,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3722
3722
|
"project-context-validate",
|
|
3723
3723
|
"orchestrator-manage-lessons",
|
|
3724
3724
|
"orchestrator-update-triage",
|
|
3725
|
+
"wireframe-validate",
|
|
3725
3726
|
]),
|
|
3726
3727
|
args: z.array(z.string()).default([]),
|
|
3727
3728
|
}),
|
|
@@ -3732,6 +3733,7 @@ export default function devflowCore(pi: ExtensionAPI) {
|
|
|
3732
3733
|
"project-context-validate": ["python3", "workflow-project-context/scripts/validate_initialization.py"],
|
|
3733
3734
|
"orchestrator-manage-lessons": ["ruby", "workflow-orchestrator/scripts/manage_lessons.rb"],
|
|
3734
3735
|
"orchestrator-update-triage": ["ruby", "workflow-orchestrator/scripts/update_triage_route.rb"],
|
|
3736
|
+
"wireframe-validate": ["python3", "workflow-orchestrator/scripts/wireframe_validate.py"],
|
|
3735
3737
|
};
|
|
3736
3738
|
const [runtime, relativeScript] = scripts[params.script];
|
|
3737
3739
|
const result = await runProcess(
|
|
@@ -11,3 +11,11 @@ Read the controlled requirement baseline and the wireframe schema/registry. Prod
|
|
|
11
11
|
- .planning/features/{feature}/questions.yaml
|
|
12
12
|
|
|
13
13
|
Use only registered element types and screen-level requirement_refs. Keep IDs stable during revision. Do not add API fields, coordinates, CSS, nested children, or approval fields. Emit valid YAML only and never claim that the proposal is validated or approved. The validator and Core decide legality, fingerprints, and approval.
|
|
14
|
+
|
|
15
|
+
The proposal file is strict. Its only top-level keys are `schema_version`, `feature`, and `screens`. Each screen must contain `id`, `title`, `requirement_refs`, `elements`, and `states: [default]`; optional screen keys are only `presentation`, `platforms`, and `actions`.
|
|
16
|
+
|
|
17
|
+
Allowed element types are only `page_header`, `filter_bar`, `table`, `card_list`, `bottom_navigation`, `tabs`, `input`, `select`, `button`, and `icon_button`. Do not emit `text`, `alert`, `modal`, `empty_state`, or `description_list`. Put explanatory text in supported `title`, `label`, or `meta` fields. `table.columns` must be objects shaped as `{id, label}`, and all nested item/field IDs must be stable IDs.
|
|
18
|
+
|
|
19
|
+
The questions file is also strict. Its only top-level keys are `schema_version`, `feature`, and `questions`; do not add `step`, `notes`, or other metadata. Each question must contain `id`, `text`, `kind`, and `status`, with optional `target` and `answer`.
|
|
20
|
+
|
|
21
|
+
Before finishing, compare both files against `skill://workflow-orchestrator/contracts/proposal.schema.yaml` and `skill://workflow-orchestrator/contracts/questions.schema.yaml`. Do not invent legacy fields such as `name`, `route_hint`, `proposal_id`, `source_refs`, `principles`, or `flows`.
|
|
@@ -5,4 +5,20 @@ description: Run deterministic validation for a wireframe proposal and build can
|
|
|
5
5
|
|
|
6
6
|
# Wireframe Validate
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Run the bundled validator through Core's `devflow_skill_script` tool:
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
devflow_skill_script(
|
|
12
|
+
script="wireframe-validate",
|
|
13
|
+
args=[
|
|
14
|
+
"--proposal", ".planning/features/<feature>/proposal.yaml",
|
|
15
|
+
"--traceability", ".planning/requirements/<feature>/traceability.json",
|
|
16
|
+
"--questions", ".planning/features/<feature>/questions.yaml",
|
|
17
|
+
"--canonical-out", ".planning/features/<feature>/canonical.json"
|
|
18
|
+
]
|
|
19
|
+
)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The bundled script is `plugins/devflow-core/skills/workflow-orchestrator/scripts/wireframe_validate.py`.
|
|
23
|
+
Do not read `skill://wireframe-validate/scripts/wireframe_validate.py`; that path does not exist.
|
|
24
|
+
Preserve its structured verdict: allow, await_user, or block_unsafe. Do not edit proposal or questions, repair unknown fields, write approval, or modify workflow state.
|
|
@@ -55,6 +55,37 @@ def issue(errors: list[dict[str, str]], code: str, path: str, message: str) -> N
|
|
|
55
55
|
errors.append({"code": code, "path": path, "message": message})
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
def traceability_refs(traceability: Any) -> set[str]:
|
|
59
|
+
"""Return graph-backed refs from both legacy and Core traceability views."""
|
|
60
|
+
if not isinstance(traceability, dict):
|
|
61
|
+
return set()
|
|
62
|
+
requirements = traceability.get("requirements")
|
|
63
|
+
if isinstance(requirements, list):
|
|
64
|
+
return {
|
|
65
|
+
item.get("id")
|
|
66
|
+
for item in requirements
|
|
67
|
+
if isinstance(item, dict)
|
|
68
|
+
and isinstance(item.get("id"), str)
|
|
69
|
+
and item.get("status") in {"confirmed", "approved"}
|
|
70
|
+
and item.get("scope", "in") == "in"
|
|
71
|
+
}
|
|
72
|
+
nodes = traceability.get("nodes")
|
|
73
|
+
if not isinstance(nodes, list):
|
|
74
|
+
return set()
|
|
75
|
+
allowed_kinds = {"requirement", "rule", "behavior", "boundary"}
|
|
76
|
+
refs: set[str] = set()
|
|
77
|
+
for node in nodes:
|
|
78
|
+
if not isinstance(node, dict) or node.get("kind") not in allowed_kinds:
|
|
79
|
+
continue
|
|
80
|
+
node_id = node.get("id")
|
|
81
|
+
status = node.get("status")
|
|
82
|
+
data = node.get("data") if isinstance(node.get("data"), dict) else {}
|
|
83
|
+
scope = data.get("scope", node.get("scope", "in"))
|
|
84
|
+
if isinstance(node_id, str) and status in {"candidate", "confirmed", "approved"} and scope != "out":
|
|
85
|
+
refs.add(node_id)
|
|
86
|
+
return refs
|
|
87
|
+
|
|
88
|
+
|
|
58
89
|
def unknown_keys(value: Any, allowed: set[str], path: str, errors: list[dict[str, str]]) -> None:
|
|
59
90
|
if not isinstance(value, dict):
|
|
60
91
|
return
|
|
@@ -234,9 +265,7 @@ def validate(data: Any, traceability: Any, schema: dict[str, Any], business_mode
|
|
|
234
265
|
if not isinstance(screens, list) or not screens:
|
|
235
266
|
issue(errors, "WIREFRAME_SCREENS_MISSING", "screens", "must be a non-empty array")
|
|
236
267
|
return errors, None
|
|
237
|
-
reqs =
|
|
238
|
-
if isinstance(traceability, dict):
|
|
239
|
-
reqs = {r.get("id") for r in traceability.get("requirements", []) if isinstance(r, dict) and r.get("status") in {"confirmed", "approved"} and r.get("scope", "in") == "in"}
|
|
268
|
+
reqs = traceability_refs(traceability)
|
|
240
269
|
screen_ids: set[str] = set()
|
|
241
270
|
canonical_screens: list[dict[str, Any]] = []
|
|
242
271
|
for i, screen in enumerate(screens):
|
package/src/constants.ts
CHANGED
package/src/runtime.ts
CHANGED
|
@@ -278,6 +278,29 @@ export async function runInteractiveRuntime(project: ProjectContext, options: Ru
|
|
|
278
278
|
void consumeControlledRollover();
|
|
279
279
|
}, delay);
|
|
280
280
|
};
|
|
281
|
+
const waitForControlledRollover = async (): Promise<void> => {
|
|
282
|
+
const requestPath = path.join(project.root, ".planning", "rollover-request.json");
|
|
283
|
+
let requestObserved = false;
|
|
284
|
+
// The input hook may finish its async state transition just after
|
|
285
|
+
// submitInteractiveInput resolves. Wait briefly for that durable request
|
|
286
|
+
// so approval starts the fresh session without requiring another input.
|
|
287
|
+
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
288
|
+
try {
|
|
289
|
+
await readFile(requestPath, "utf8");
|
|
290
|
+
requestObserved = true;
|
|
291
|
+
} catch {
|
|
292
|
+
if (requestObserved) return;
|
|
293
|
+
}
|
|
294
|
+
await consumeControlledRollover();
|
|
295
|
+
try {
|
|
296
|
+
await readFile(requestPath, "utf8");
|
|
297
|
+
} catch {
|
|
298
|
+
if (requestObserved) return;
|
|
299
|
+
}
|
|
300
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
301
|
+
}
|
|
302
|
+
scheduleControlledRolloverRetry();
|
|
303
|
+
};
|
|
281
304
|
// A control-plane request may be created by an extension input handler that
|
|
282
305
|
// marks the message handled, so it is not guaranteed to pass through the
|
|
283
306
|
// normal submitInteractiveInput return path. Keep a small runtime-owned
|
|
@@ -298,11 +321,16 @@ export async function runInteractiveRuntime(project: ProjectContext, options: Ru
|
|
|
298
321
|
} finally {
|
|
299
322
|
interactiveSubmissionInFlight = false;
|
|
300
323
|
}
|
|
301
|
-
|
|
324
|
+
// Only an explicit approval can create a user-driven rollover after this
|
|
325
|
+
// input. Ordinary chat messages must return to the prompt immediately.
|
|
326
|
+
if (/^(?:批准|确认|同意|通过)(?:\s*(?:(?:full|完整|全量)?\s*初始化|执行|完成|继续|推进|当前(?:方案|步骤|计划)))?[。!!]?$/i.test(input.trim())) {
|
|
327
|
+
await waitForControlledRollover();
|
|
328
|
+
} else {
|
|
329
|
+
await consumeControlledRollover();
|
|
330
|
+
scheduleControlledRolloverRetry();
|
|
331
|
+
}
|
|
302
332
|
// The submitted turn may create a durable rollover request after the
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
// trigger the fresh-session handoff.
|
|
306
|
-
scheduleControlledRolloverRetry();
|
|
333
|
+
// input hook settles; the bounded wait above handles the normal path and
|
|
334
|
+
// the poller remains as a recovery path for a slow or interrupted hook.
|
|
307
335
|
}
|
|
308
336
|
}
|