@zalom/plastic 1.0.0-beta.1 → 1.0.0-beta.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/PLASTIC.md +21 -5
- package/agents/plastic-brainstorming.md +9 -1
- package/agents/plastic-executor.md +10 -0
- package/agents/plastic-intent-curator.md +7 -5
- package/agents/plastic-planner.md +11 -1
- package/agents/plastic-spec-specialist.md +9 -1
- package/hooks/statusline +150 -41
- package/package.json +1 -1
- package/scripts/agent-report +142 -0
- package/scripts/dashboard.rb +5 -3
- package/scripts/doctor.rb +243 -0
- package/scripts/lib/bridge.rb +72 -24
- package/scripts/lib/frontmatter_writer.rb +130 -0
- package/scripts/lib/graph_rebuild.rb +328 -0
- package/scripts/lib/installer_core.rb +5 -0
- package/scripts/lib/intent_validator.rb +79 -0
- package/scripts/lib/links_projection.rb +160 -0
- package/scripts/lib/links_section.rb +207 -0
- package/scripts/lib/power_tools.rb +76 -0
- package/scripts/lib/qmd_hook.rb +38 -25
- package/scripts/lib/qmd_sync.rb +21 -0
- package/scripts/new-intent +172 -22
- package/scripts/project-links +287 -0
- package/scripts/qmd-sync +50 -3
- package/scripts/rebuild-graph +244 -0
- package/scripts/spawn-preamble +18 -1
- package/skills/auto/SKILL.md +13 -3
- package/skills/auto/evals/evals.json +48 -0
- package/skills/auto/references/agent-architecture.md +20 -0
- package/skills/auto/references/agent-report-contract.md +86 -0
- package/skills/brainstorming/SKILL.md +1 -0
- package/skills/brainstorming/evals/evals.json +22 -0
- package/skills/continuing/SKILL.md +8 -1
- package/skills/continuing/evals/evals.json +9 -0
- package/skills/creating-intent/SKILL.md +28 -8
- package/skills/creating-intent/evals/evals.json +72 -0
- package/skills/creating-intent/references/lifecycle.md +12 -4
- package/skills/dashboard/SKILL.md +5 -0
- package/skills/dashboard/evals/evals.json +22 -0
- package/skills/executing-plan/SKILL.md +2 -2
- package/skills/intent-curator/SKILL.md +3 -1
- package/skills/intent-curator/evals/evals.json +22 -0
- package/skills/linking-intents/SKILL.md +17 -6
- package/skills/linking-intents/evals/evals.json +22 -0
- package/skills/linking-intents/references/zettelkasten.md +15 -3
- package/skills/managing-index/SKILL.md +6 -0
- package/skills/managing-index/evals/evals.json +22 -0
- package/skills/managing-index/references/zettelkasten-linking.md +7 -2
- package/skills/research/SKILL.md +8 -0
- package/skills/research/evals/evals.json +22 -0
package/PLASTIC.md
CHANGED
|
@@ -35,15 +35,28 @@ Identity and knowledge graph only. Nothing operational.
|
|
|
35
35
|
---
|
|
36
36
|
id: "4a1"
|
|
37
37
|
intent: "Short description of the desire"
|
|
38
|
-
sources: ["4a"] #
|
|
39
|
-
chain: ["4a1a"] # forward
|
|
38
|
+
sources: ["4a"] # direct ascendants: intents this was created from
|
|
39
|
+
chain: ["4a1a"] # forward: what this spawned and related successors
|
|
40
40
|
created: 2026-05-29
|
|
41
41
|
author: human # human | agent-name
|
|
42
42
|
tags: [plastic, architecture]
|
|
43
43
|
---
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
- `sources`
|
|
46
|
+
- `sources` (formative, must-load, acyclic) and `chain` (forward + relational, lighter,
|
|
47
|
+
may cycle) form the directed knowledge graph. Reciprocity is one-directional: every
|
|
48
|
+
`sources` edge has a reciprocal `chain` entry (I1), but `chain` may carry relational
|
|
49
|
+
entries with no reciprocal `sources` (I2), so the graph is not strictly symmetric.
|
|
50
|
+
- Context contract: load `sources` strongly (they are what the intent was built from);
|
|
51
|
+
traverse `chain` lightly for discovery. See
|
|
52
|
+
docs/concepts/how-plastic-sources-and-chains-intents.md for the full model.
|
|
53
|
+
- `## Links` (I5) is the human-readable projection of the graph. It mirrors the
|
|
54
|
+
frontmatter exactly: every entry is `- [[id--slug|<target's full intent: text>]]`, a
|
|
55
|
+
clickable `id--slug` wikilink target with the target intent's full `intent:` text as the
|
|
56
|
+
label (cross-store targets render `- [[store:id--slug|<target's full intent: text>]]`).
|
|
57
|
+
Ordering is mandatory: all `sources` first (top), then all `chain`, frontmatter order
|
|
58
|
+
preserved within each group. Sources never appear at the end. No source/chain tags, no
|
|
59
|
+
sub-grouping. An intent with empty `sources` and `chain` carries the empty-state comment.
|
|
47
60
|
- IDs use Luhmann's alternating convention: `1` → `1a` → `1a1` → `1a1a`
|
|
48
61
|
- Multiple branches increment: `1a`, `1b`, `1c`
|
|
49
62
|
|
|
@@ -155,8 +168,11 @@ Format: `ID--three-to-five-words` (all stores).
|
|
|
155
168
|
|
|
156
169
|
- **Branch (`14a`, `14b`)** — a sub-task, refinement, or direct continuation of the
|
|
157
170
|
parent. It cannot stand on its own; it only makes sense as part of the parent's work.
|
|
158
|
-
- **Root (`15`, `16`)
|
|
159
|
-
|
|
171
|
+
- **Root (`15`, `16`)**: an independent thought, even if inspired by another intent.
|
|
172
|
+
Reserve `sources` for true created-from provenance (intents this was built out of). An
|
|
173
|
+
independent intent merely related to or inspired by another carries NO `sources`; record
|
|
174
|
+
the relation on the PREDECESSOR's `chain` (and mirror it as a
|
|
175
|
+
`[[id--slug|<target's full intent: text>]]` wikilink in `## Links`).
|
|
160
176
|
- **Rule of thumb:** if the intent could exist without its parent, it's a root.
|
|
161
177
|
|
|
162
178
|
## INDEX.md
|
|
@@ -27,7 +27,15 @@ When dispatched in auto mode you receive the standard Plastic spawn preamble (fr
|
|
|
27
27
|
2. Read the intent file, its `## Links`, and related code or docs
|
|
28
28
|
3. Research with the adaptive budget the enforcer set (simple 2-3, medium 5-8, complex 10-15 steps)
|
|
29
29
|
4. Produce (output handoff): an enriched `## Context` plus `### Decisions` with rationale
|
|
30
|
-
5. Log autonomous calls in `## Insights` with the `(autonomous)` marker, then report for gating
|
|
30
|
+
5. Log autonomous calls in `## Insights` with the `(autonomous)` marker, then report for gating (see `## Completion Report`)
|
|
31
|
+
|
|
32
|
+
## Completion Report
|
|
33
|
+
|
|
34
|
+
END your turn with a structured completion report as your final message, per the spawn preamble's `REPORT_CONTRACT` and `skills/auto/references/agent-report-contract.md`. Do not finish silently. Carry the common envelope (role, intent id, stage, status, artifacts written, verification, checklist deltas, deviations, blockers) plus the brainstorming payload:
|
|
35
|
+
|
|
36
|
+
- Decisions recorded in `### Decisions`, each with its one-line rationale
|
|
37
|
+
- Context enriched: what was researched and the key findings
|
|
38
|
+
- Open questions resolved, and any deliberately left for the spec
|
|
31
39
|
|
|
32
40
|
## Constraints
|
|
33
41
|
|
|
@@ -28,6 +28,16 @@ When dispatched in auto mode you receive the standard Plastic spawn preamble (fr
|
|
|
28
28
|
3. Edit project code (the gate is open now that plan and checklist exist)
|
|
29
29
|
4. Run the full suite, iterate to zero failures and zero errors
|
|
30
30
|
5. Produce (output handoff): the code changes, a checked-off `checklist.md`, and `## Insights`
|
|
31
|
+
6. Report for gating (see `## Completion Report`); the enforcer reviews the work at the final gate
|
|
32
|
+
|
|
33
|
+
## Completion Report
|
|
34
|
+
|
|
35
|
+
END your turn with a structured completion report as your final message, per the spawn preamble's `REPORT_CONTRACT` and `skills/auto/references/agent-report-contract.md`. Do not finish silently. Carry the common envelope (role, intent id, stage, status, artifacts written, verification, checklist deltas, deviations, blockers) plus the executor payload:
|
|
36
|
+
|
|
37
|
+
- Actions implemented this turn, mapped to checklist items checked off (checked / total)
|
|
38
|
+
- A summary of the code changed (files and the shape of the change)
|
|
39
|
+
- Test result: the full-suite command and its pass / fail counts
|
|
40
|
+
- Insights appended, with the `(autonomous)` marker
|
|
31
41
|
|
|
32
42
|
## Constraints
|
|
33
43
|
|
|
@@ -18,23 +18,25 @@ You are the Plastic Intent Curator. Your role is to maintain the health and navi
|
|
|
18
18
|
|
|
19
19
|
## Your Responsibilities
|
|
20
20
|
|
|
21
|
-
1. **Intent lifecycle management** — move intents between Active/Future/Completed in INDEX.md, fill in `## Outcome` sections
|
|
22
|
-
2. **INDEX.md maintenance** — keep Active/Future/Clusters/Completed sections accurate and well-organized
|
|
21
|
+
1. **Intent lifecycle management** — move intents between Active/Future/Completed/Abandoned in INDEX.md, fill in `## Outcome` sections (including the abandonment rationale when an intent is abandoned)
|
|
22
|
+
2. **INDEX.md maintenance** — keep Active/Future/Clusters/Completed/Abandoned sections accurate and well-organized
|
|
23
23
|
3. **Link discovery** — suggest connections between intents that share topics but aren't linked
|
|
24
24
|
4. **Cluster management** — create new clusters when 3+ unlinked intents share tags, merge or rename clusters as topics evolve
|
|
25
25
|
5. **Orphan detection** — flag intents with no links and no cluster membership
|
|
26
26
|
|
|
27
27
|
## How You Work
|
|
28
28
|
|
|
29
|
+
0. QMD-first (when available): when you need to locate a specific intent (to reclassify, link, or cluster it) rather than rebuild the whole landscape, before scanning the store with grep/Read run `ruby ~/.plastic/scripts/qmd-sync search "<terms>"` to surface candidate or related intents, then open the authoritative intent file for any hit you act on. The command is a no-op when QMD is absent, so fall back to the full scan below. (This is discovery; the reindex step at a terminal-state transition is separate.)
|
|
29
30
|
1. Scan `~/.plastic/store/*/ID--slug.md` (or project store) to understand the full intent landscape
|
|
30
31
|
2. Read `~/.plastic/INDEX.md` (or project INDEX.md) to understand current organization
|
|
31
|
-
3. Compare: are there intents not in any cluster? Missing from Active/Completed? Status mismatches?
|
|
32
|
+
3. Compare: are there intents not in any cluster? Missing from Active/Completed/Abandoned? Status mismatches?
|
|
32
33
|
4. Make targeted edits to INDEX.md and intent frontmatter/links
|
|
33
|
-
5.
|
|
34
|
+
5. On a terminal-state transition: whenever you move an intent to Completed OR Abandoned, refresh the QMD index for that store so the new outcome (or abandonment rationale) is searchable. This is mandatory on any terminal-state move and a no-op when QMD is absent, and it runs in the background so it never blocks: `ruby ~/.plastic/scripts/qmd-sync reindex --store <store-root> --async`
|
|
35
|
+
6. Report what you changed
|
|
34
36
|
|
|
35
37
|
## Constraints
|
|
36
38
|
|
|
37
39
|
- You only edit `~/.plastic/INDEX.md` (or project INDEX.md) and `~/.plastic/store/*/ID--slug.md` (or project store) files
|
|
38
40
|
- You never create new intents — that's the creating-intent skill's job
|
|
39
41
|
- You never modify `## Insights`, `## Context`, or `## Outcome` content sections — those belong to the worker
|
|
40
|
-
-
|
|
42
|
+
- For discovery, put QMD first when available (`qmd-sync search`), then fall back to Read and grep/find; use Edit for targeted changes
|
|
@@ -27,7 +27,17 @@ When dispatched in auto mode you receive the standard Plastic spawn preamble (fr
|
|
|
27
27
|
2. Read `spec.md` and the plan/checklist templates; match their FORM
|
|
28
28
|
3. Write `plan.md`, the `actions/` directory, and `checklist.md` into the intent directory
|
|
29
29
|
4. Produce (output handoff): `plan.md` plus `actions/` plus `checklist.md`
|
|
30
|
-
5. Report for gating; the enforcer verifies plan and checklist exist before Exec is allowed
|
|
30
|
+
5. Report for gating (see `## Completion Report`); the enforcer verifies plan and checklist exist before Exec is allowed
|
|
31
|
+
|
|
32
|
+
## Completion Report
|
|
33
|
+
|
|
34
|
+
END your turn with a structured completion report as your final message, per the spawn preamble's `REPORT_CONTRACT` and `skills/auto/references/agent-report-contract.md`. Do not finish silently. Carry the common envelope (role, intent id, stage, status, artifacts written, verification, checklist deltas, deviations, blockers) plus the planner payload, which EXPLAINS THE PLAN BACK TO THE ORCHESTRATOR:
|
|
35
|
+
|
|
36
|
+
- The ordered actions, one line each: what the action does and how it is verified
|
|
37
|
+
- Decomposition rationale: why this order, and why the actions are independent
|
|
38
|
+
- Checklist coverage: the item count, and that every action plus suite-green is covered
|
|
39
|
+
|
|
40
|
+
The plan is an argument; the orchestrator gates on whether that argument is sound before any code is written, so make the report make that case.
|
|
31
41
|
|
|
32
42
|
## Constraints
|
|
33
43
|
|
|
@@ -27,7 +27,15 @@ When dispatched in auto mode you receive the standard Plastic spawn preamble (fr
|
|
|
27
27
|
2. Read the spec template (`templates/spec.md`) and match its FORM exactly
|
|
28
28
|
3. Write `spec.md` into the intent directory, resolving the decisions into a coherent approach
|
|
29
29
|
4. Produce (output handoff): a complete `spec.md`
|
|
30
|
-
5. Report for gating; the enforcer checks the spec against its exit criteria before How begins
|
|
30
|
+
5. Report for gating (see `## Completion Report`); the enforcer checks the spec against its exit criteria before How begins
|
|
31
|
+
|
|
32
|
+
## Completion Report
|
|
33
|
+
|
|
34
|
+
END your turn with a structured completion report as your final message, per the spawn preamble's `REPORT_CONTRACT` and `skills/auto/references/agent-report-contract.md`. Do not finish silently. Carry the common envelope (role, intent id, stage, status, artifacts written, verification, checklist deltas, deviations, blockers) plus the spec payload:
|
|
35
|
+
|
|
36
|
+
- The spec sections produced (Problem, Goals, Non-Goals, Approach, Decisions, Acceptance Criteria)
|
|
37
|
+
- How the recorded decisions resolved into the chosen approach
|
|
38
|
+
- The acceptance-criteria count, so the planner knows the surface to cover
|
|
31
39
|
|
|
32
40
|
## Constraints
|
|
33
41
|
|
package/hooks/statusline
CHANGED
|
@@ -1,56 +1,165 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# plastic-hook-version:
|
|
3
|
-
# StatusLine hook
|
|
2
|
+
# plastic-hook-version: 3.0.0
|
|
3
|
+
# StatusLine hook - Plastic owns the full statusline (no chaining).
|
|
4
|
+
# Renders: {model} - Plastic {version}[ -> {next}] - {path} - [{project}: {id} {title}]
|
|
5
|
+
# Pure bash (macOS 3.2 compatible). No ruby, no jq. Reads stdin once; small file reads only.
|
|
4
6
|
|
|
5
7
|
INPUT=$(cat)
|
|
6
8
|
|
|
7
|
-
# ---
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
# --- Colors (ANSI-C quoting; bash 3.2 supports $'\033') ---
|
|
10
|
+
C_MODEL=$'\033[38;2;90;140;220m' # navy blue - model (lightened for legibility)
|
|
11
|
+
C_VER=$'\033[37m' # white - "Plastic {version}" and update arrow
|
|
12
|
+
C_NEXT=$'\033[33m' # yellow - next version
|
|
13
|
+
C_PATH=$'\033[38;2;128;134;144m' # gray - path (matches dim UI text)
|
|
14
|
+
C_BRACK=$'\033[33m' # yellow - work-unit brackets
|
|
15
|
+
C_PROJ=$'\033[38;2;249;140;30m' # orange - project name
|
|
16
|
+
C_ID=$'\033[38;2;45;212;191m' # teal - intent id (accent between orange + yellow)
|
|
17
|
+
C_INTENT=$'\033[33m' # yellow - intent title
|
|
18
|
+
C_SEP=$'\033[38;2;128;134;144m' # gray - separators
|
|
19
|
+
RST=$'\033[0m'
|
|
20
|
+
SEP="${C_SEP} "$'\302\267'" ${RST}" # " . " middot, dim
|
|
21
|
+
|
|
22
|
+
# --- Extract a flat JSON string field from stdin: json_str <key> ---
|
|
23
|
+
json_str() {
|
|
24
|
+
printf '%s' "$INPUT" \
|
|
25
|
+
| grep -o "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" \
|
|
26
|
+
| head -1 \
|
|
27
|
+
| sed "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"//;s/\"$//"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# --- Model (model.display_name) ---
|
|
31
|
+
MODEL=$(json_str "display_name")
|
|
32
|
+
|
|
33
|
+
# --- Path (workspace.current_dir, fallback top-level cwd) ---
|
|
34
|
+
REAL_CWD=$(json_str "current_dir")
|
|
35
|
+
[ -z "$REAL_CWD" ] && REAL_CWD=$(json_str "cwd")
|
|
36
|
+
# Abbreviate $HOME -> ~
|
|
37
|
+
CWD="$REAL_CWD"
|
|
38
|
+
case "$CWD" in
|
|
39
|
+
"$HOME") CWD="~" ;;
|
|
40
|
+
"$HOME"/*) CWD="~${CWD#"$HOME"}" ;;
|
|
41
|
+
esac
|
|
42
|
+
|
|
43
|
+
# --- Plastic version ---
|
|
44
|
+
VERSION=""
|
|
45
|
+
[ -f "$HOME/.plastic/VERSION" ] && VERSION=$(tr -d ' \t\n' < "$HOME/.plastic/VERSION")
|
|
46
|
+
|
|
47
|
+
# --- Update (next version when one is available) ---
|
|
48
|
+
NEXT=""
|
|
49
|
+
CACHE_FILE="$HOME/.plastic/.cache/update-check.json"
|
|
50
|
+
if [ -f "$CACHE_FILE" ]; then
|
|
51
|
+
if grep -q '"updateAvailable"[[:space:]]*:[[:space:]]*true' "$CACHE_FILE" 2>/dev/null; then
|
|
52
|
+
LATEST=$(grep -o '"latest"[[:space:]]*:[[:space:]]*"[^"]*"' "$CACHE_FILE" 2>/dev/null \
|
|
53
|
+
| head -1 | sed 's/.*"latest"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
|
54
|
+
if [ -n "$LATEST" ]; then
|
|
55
|
+
case "$LATEST" in
|
|
56
|
+
*-*) NEXT="${LATEST##*-}" ;; # 1.0.0-beta.3 -> beta.3
|
|
57
|
+
*) NEXT="$LATEST" ;;
|
|
58
|
+
esac
|
|
59
|
+
fi
|
|
17
60
|
fi
|
|
18
61
|
fi
|
|
19
62
|
|
|
20
|
-
# ---
|
|
21
|
-
|
|
63
|
+
# --- Scope resolution: cwd -> registered project store, else global ---
|
|
64
|
+
# Match the longest registered project path that prefixes the real cwd.
|
|
65
|
+
PROJECTS_YML="$HOME/.plastic/projects.yml"
|
|
66
|
+
SLUG=""
|
|
67
|
+
if [ -n "$REAL_CWD" ] && [ -f "$PROJECTS_YML" ]; then
|
|
68
|
+
SLUG=$(awk -v cwd="$REAL_CWD" '
|
|
69
|
+
/^ [A-Za-z0-9_.-]+:[[:space:]]*$/ { s=$1; sub(/:$/,"",s); next }
|
|
70
|
+
/^ path:/ {
|
|
71
|
+
p=$0; sub(/^[[:space:]]*path:[[:space:]]*/,"",p); gsub(/"/,"",p);
|
|
72
|
+
if (length(p)>0 && (cwd==p || index(cwd, p"/")==1) && length(p)>best) { best=length(p); win=s }
|
|
73
|
+
}
|
|
74
|
+
END { if (win!="") print win }
|
|
75
|
+
' "$PROJECTS_YML")
|
|
76
|
+
fi
|
|
22
77
|
|
|
23
|
-
|
|
24
|
-
INDEX="$HOME/.plastic/INDEX.md"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
PLASTIC_PARTS="plastic: ${INTENT}"
|
|
30
|
-
fi
|
|
78
|
+
if [ -n "$SLUG" ]; then
|
|
79
|
+
INDEX="$HOME/.plastic/projects/$SLUG/INDEX.md"
|
|
80
|
+
PROJ_LABEL="$SLUG"
|
|
81
|
+
else
|
|
82
|
+
INDEX="$HOME/.plastic/INDEX.md"
|
|
83
|
+
PROJ_LABEL="global"
|
|
31
84
|
fi
|
|
85
|
+
INDEX_DIR=$(dirname "$INDEX")
|
|
32
86
|
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
87
|
+
# --- Current-session work-unit: the live bridge wins over savepoint recency ---
|
|
88
|
+
# The statusline receives the real session_id on stdin; the session's bridge
|
|
89
|
+
# (plastic-{session_id}.json) is the authoritative "what THIS session is driving".
|
|
90
|
+
# This beats the shared, savepoint-recency heuristic so parallel sessions on one
|
|
91
|
+
# store no longer overwrite each other's line. grep/sed only - no jq, no ruby.
|
|
92
|
+
WORK=""
|
|
93
|
+
SID=$(json_str "session_id")
|
|
94
|
+
if [ -n "$SID" ]; then
|
|
95
|
+
BRIDGE="${PLASTIC_TMP:-/tmp}/plastic-${SID}.json"
|
|
96
|
+
if [ -f "$BRIDGE" ]; then
|
|
97
|
+
# Bridge JSON is pretty-printed (one field per line). Scope extraction to the
|
|
98
|
+
# "intent" object so a sibling top-level "id"/"name" can never win (the intent
|
|
99
|
+
# object holds only string fields, so the first "}" closes it). grep/sed only.
|
|
100
|
+
INTENT_BLOCK=$(sed -n '/"intent"[[:space:]]*:[[:space:]]*{/,/}/p' "$BRIDGE" 2>/dev/null)
|
|
101
|
+
B_ID=$(printf '%s\n' "$INTENT_BLOCK" | grep -o '"id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 \
|
|
102
|
+
| sed 's/.*"id"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
|
103
|
+
B_NAME=$(printf '%s\n' "$INTENT_BLOCK" | grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 \
|
|
104
|
+
| sed 's/.*"name"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
|
105
|
+
if [ -n "$B_ID" ] && [ -n "$B_NAME" ]; then
|
|
106
|
+
EMDASH=$'\342\200\224'
|
|
107
|
+
WORK="${B_ID} ${EMDASH} ${B_NAME}"
|
|
45
108
|
fi
|
|
46
109
|
fi
|
|
47
110
|
fi
|
|
48
111
|
|
|
49
|
-
# ---
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
112
|
+
# --- Most-recently-accessed active intent (mirror dashboard: last savepoint ts) ---
|
|
113
|
+
# Fallback only: runs when no session bridge resolved a work-unit above.
|
|
114
|
+
if [ -z "$WORK" ] && [ -f "$INDEX" ]; then
|
|
115
|
+
BEST_TS=""
|
|
116
|
+
while IFS= read -r line; do
|
|
117
|
+
[ -z "$line" ] && continue
|
|
118
|
+
inside=${line#*[}; inside=${inside%%]*} # "{id} - {title}"
|
|
119
|
+
dir=${line#*](store/}; dir=${dir%%/*} # intent directory name
|
|
120
|
+
[ -z "$dir" ] && continue
|
|
121
|
+
sp="$INDEX_DIR/store/$dir/savepoint.md"
|
|
122
|
+
ts=""
|
|
123
|
+
if [ -f "$sp" ]; then
|
|
124
|
+
ts=$(grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z' "$sp" 2>/dev/null | tail -1)
|
|
125
|
+
fi
|
|
126
|
+
# ISO8601 sorts lexicographically; keep the newest. Empty ts sorts lowest.
|
|
127
|
+
if [ -z "$BEST_TS" ] || { [ -n "$ts" ] && [ "$ts" \> "$BEST_TS" ]; }; then
|
|
128
|
+
BEST_TS="$ts"
|
|
129
|
+
WORK="$inside"
|
|
130
|
+
fi
|
|
131
|
+
done <<EOF
|
|
132
|
+
$(sed -n '/^## Active$/,/^## /{/^- \[/p;}' "$INDEX")
|
|
133
|
+
EOF
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# --- Build the work-unit "[{project}: {id} {title}]" with title truncation ---
|
|
137
|
+
WORK_SEG=""
|
|
138
|
+
if [ -n "$WORK" ]; then
|
|
139
|
+
# Split "{id} - {title}" on the em-dash (U+2014).
|
|
140
|
+
EMDASH=$'\342\200\224'
|
|
141
|
+
id=${WORK%% $EMDASH *}
|
|
142
|
+
title=${WORK#* $EMDASH }
|
|
143
|
+
# Fallback if the em-dash form is absent: split on first space.
|
|
144
|
+
if [ "$id" = "$WORK" ]; then id=${WORK%% *}; title=${WORK#* }; fi
|
|
145
|
+
# Truncate long titles (~40 chars).
|
|
146
|
+
if [ ${#title} -gt 40 ]; then title="$(printf '%.40s' "$title")..."; fi
|
|
147
|
+
WORK_SEG="${C_BRACK}[${RST}${C_PROJ}${PROJ_LABEL}${RST}${C_INTENT}: ${RST}${C_ID}${id}${RST}${C_INTENT} ${title}${RST}${C_BRACK}]${RST}"
|
|
56
148
|
fi
|
|
149
|
+
|
|
150
|
+
# --- Assemble segments in order, joined by " . " ---
|
|
151
|
+
PARTS=""
|
|
152
|
+
add() { [ -z "$1" ] && return; if [ -z "$PARTS" ]; then PARTS="$1"; else PARTS="$PARTS$SEP$1"; fi; }
|
|
153
|
+
|
|
154
|
+
# Order: stable values first (model, work-unit), then the volatile-length
|
|
155
|
+
# values last (version, path) so the line does not visually jump as they change.
|
|
156
|
+
[ -n "$MODEL" ] && add "${C_MODEL}${MODEL}${RST}"
|
|
157
|
+
[ -n "$WORK_SEG" ] && add "$WORK_SEG"
|
|
158
|
+
if [ -n "$VERSION" ]; then
|
|
159
|
+
vseg="${C_VER}Plastic ${VERSION}${RST}"
|
|
160
|
+
[ -n "$NEXT" ] && vseg="${vseg}${C_VER} "$'\342\206\222'" ${RST}${C_NEXT}${NEXT}${RST}"
|
|
161
|
+
add "$vseg"
|
|
162
|
+
fi
|
|
163
|
+
[ -n "$CWD" ] && add "${C_PATH}${CWD}${RST}"
|
|
164
|
+
|
|
165
|
+
printf '%s\n' "$PARTS"
|
package/package.json
CHANGED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: UTF-8
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# agent-report - deterministic filesystem-derived completion report (intent 74).
|
|
6
|
+
#
|
|
7
|
+
# The auto-mode report contract is decision-shaped (the spawn preamble's
|
|
8
|
+
# REPORT_CONTRACT plus the role prompts ask every dispatched agent to END with a
|
|
9
|
+
# structured completion report). Because child-agent honor is best-effort across
|
|
10
|
+
# harnesses (Tier B/C, docs/reference/harness-adapters.md), the contract is never a
|
|
11
|
+
# hard block. This script is the always-a-report fallback: when a dispatched agent
|
|
12
|
+
# returns no usable report, the enforcer runs it to synthesize one from the intent
|
|
13
|
+
# directory, so the handoff account always exists.
|
|
14
|
+
#
|
|
15
|
+
# Like scripts/spawn-preamble, it is a PURE function of the intent dir: no network,
|
|
16
|
+
# no randomness, no wall-clock reads. Two runs over the same on-disk state produce
|
|
17
|
+
# byte-identical output.
|
|
18
|
+
#
|
|
19
|
+
# Usage:
|
|
20
|
+
# agent-report <intent_dir> [--role ROLE]
|
|
21
|
+
#
|
|
22
|
+
# Exit codes: 0 (report emitted), 2 (usage).
|
|
23
|
+
|
|
24
|
+
require_relative "lib/bridge"
|
|
25
|
+
|
|
26
|
+
def parse_args(argv)
|
|
27
|
+
role = nil
|
|
28
|
+
positional = []
|
|
29
|
+
i = 0
|
|
30
|
+
while i < argv.length
|
|
31
|
+
case argv[i]
|
|
32
|
+
when "--role"
|
|
33
|
+
role = argv[i + 1]
|
|
34
|
+
i += 2
|
|
35
|
+
else
|
|
36
|
+
positional << argv[i]
|
|
37
|
+
i += 1
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
[positional.first, role]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Read the intent file frontmatter via the same path spawn-preamble uses, so the
|
|
44
|
+
# id/intent we report match what the rest of Plastic sees. Never raises.
|
|
45
|
+
def frontmatter_for(intent_dir)
|
|
46
|
+
ifile = Bridge.intent_file(intent_dir)
|
|
47
|
+
return {} unless File.exist?(ifile)
|
|
48
|
+
|
|
49
|
+
content = File.read(ifile)
|
|
50
|
+
return {} unless content.start_with?("---")
|
|
51
|
+
|
|
52
|
+
parts = content.split("---", 3)
|
|
53
|
+
return {} if parts.length < 3
|
|
54
|
+
|
|
55
|
+
require "yaml"
|
|
56
|
+
require "date"
|
|
57
|
+
require "time"
|
|
58
|
+
YAML.safe_load(parts[1], permitted_classes: [Date, Time]) || {}
|
|
59
|
+
rescue StandardError
|
|
60
|
+
{}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Current stage: prefer the last non-empty savepoint line (the ledger encodes the
|
|
64
|
+
# furthest-reached milestone), else the file-derived stage. Same logic as
|
|
65
|
+
# spawn-preamble's current_stage.
|
|
66
|
+
STAGE_LABELS = {
|
|
67
|
+
"what" => "What", "why" => "Why", "how" => "How",
|
|
68
|
+
"exec" => "Exec", "done" => "Done"
|
|
69
|
+
}.freeze
|
|
70
|
+
|
|
71
|
+
def current_stage(intent_dir)
|
|
72
|
+
ledger = File.join(intent_dir, Bridge::SAVEPOINT_FILE)
|
|
73
|
+
if File.exist?(ledger)
|
|
74
|
+
last = File.read(ledger).each_line.map(&:strip).reject(&:empty?).last
|
|
75
|
+
return last if last
|
|
76
|
+
end
|
|
77
|
+
STAGE_LABELS.fetch(Bridge.derive_stage(intent_dir), Bridge.derive_stage(intent_dir))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# checklist.md checked/total: count GFM task-list items. Returns "n/a" when there
|
|
81
|
+
# is no checklist (a checklist is only meaningful from How onward).
|
|
82
|
+
def checklist_progress(intent_dir)
|
|
83
|
+
path = File.join(intent_dir, "checklist.md")
|
|
84
|
+
return "n/a" unless Bridge.stage_file_present?(path)
|
|
85
|
+
|
|
86
|
+
lines = File.readlines(path)
|
|
87
|
+
total = lines.count { |l| l =~ /^\s*- \[[ xX]\]/ }
|
|
88
|
+
checked = lines.count { |l| l =~ /^\s*- \[[xX]\]/ }
|
|
89
|
+
return "n/a" if total.zero?
|
|
90
|
+
|
|
91
|
+
"#{checked}/#{total}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The outcome line: first content line under outcome.md's "## Summary" (or the first
|
|
95
|
+
# non-heading content line), else a placeholder. Pure read of the intent dir.
|
|
96
|
+
def outcome_line(intent_dir)
|
|
97
|
+
path = File.join(intent_dir, "outcome.md")
|
|
98
|
+
return "(no outcome yet)" unless Bridge.stage_file_present?(path)
|
|
99
|
+
|
|
100
|
+
lines = File.readlines(path).map(&:rstrip)
|
|
101
|
+
start = lines.index { |l| l.strip.downcase == "## summary" }
|
|
102
|
+
scan = start ? lines[(start + 1)..] : lines
|
|
103
|
+
(scan || []).each do |l|
|
|
104
|
+
s = l.strip
|
|
105
|
+
next if s.empty? || s.start_with?("#") || s.start_with?("<!--")
|
|
106
|
+
return s
|
|
107
|
+
end
|
|
108
|
+
"(no outcome yet)"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
intent_dir_arg, role = parse_args(ARGV)
|
|
112
|
+
|
|
113
|
+
if intent_dir_arg.nil? || intent_dir_arg.empty?
|
|
114
|
+
warn "usage: agent-report <intent_dir> [--role ROLE]"
|
|
115
|
+
exit 2
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
intent_dir = File.expand_path(intent_dir_arg)
|
|
119
|
+
|
|
120
|
+
fm = frontmatter_for(intent_dir)
|
|
121
|
+
id = fm["id"].to_s.strip
|
|
122
|
+
intent_name = fm["intent"].to_s.strip
|
|
123
|
+
id = "(unknown)" if id.empty?
|
|
124
|
+
intent_name = "(unknown)" if intent_name.empty?
|
|
125
|
+
|
|
126
|
+
stage = current_stage(intent_dir)
|
|
127
|
+
role_label = (role && !role.empty?) ? role : stage
|
|
128
|
+
artifacts = Bridge.has_files(intent_dir)
|
|
129
|
+
artifacts_str = artifacts.empty? ? "(none)" : artifacts.join(", ")
|
|
130
|
+
|
|
131
|
+
lines = []
|
|
132
|
+
lines << "=== Plastic agent report (synthesized) ==="
|
|
133
|
+
lines << "Intent: #{id} - #{intent_name}"
|
|
134
|
+
lines << "Stage: #{stage}"
|
|
135
|
+
lines << "Role: #{role_label}"
|
|
136
|
+
lines << "Status: synthesized (filesystem-derived; no agent-authored report)"
|
|
137
|
+
lines << "Artifacts present: #{artifacts_str}"
|
|
138
|
+
lines << "Checklist: #{checklist_progress(intent_dir)}"
|
|
139
|
+
lines << "Outcome: #{outcome_line(intent_dir)}"
|
|
140
|
+
lines << "=== end report ==="
|
|
141
|
+
|
|
142
|
+
puts lines.join("\n")
|
package/scripts/dashboard.rb
CHANGED
|
@@ -223,15 +223,17 @@ def effort_of(rec, type)
|
|
|
223
223
|
end
|
|
224
224
|
|
|
225
225
|
# Value -> :high | :low (explicit frontmatter field wins).
|
|
226
|
-
# High is deliberately rare: an explicit stamp,
|
|
227
|
-
#
|
|
226
|
+
# High is deliberately rare: an explicit stamp, a human-authored root idea, or an intent
|
|
227
|
+
# that has SPAWNED follow-on work, i.e. a strategic theme the user owns. "Has spawned work"
|
|
228
|
+
# means a reciprocal (I1) edge: another intent lists this one in its `sources`, captured by
|
|
229
|
+
# `referenced`. A purely relational `chain` entry (D2, no reciprocal `sources`) does NOT
|
|
230
|
+
# count as spawned, so bare `chain` membership is not a high-value signal (intent 68).
|
|
228
231
|
def value_of(rec, referenced = {})
|
|
229
232
|
case rec[:value_field]
|
|
230
233
|
when "high" then return :high
|
|
231
234
|
when "low" then return :low
|
|
232
235
|
end
|
|
233
236
|
return :high if rec[:author] == "human" && root_intent?(rec[:id])
|
|
234
|
-
return :high unless rec[:chain].empty?
|
|
235
237
|
return :high if referenced[[rec[:scope], rec[:id]]]
|
|
236
238
|
:low
|
|
237
239
|
end
|