crewly 1.20.434 → 1.20.436

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.
Files changed (41) hide show
  1. package/config/constants.ts +14 -0
  2. package/config/skills/agent/core/reproduce-ui/SKILL.md +35 -13
  3. package/config/skills/agent/core/reproduce-ui/execute.sh +14 -5
  4. package/config/skills/agent/core/reproduce-ui/execute.test.sh +72 -2
  5. package/config/skills/agent/core/reproduce-ui/reproduce.mjs +173 -3
  6. package/config/skills/orchestrator/set-goal/SKILL.md +13 -4
  7. package/config/sops/developer/debugging.md +1 -1
  8. package/dist/backend/backend/src/constants.d.ts +6 -0
  9. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  10. package/dist/backend/backend/src/controllers/memory/memory.controller.d.ts +4 -2
  11. package/dist/backend/backend/src/controllers/memory/memory.controller.d.ts.map +1 -1
  12. package/dist/backend/backend/src/controllers/memory/memory.controller.js +13 -6
  13. package/dist/backend/backend/src/controllers/memory/memory.controller.js.map +1 -1
  14. package/dist/backend/backend/src/services/memory/goal-tracking.service.d.ts +20 -3
  15. package/dist/backend/backend/src/services/memory/goal-tracking.service.d.ts.map +1 -1
  16. package/dist/backend/backend/src/services/memory/goal-tracking.service.js +24 -4
  17. package/dist/backend/backend/src/services/memory/goal-tracking.service.js.map +1 -1
  18. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-goal.d.ts +45 -6
  19. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-goal.d.ts.map +1 -1
  20. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-goal.js +109 -15
  21. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot-goal.js.map +1 -1
  22. package/dist/backend/build-info.json +2 -2
  23. package/dist/backend/config/constants.d.ts +13 -0
  24. package/dist/backend/config/constants.d.ts.map +1 -1
  25. package/dist/backend/config/constants.js +13 -0
  26. package/dist/backend/config/constants.js.map +1 -1
  27. package/dist/backend/config/index.d.ts +6 -0
  28. package/dist/backend/config/index.d.ts.map +1 -1
  29. package/dist/cli/backend/src/constants.d.ts +6 -0
  30. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  31. package/dist/cli/backend/src/services/memory/goal-tracking.service.d.ts +20 -3
  32. package/dist/cli/backend/src/services/memory/goal-tracking.service.d.ts.map +1 -1
  33. package/dist/cli/backend/src/services/memory/goal-tracking.service.js +24 -4
  34. package/dist/cli/backend/src/services/memory/goal-tracking.service.js.map +1 -1
  35. package/dist/cli/config/constants.d.ts +13 -0
  36. package/dist/cli/config/constants.d.ts.map +1 -1
  37. package/dist/cli/config/constants.js +13 -0
  38. package/dist/cli/config/constants.js.map +1 -1
  39. package/dist/cli/config/index.d.ts +6 -0
  40. package/dist/cli/config/index.d.ts.map +1 -1
  41. package/package.json +1 -1
@@ -791,6 +791,20 @@ export const MEMORY_CONSTANTS = {
791
791
  AGENTS_INDEX: 'agents-index.json',
792
792
  },
793
793
 
794
+ /**
795
+ * Status word a goals-log entry heading may carry right after its
796
+ * timestamp: `### [<iso>] REACHED — Set by <who>`. A closed status
797
+ * (reached, superseded, cancelled) takes the entry out of the project's
798
+ * active goal; `reached` also makes it a milestone the daily retro
799
+ * reviews (#985). Entries without a status word are active.
800
+ */
801
+ GOAL_STATUSES: {
802
+ ACTIVE: 'active',
803
+ REACHED: 'reached',
804
+ SUPERSEDED: 'superseded',
805
+ CANCELLED: 'cancelled',
806
+ },
807
+
794
808
  /**
795
809
  * File names for agent-level memory
796
810
  */
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: Reproduce UI
3
- description: Open a Crewly dashboard feature by its feature-map entry in a headless browser and save evidence (screenshot, console errors, failed requests, timings) before you reproduce or fix a UI bug report.
4
- version: 1.0.0
3
+ description: Turn a UI bug report (a screenshot and a sentence) into evidence - match it to a feature-map entry, open that feature in a headless browser and save screenshot, console errors, failed requests and timings, plus one line for the ticket - before you reproduce or fix it.
4
+ version: 1.1.0
5
5
  category: development
6
6
  skillType: claude-skill
7
7
  assignableRoles:
@@ -17,6 +17,7 @@ triggers:
17
17
  - reproduce
18
18
  - screenshot bug
19
19
  - UI bug
20
+ - bug report
20
21
  - feature map
21
22
  tags:
22
23
  - frontend
@@ -39,6 +40,10 @@ for every route how a user reaches it, what it should do and which
39
40
  `data-testid` anchors it has; this skill turns one map entry into a run.
40
41
 
41
42
  ```bash
43
+ # From the bug report: the owner's sentence plus the words you read on the
44
+ # screenshot (headings, tab names, button labels). Picks the feature, runs it.
45
+ bash execute.sh --report "runs tab is blank — Tickets, Board · Requests · Runs"
46
+
42
47
  # By route (tabs go in the query string, as the map shows them)
43
48
  bash execute.sh --route "/tickets?tab=runs"
44
49
 
@@ -55,20 +60,37 @@ bash execute.sh --feature Usage --dry-run
55
60
  What you get back is one JSON line with `dir`: the evidence folder under
56
61
  `$CREWLY_HOME/evidence/<utc stamp>-<route>/` holding `screenshot.png`,
57
62
  `console.json` (console errors, warnings and page errors), `requests.json`
58
- (failed requests and responses ≥ 400), `report.md` and `summary.json`.
59
- Exit 0 means the key element appeared; exit 2 means it did not (the
60
- evidence is still there — that is the bug, or the wrong selector); exit 1 is
61
- a usage or browser problem.
63
+ (failed requests and responses ≥ 400), `report.md` and `summary.json`; its
64
+ `ticketLine` is the one line to put in the ticket ("Reproduced `/x`: key
65
+ element NOT found; 1 console error; 2 failed requests (first: POST /api/b →
66
+ 500); evidence: <dir>"). Exit 0 means the key element appeared; exit 2 means
67
+ it did not (the evidence is still there — that is the bug, or the wrong
68
+ selector); exit 1 is a usage or browser problem; exit 3 means the report
69
+ matched no single feature (the candidates are on stderr — pass `--route`).
70
+
71
+ How `--report` matches: every word of the report is looked up in each
72
+ feature's title and **Expected** line (2 points), its route, **Reach** and
73
+ **Elements** (1 point), and its tab names (4 points, and the tab becomes
74
+ `?tab=`). A feature wins with two or more matching words, 3+ points and half
75
+ again as much as the runner-up. The words on the screenshot are what make
76
+ it unambiguous: a report of "blank page" alone matches nothing; "blank page,
77
+ Tickets, Runs" opens `/tickets?tab=runs`. Read the screenshot first and put
78
+ what you see into the report text.
62
79
 
63
80
  How to use it on a report:
64
81
 
65
- 1. Find the feature in the map (the words on the screenshot are usually in
66
- a page's **Expected** or **Elements** line). Note its route and `?tab=`.
67
- 2. Run this skill on that route. Compare `screenshot.png` with the owner's
68
- screenshot; read `console.json` and `requests.json` first — a failed
69
- `/api/...` call explains most blank or stale pages.
70
- 3. Put the `report.md` path (or its key lines) in the ticket, then fix. After
71
- the fix, run it again and attach the second folder as proof.
82
+ 1. Write the report text: the owner's sentence, then the headings, tab
83
+ names and labels you can read on the screenshot. Run `--report` with it
84
+ (or `--report-file`). If it exits 3, pick the route from the candidates it
85
+ printed and run `--route`.
86
+ 2. Compare `screenshot.png` with the owner's screenshot; read `console.json`
87
+ and `requests.json` first — a failed `/api/...` call explains most blank
88
+ or stale pages. `report.md` quotes the report and says which words chose
89
+ the feature, so a wrong match is visible at a glance.
90
+ 3. Put `ticketLine` in the ticket (the evidence folder stays on this
91
+ machine; quote the folder path and the key lines, not the files), then
92
+ fix. After the fix, run it again and put the second line under the first
93
+ as proof.
72
94
 
73
95
  Notes:
74
96
 
@@ -21,11 +21,15 @@ print_usage() {
21
21
  Usage:
22
22
  bash execute.sh --route /tickets?tab=runs Open a route, wait for its key element, save evidence
23
23
  bash execute.sh --feature "Run detail" --param id=<uuid> Resolve the route from the feature map
24
+ bash execute.sh --report "runs tab is blank, Tickets" Match a bug report to a feature, then run it
24
25
  bash execute.sh --route /wiki --wait '[data-testid="wiki-recent-list"]' --full-page
25
26
 
26
27
  Options:
27
28
  --route Path to open (with ?tab= when the page has tabs)
28
29
  --feature A route or a word from a Routes row of FEATURE_MAP.md (needs the map)
30
+ --report A bug report: the owner's sentence plus the words you read on the
31
+ screenshot; scored against the map's Features (needs the map)
32
+ --report-file The same, read from a file
29
33
  --param k=v Value for a :param in the route (repeatable)
30
34
  --wait CSS selector to wait for (default: the feature's first data-testid, else body)
31
35
  --map FEATURE_MAP.md path (default: <project>/frontend/FEATURE_MAP.md when present)
@@ -38,17 +42,20 @@ Options:
38
42
  --help | -h Show this help
39
43
 
40
44
  Output: one JSON line with the evidence folder (screenshot.png, console.json,
41
- requests.json, report.md, summary.json). Exit 0 when the element appeared, 2
42
- when it did not (evidence still saved), 1 on a usage or browser error.
45
+ requests.json, report.md, summary.json; summary.ticketLine is the line to put
46
+ in the ticket). Exit 0 when the element appeared, 2 when it did not (evidence
47
+ still saved), 1 on a usage or browser error, 3 when the report matches no
48
+ single feature (candidates on stderr; pass --route).
43
49
  EOF_USAGE
44
50
  }
45
51
 
46
- ROUTE=""; FEATURE=""; WAIT=""; MAP=""; BASE_URL=""; OUT=""; TIMEOUT=""; FULL_PAGE=""; BROWSER=""; DRY_RUN=""
52
+ ROUTE=""; FEATURE=""; REPORT=""; WAIT=""; MAP=""; BASE_URL=""; OUT=""; TIMEOUT=""; FULL_PAGE=""; BROWSER=""; DRY_RUN=""
47
53
  PARAMS=()
48
54
 
49
55
  if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
50
56
  ROUTE=$(printf '%s' "$1" | jq -r '.route // empty')
51
57
  FEATURE=$(printf '%s' "$1" | jq -r '.feature // empty')
58
+ REPORT=$(printf '%s' "$1" | jq -r '.report // empty')
52
59
  WAIT=$(printf '%s' "$1" | jq -r '.wait // empty')
53
60
  MAP=$(printf '%s' "$1" | jq -r '.map // empty')
54
61
  BASE_URL=$(printf '%s' "$1" | jq -r '.baseUrl // empty')
@@ -64,6 +71,8 @@ while [[ $# -gt 0 ]]; do
64
71
  case "$1" in
65
72
  --route) [ $# -ge 2 ] || error_exit "--route requires a value"; ROUTE="$2"; shift 2 ;;
66
73
  --feature) [ $# -ge 2 ] || error_exit "--feature requires a value"; FEATURE="$2"; shift 2 ;;
74
+ --report) [ $# -ge 2 ] || error_exit "--report requires text"; REPORT="$2"; shift 2 ;;
75
+ --report-file) [ $# -ge 2 ] || error_exit "--report-file requires a path"; [ -f "$2" ] || error_exit "--report-file not found: $2"; REPORT="$(cat "$2")"; shift 2 ;;
67
76
  --param) [ $# -ge 2 ] || error_exit "--param requires k=v"; PARAMS+=("$2"); shift 2 ;;
68
77
  --wait) [ $# -ge 2 ] || error_exit "--wait requires a selector"; WAIT="$2"; shift 2 ;;
69
78
  --map) [ $# -ge 2 ] || error_exit "--map requires a path"; MAP="$2"; shift 2 ;;
@@ -78,7 +87,7 @@ while [[ $# -gt 0 ]]; do
78
87
  esac
79
88
  done
80
89
 
81
- [ -n "$ROUTE" ] || [ -n "$FEATURE" ] || error_exit "one of --route or --feature is required (--help for usage)"
90
+ [ -n "$ROUTE" ] || [ -n "$FEATURE" ] || [ -n "$REPORT" ] || error_exit "one of --route, --feature or --report is required (--help for usage)"
82
91
  command -v node >/dev/null 2>&1 || error_exit "node is required"
83
92
 
84
93
  # The map: explicit, else the project's checkout, else the package's own copy (a repo install).
@@ -95,6 +104,6 @@ for kv in "${PARAMS[@]+"${PARAMS[@]}"}"; do
95
104
  export "RU_PARAM_${key}=${val}"
96
105
  done
97
106
 
98
- RU_MAIN=1 RU_ROUTE="$ROUTE" RU_FEATURE="$FEATURE" RU_WAIT="$WAIT" RU_MAP="$MAP" RU_BASE_URL="$BASE_URL" \
107
+ RU_MAIN=1 RU_ROUTE="$ROUTE" RU_FEATURE="$FEATURE" RU_REPORT="$REPORT" RU_WAIT="$WAIT" RU_MAP="$MAP" RU_BASE_URL="$BASE_URL" \
99
108
  RU_OUT="$OUT" RU_TIMEOUT_MS="$TIMEOUT" RU_FULL_PAGE="$FULL_PAGE" RU_BROWSER="$BROWSER" RU_DRY_RUN="$DRY_RUN" \
100
109
  RU_PKG_ROOT="$PKG_ROOT" node "${SCRIPT_DIR}/reproduce.mjs"
@@ -34,19 +34,22 @@ cat > "$MAP" <<'EOF_MAP'
34
34
 
35
35
  ### Dashboard (`/`)
36
36
 
37
+ - **Expected**: "Waiting on you" first, then "Crew now" with the health bar.
37
38
  - **Elements**: `dashboard`, `crew-now`.
38
39
 
39
40
  ### Tickets hub (`/tickets`, tabs `board` · `runs`)
40
41
 
42
+ - **Expected**: Board = kanban columns; Runs = one row per work item.
41
43
  - **Elements**: `tickets-panel-${tab}`, `tickets-page`, `tickets-board`.
42
44
 
43
45
  ### Run detail (`/tickets/runs/:id`)
44
46
 
47
+ - **Expected**: the work item's header, metrics and timeline.
45
48
  - **Elements**: `workitem-detail-loading|error|not-found`, `workitem-detail`.
46
49
 
47
50
  ### Usage (`/usage`)
48
51
 
49
- - **Expected**: one headline.
52
+ - **Expected**: one headline with tokens by team.
50
53
  EOF_MAP
51
54
  export CREWLY_API_URL="http://127.0.0.1:18799"
52
55
  export CREWLY_HOME="$T/home"
@@ -60,7 +63,7 @@ check "help names --feature" "$(printf '%s' "$OUT" | grep -c -- '--feature')" "$
60
63
  # ---- no route / feature ----
61
64
  ERR=$(bash "$EXEC" --dry-run 2>&1 >/dev/null); RC=$?
62
65
  check "no route: exit 1" "$RC" "1"
63
- check "no route: error says so" "$(printf '%s' "$ERR" | jq -r .error | grep -c 'route or --feature')" "1"
66
+ check "no route: error says so" "$(printf '%s' "$ERR" | jq -r .error | grep -c 'route, --feature or --report')" "1"
64
67
 
65
68
  # ---- route + map: wait selector from the Elements line; ${} and a|b skipped ----
66
69
  OUT=$(bash "$EXEC" --route "/tickets?tab=runs" --map "$MAP" --dry-run)
@@ -113,6 +116,73 @@ OUT=$(bash "$EXEC" '{"route":"/usage","map":"'"$MAP"'","dryRun":true,"params":{"
113
116
  check "json input: route" "$(printf '%s' "$OUT" | jq -r .path)" "/usage"
114
117
  check "json input: fullPage" "$(printf '%s' "$OUT" | jq -r .fullPage)" "true"
115
118
 
119
+ # ---- --report: a bug report matched to a feature (slice 3) ----
120
+ OUT=$(bash "$EXEC" --report "the runs tab under tickets is blank" --map "$MAP" --dry-run)
121
+ check "report: tab word picks the hub with ?tab=" "$(printf '%s' "$OUT" | jq -r .route)" "/tickets?tab=runs"
122
+ check "report: wait = that tab's panel" "$(printf '%s' "$OUT" | jq -r .waitFor)" '[data-testid="tickets-panel-runs"]'
123
+ check "report: match title" "$(printf '%s' "$OUT" | jq -r .match.title)" "Tickets hub"
124
+ check "report: matched words (plural dropped)" "$(printf '%s' "$OUT" | jq -c .match.matched)" '["run","ticket"]'
125
+ check "report: runner-up listed" "$(printf '%s' "$OUT" | jq -r '.match.candidates[0].route')" "/tickets/runs/:id"
126
+ check "report: text kept in the plan" "$(printf '%s' "$OUT" | jq -r .report)" "the runs tab under tickets is blank"
127
+
128
+ OUT=$(bash "$EXEC" --report "Tokens by team headline missing on the usage page" --map "$MAP" --dry-run)
129
+ check "report: Expected words pick Usage" "$(printf '%s' "$OUT" | jq -r .route)" "/usage"
130
+
131
+ OUT=$(bash "$EXEC" --report "Waiting on you never loads, Crew now spins" --map "$MAP" --dry-run)
132
+ check "report: Dashboard by its Expected words" "$(printf '%s' "$OUT" | jq -r .route)" "/"
133
+
134
+ ERR=$(bash "$EXEC" --report "nothing happens at all" --map "$MAP" --dry-run 2>&1 >/dev/null); RC=$?
135
+ check "report with no match: exit 3" "$RC" "3"
136
+ check "report with no match: says so" "$(printf '%s' "$ERR" | jq -r .error | grep -c 'matches no feature')" "1"
137
+
138
+ ERR=$(bash "$EXEC" --report "tickets" --map "$MAP" --dry-run 2>&1 >/dev/null); RC=$?
139
+ check "one-word report: exit 3" "$RC" "3"
140
+ check "one-word report: asks for more words" "$(printf '%s' "$ERR" | jq -r .error | grep -c 'too weakly')" "1"
141
+ check "one-word report: best candidate first" "$(printf '%s' "$ERR" | jq -r '.candidates[0].route')" "/tickets"
142
+
143
+ ERR=$(bash "$EXEC" --report "the work item is wrong" --map "$MAP" --dry-run 2>&1 >/dev/null); RC=$?
144
+ check "even report: exit 3" "$RC" "3"
145
+ check "even report: says equally" "$(printf '%s' "$ERR" | jq -r .error | grep -c 'about equally')" "1"
146
+ check "even report: lists candidates" "$(printf '%s' "$ERR" | jq -r '.candidates | length' )" "2"
147
+
148
+ printf 'runs tab under\ntickets is blank\n' > "$T/report.txt"
149
+ OUT=$(bash "$EXEC" --report-file "$T/report.txt" --map "$MAP" --dry-run)
150
+ check "report-file: same match" "$(printf '%s' "$OUT" | jq -r .route)" "/tickets?tab=runs"
151
+ ERR=$(bash "$EXEC" --report-file "$T/nope.txt" --map "$MAP" --dry-run 2>&1 >/dev/null); RC=$?
152
+ check "report-file missing: exit 1" "$RC" "1"
153
+
154
+ OUT=$(bash "$EXEC" '{"report":"runs tab under tickets","map":"'"$MAP"'","dryRun":true}')
155
+ check "json input: report" "$(printf '%s' "$OUT" | jq -r .route)" "/tickets?tab=runs"
156
+
157
+ OUT=$(bash "$EXEC" --route /usage --report "runs tab under tickets" --map "$MAP" --dry-run)
158
+ check "route beats report" "$(printf '%s' "$OUT" | jq -r .route)" "/usage"
159
+ check "route beats report: no match block" "$(printf '%s' "$OUT" | jq -r .match)" "null"
160
+
161
+ ERR=$(bash "$EXEC" --report "runs tab" --map "$T/missing.md" --dry-run 2>&1 >/dev/null); RC=$?
162
+ check "report without map: exit 1" "$RC" "1"
163
+ check "report without map: explains" "$(printf '%s' "$ERR" | jq -r .error | grep -c 'needs the feature map')" "1"
164
+
165
+ # ---- ticketLine / report sections (pure functions, no browser) ----
166
+ OUT=$(cd "$HERE" && node --input-type=module -e '
167
+ import { ticketLine, renderReport } from "./reproduce.mjs";
168
+ const reqs = [{ url: "http://x/api/a", method: "GET", status: 404 }, { url: "http://x/api/b", method: "POST", status: 500 }];
169
+ const s = { route: "/tickets?tab=runs", url: "http://x/tickets?tab=runs", finalUrl: "", title: "T", waitFor: "[data-testid=\"p\"]", waitSource: "map",
170
+ found: false, error: "Timeout 60000ms", timings: { navigationMs: 1, elementMs: null, totalMs: 2 }, consoleErrors: 1, failedRequests: 2, dir: "/e/1",
171
+ screenshot: null, feature: null, report: "runs tab blank\nsecond line", match: { title: "Tickets hub", score: 6, matched: ["run", "ticket"] } };
172
+ s.ticketLine = ticketLine(s, reqs);
173
+ console.log(s.ticketLine);
174
+ console.log(renderReport(s, [{ type: "error", text: "boom" }], reqs));
175
+ ')
176
+ check "ticketLine: not found + 5xx first" "$(printf '%s' "$OUT" | sed -n 1p)" 'Reproduced `/tickets?tab=runs`: key element `[data-testid="p"]` NOT found (Timeout 60000ms); 1 console error; 2 failed requests (first: POST http://x/api/b → 500); evidence: /e/1'
177
+ check "report.md: match line" "$(printf '%s' "$OUT" | grep -c 'Matched from the report: Tickets hub (score 6, words: run, ticket)')" "1"
178
+ check "report.md: ticket section" "$(printf '%s' "$OUT" | grep -c '^## For the ticket')" "1"
179
+ check "report.md: report quoted" "$(printf '%s' "$OUT" | grep -c '^> second line')" "1"
180
+ OUT=$(cd "$HERE" && node --input-type=module -e '
181
+ import { ticketLine } from "./reproduce.mjs";
182
+ console.log(ticketLine({ route: "/", found: true, consoleErrors: 0, failedRequests: 0, dir: "/e/2" }, []));
183
+ ')
184
+ check "ticketLine: rendered, nothing failed" "$OUT" 'Reproduced `/`: rendered; 0 console errors; 0 failed requests; evidence: /e/2'
185
+
116
186
  ERR=$(bash "$EXEC" --route relative --dry-run 2>&1 >/dev/null); RC=$?
117
187
  check "route must start with /" "$(printf '%s' "$ERR" | jq -r .error | grep -c 'must start with /')" "1"
118
188
 
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * reproduce-ui — open a dashboard feature by its feature-map entry and save
3
- * evidence (#811 slice 2, specs/2026-10-26-feature-map.md).
3
+ * evidence (#811 slices 2–3, specs/2026-10-26-feature-map.md).
4
4
  *
5
5
  * Driven by execute.sh through environment variables:
6
6
  * RU_ROUTE route to open, e.g. `/tickets?tab=runs` (or RU_FEATURE)
7
7
  * RU_FEATURE a word of a Routes row of the map, resolved to its route
8
+ * RU_REPORT a bug report (the owner's sentence plus the words read off
9
+ * the screenshot); matched to one Features section of the map
8
10
  * RU_WAIT CSS selector to wait for; default = the first data-testid
9
11
  * of the feature's Elements line in the map, else `body`
10
12
  * RU_MAP path of FEATURE_MAP.md (optional; lookups need it)
@@ -18,7 +20,8 @@
18
20
  *
19
21
  * Output: one JSON line on stdout — the plan (dry run) or the summary with the
20
22
  * evidence folder. Exit 0 when the element appeared, 2 when it did not (the
21
- * evidence is still written), 1 on a usage or launch error.
23
+ * evidence is still written), 1 on a usage or launch error, 3 when a report
24
+ * matches no single feature (the candidates are on stderr).
22
25
  */
23
26
 
24
27
  import { createRequire } from 'node:module';
@@ -131,6 +134,128 @@ export function defaultWaitSelector(markdown, route, tab) {
131
134
  return null;
132
135
  }
133
136
 
137
+ /**
138
+ * Words that say nothing about which feature a report is about.
139
+ */
140
+ const REPORT_STOPWORDS = new Set([
141
+ 'the', 'and', 'for', 'with', 'this', 'that', 'there', 'then', 'than', 'when', 'what', 'where', 'which', 'who',
142
+ 'why', 'how', 'but', 'not', 'nothing', 'none', 'all', 'any', 'some', 'are', 'was', 'were', 'has', 'have',
143
+ 'had', 'does', 'did', 'can', 'cannot', 'could', 'should', 'would', 'will', 'just', 'still', 'again', 'also',
144
+ 'only', 'very', 'too', 'from', 'into', 'onto', 'out', 'over', 'under', 'after', 'before', 'about', 'here',
145
+ 'see', 'sees', 'seen', 'shows', 'show', 'shown', 'showing', 'looks', 'look', 'like', 'seems', 'says', 'said',
146
+ 'page', 'pages', 'screen', 'screenshot', 'tab', 'tabs', 'click', 'clicked', 'clicking', 'open', 'opens',
147
+ 'opened', 'opening', 'get', 'gets', 'got', 'its', 'you', 'your', 'our', 'they', 'them', 'their', 'one', 'two',
148
+ 'bug', 'broken', 'wrong', 'blank', 'empty', 'missing', 'error', 'errors', 'fails', 'failed', 'failing',
149
+ 'loading', 'loads', 'load', 'stuck', 'forever', 'instead', 'please', 'fix', 'crewly', 'dashboard',
150
+ 'something', 'anything', 'everything', 'thing', 'things', 'off', 'yet', 'now',
151
+ ]);
152
+
153
+ /**
154
+ * The comparable words of a text: lower-case, letters and digits only,
155
+ * three characters or more, no stopwords, a trailing plural `s` dropped so
156
+ * that "runs" meets "run".
157
+ *
158
+ * @param {string} text - Any text
159
+ * @returns {Set<string>}
160
+ */
161
+ export function reportTokens(text) {
162
+ const out = new Set();
163
+ for (const raw of (text || '').toLowerCase().split(/[^a-z0-9]+/)) {
164
+ if (raw.length < 3 || REPORT_STOPWORDS.has(raw)) continue;
165
+ out.add(raw.length > 3 && raw.endsWith('s') && !raw.endsWith('ss') ? raw.slice(0, -1) : raw);
166
+ }
167
+ return out;
168
+ }
169
+
170
+ /**
171
+ * One Features section of the map, broken into the word sets a report is
172
+ * scored against.
173
+ *
174
+ * @typedef {object} FeatureBlock
175
+ * @property {string} title - The heading without the parenthesis
176
+ * @property {string} route - The first route in the heading
177
+ * @property {string[]} tabs - Tab names from `tabs \`a\` · \`b\``
178
+ * @property {Set<string>} strong - Title and Expected words (weight 2)
179
+ * @property {Set<string>} weak - Route segments, Reach, Elements words (weight 1)
180
+ */
181
+
182
+ /**
183
+ * The Features sections of the map.
184
+ *
185
+ * @param {string} markdown - FEATURE_MAP.md
186
+ * @returns {FeatureBlock[]}
187
+ */
188
+ export function featureBlocks(markdown) {
189
+ const blocks = [];
190
+ for (const block of markdownSection(markdown, '## Features').split(/\n(?=### )/)) {
191
+ const lines = block.split('\n');
192
+ const heading = lines[0] ?? '';
193
+ if (!heading.startsWith('### ')) continue;
194
+ const route = /`(\/[^`]*)`/.exec(heading)?.[1];
195
+ if (!route) continue;
196
+ const title = heading.slice(4).replace(/\s*\(.*$/, '').trim();
197
+ const tabsText = /tabs\s+((?:`[^`]+`\s*(?:·\s*)?)+)/.exec(heading)?.[1] ?? '';
198
+ const tabs = [...tabsText.matchAll(/`([^`]+)`/g)].map((m) => m[1]);
199
+ const lineOf = (label) => lines.find((l) => l.includes(`**${label}**`)) ?? '';
200
+ const strong = reportTokens(`${title} ${lineOf('Expected')}`);
201
+ const weak = reportTokens(`${route.replace(/:[A-Za-z_]+/g, ' ')} ${lineOf('Reach')} ${lineOf('Elements').replace(/[-_$]/g, ' ')}`);
202
+ blocks.push({ title, route, tabs, strong, weak });
203
+ }
204
+ return blocks;
205
+ }
206
+
207
+ /**
208
+ * Score every feature against a bug report: a report word found in the
209
+ * feature's title or Expected line counts 2, in its route, Reach or
210
+ * Elements 1, and a word that is one of the feature's tab names 4 (the tab
211
+ * also becomes the `?tab=` of the route). The best feature wins when at
212
+ * least two words matched, it scores 3 or more, and half again as much as
213
+ * the runner-up; otherwise the caller gets the candidates and asks for
214
+ * `--route`. One word is never enough: "Usage" alone is a `--feature`.
215
+ *
216
+ * @param {string} markdown - FEATURE_MAP.md
217
+ * @param {string} report - The owner's sentence plus words read off the screenshot
218
+ * @returns {{chosen: object|null, candidates: object[], tokens: string[]}}
219
+ * candidates: `{route, title, score, matched}` sorted best first (score > 0 only)
220
+ */
221
+ export function matchReport(markdown, report) {
222
+ const tokens = [...reportTokens(report)];
223
+ const scored = [];
224
+ for (const b of featureBlocks(markdown)) {
225
+ let score = 0;
226
+ const matched = [];
227
+ let tab = null;
228
+ for (const t of tokens) {
229
+ const tabHit = b.tabs.find((name) => reportTokens(name).has(t));
230
+ const w = tabHit ? 4 : b.strong.has(t) ? 2 : b.weak.has(t) ? 1 : 0;
231
+ if (!w) continue;
232
+ score += w;
233
+ matched.push(t);
234
+ if (tabHit && !tab) tab = tabHit;
235
+ }
236
+ if (score > 0) scored.push({ route: tab ? `${b.route}?tab=${tab}` : b.route, title: b.title, score, matched });
237
+ }
238
+ scored.sort((a, c) => c.score - a.score || a.route.localeCompare(c.route));
239
+ const [best, second] = scored;
240
+ const confident = best && best.matched.length >= 2 && best.score >= 3 && (!second || best.score >= second.score * 1.5);
241
+ return { chosen: confident ? best : null, candidates: scored.slice(0, 5), tokens };
242
+ }
243
+
244
+ /**
245
+ * Why a report could not be matched, for the error line.
246
+ *
247
+ * @param {{candidates: object[]}} m - From {@link matchReport}
248
+ * @returns {string}
249
+ */
250
+ export function matchFailure(m) {
251
+ if (!m.candidates.length) return 'the report matches no feature in the map; add the words you see on the screenshot, or pass --route';
252
+ const best = m.candidates.map((c) => `${c.route} [${c.score}]`).join(', ');
253
+ if (m.candidates.length === 1 || m.candidates[0].score >= m.candidates[1].score * 1.5) {
254
+ return `the report matches a feature too weakly (one word only, or under 3 points); add the words you see on the screenshot, or pass --route (best: ${best})`;
255
+ }
256
+ return `the report matches several features about equally; pass --route (best: ${best})`;
257
+ }
258
+
134
259
  /**
135
260
  * The Routes row whose pattern matches an opened path (`/tickets/runs/abc`
136
261
  * → `/tickets/runs/:id`); an exact match wins, the query string is ignored.
@@ -191,6 +316,25 @@ export function buildPlan() {
191
316
  const map = mapPath ? readFileSync(mapPath, 'utf8') : null;
192
317
  let route = env.RU_ROUTE || '';
193
318
  let row = null;
319
+ let match = null;
320
+ if (!route && !env.RU_FEATURE && env.RU_REPORT) {
321
+ if (!map) fail(`--report needs the feature map; pass --map or --route (looked for ${env.RU_MAP || 'FEATURE_MAP.md'})`);
322
+ const m = matchReport(map, env.RU_REPORT);
323
+ if (!m.chosen) {
324
+ process.stderr.write(
325
+ `${JSON.stringify({
326
+ ok: false,
327
+ error: matchFailure(m),
328
+ candidates: m.candidates,
329
+ tokens: m.tokens,
330
+ })}\n`,
331
+ );
332
+ process.exit(3);
333
+ }
334
+ route = m.chosen.route;
335
+ match = { ...m.chosen, candidates: m.candidates.slice(1) };
336
+ row = matchRouteRow(routeRows(map), route);
337
+ }
194
338
  if (!route && env.RU_FEATURE) {
195
339
  if (!map) fail(`--feature needs the feature map; pass --map or --route (looked for ${env.RU_MAP || 'FEATURE_MAP.md'})`);
196
340
  const r = resolveFeature(map, env.RU_FEATURE);
@@ -200,7 +344,7 @@ export function buildPlan() {
200
344
  } else if (route && map) {
201
345
  row = matchRouteRow(routeRows(map), route);
202
346
  }
203
- if (!route) fail('one of --route or --feature is required');
347
+ if (!route) fail('one of --route, --feature or --report is required');
204
348
  if (!route.startsWith('/')) fail('--route must start with /');
205
349
  const params = {};
206
350
  for (const [k, v] of Object.entries(env)) if (k.startsWith('RU_PARAM_') && v) params[k.slice('RU_PARAM_'.length)] = v;
@@ -221,10 +365,30 @@ export function buildPlan() {
221
365
  fullPage: env.RU_FULL_PAGE === '1',
222
366
  map: mapPath,
223
367
  feature: row ? { page: row.page, reach: row.reach, job: row.job } : null,
368
+ report: env.RU_REPORT || null,
369
+ match,
224
370
  outRoot,
225
371
  };
226
372
  }
227
373
 
374
+ /**
375
+ * One line for the ticket: what was opened, whether it rendered, and what
376
+ * the console and network said, with the evidence folder.
377
+ *
378
+ * @param {object} s - Summary
379
+ * @param {Array<object>} requests - Failed requests
380
+ * @returns {string}
381
+ */
382
+ export function ticketLine(s, requests) {
383
+ const worst = requests.find((r) => r.status >= 500) ?? requests.find((r) => r.failure) ?? requests[0];
384
+ const parts = [
385
+ `Reproduced \`${s.route}\`: ${s.found ? 'rendered' : `key element \`${s.waitFor}\` NOT found${s.error ? ` (${s.error})` : ''}`}`,
386
+ `${s.consoleErrors} console error${s.consoleErrors === 1 ? '' : 's'}`,
387
+ `${s.failedRequests} failed request${s.failedRequests === 1 ? '' : 's'}${worst ? ` (first: ${worst.method} ${worst.url} → ${worst.status ?? worst.failure})` : ''}`,
388
+ ];
389
+ return `${parts.join('; ')}; evidence: ${s.dir}`;
390
+ }
391
+
228
392
  /**
229
393
  * Load playwright-core from the Crewly package.
230
394
  *
@@ -320,7 +484,10 @@ async function run(plan) {
320
484
  dir,
321
485
  screenshot: existsSync(screenshot) ? screenshot : null,
322
486
  feature: plan.feature,
487
+ report: plan.report,
488
+ match: plan.match,
323
489
  };
490
+ summary.ticketLine = ticketLine(summary, requests);
324
491
  writeFileSync(join(dir, 'console.json'), `${JSON.stringify(console_, null, 2)}\n`);
325
492
  writeFileSync(join(dir, 'requests.json'), `${JSON.stringify(requests, null, 2)}\n`);
326
493
  writeFileSync(join(dir, 'summary.json'), `${JSON.stringify(summary, null, 2)}\n`);
@@ -348,6 +515,9 @@ export function renderReport(s, console_, requests) {
348
515
  `- Screenshot: ${s.screenshot ? 'screenshot.png' : '(none)'}`,
349
516
  ];
350
517
  if (s.feature) lines.push(`- Feature: ${s.feature.job} — reached from ${s.feature.reach}; page \`${s.feature.page}\``);
518
+ if (s.match) lines.push(`- Matched from the report: ${s.match.title} (score ${s.match.score}, words: ${s.match.matched.join(', ')})`);
519
+ if (s.ticketLine) lines.push('', '## For the ticket', '', s.ticketLine);
520
+ if (s.report) lines.push('', '## Report', '', ...s.report.split('\n').map((l) => `> ${l}`));
351
521
  lines.push('', `## Console errors (${console_.length})`, '');
352
522
  lines.push(...(console_.length ? console_.map((c) => `- [${c.type}] ${c.text}`) : ['- none']));
353
523
  lines.push('', `## Failed requests (${requests.length})`, '');
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: Set Goal
3
- description: Set a project-level goal to guide team priorities and track progress.
4
- version: 1.0.0
3
+ description: Set a project-level goal to guide team priorities and track progress, or mark one reached, superseded or cancelled.
4
+ version: 1.1.0
5
5
  category: memory
6
6
  skillType: claude-skill
7
7
  assignableRoles:
@@ -11,6 +11,8 @@ triggers:
11
11
  - add goal
12
12
  - new goal
13
13
  - project goal
14
+ - goal reached
15
+ - goal met
14
16
  tags:
15
17
  - memory
16
18
  - goals
@@ -26,21 +28,28 @@ execution:
26
28
 
27
29
  # Set Goal
28
30
 
29
- Set a project-level goal to guide team priorities and track progress.
31
+ Set a project-level goal to guide team priorities and track progress. The
32
+ goals log (`<project>/.crewly/goals/goals.md`) is append-only: to close a
33
+ goal, append an entry with a `status` instead of editing the old one.
30
34
 
31
35
  ## Usage
32
36
 
33
37
  ```bash
34
38
  bash config/skills/orchestrator/set-goal/execute.sh '{"goal":"Complete API integration by end of sprint","projectPath":"/path/to/project","setBy":"orchestrator"}'
39
+
40
+ # The goal was met: say which one, then the evidence as "- " lines. The team
41
+ # lead's next daily retro reviews it as a milestone.
42
+ bash config/skills/orchestrator/set-goal/execute.sh '{"goal":"Complete API integration by end of sprint\n- all 6 endpoints live since 10/24\n- contract tests green","projectPath":"/path/to/project","setBy":"orchestrator","status":"reached"}'
35
43
  ```
36
44
 
37
45
  ## Parameters
38
46
 
39
47
  | Parameter | Required | Description |
40
48
  |-----------|----------|-------------|
41
- | `goal` | Yes | The goal description |
49
+ | `goal` | Yes | The goal description (with a closing `status`: the goal that was reached or retired, one line, then optional `- ` evidence lines) |
42
50
  | `projectPath` | No | Path to the project this goal applies to |
43
51
  | `setBy` | No | Who is setting the goal (e.g., "orchestrator") |
52
+ | `status` | No | `reached` (goal met → retro milestone), `superseded`, `cancelled`, or `active` (default when absent). Any other word is rejected. |
44
53
 
45
54
  ## Output
46
55
 
@@ -39,7 +39,7 @@ Name **one command** you have already run at least once that shows the user's ex
39
39
 
40
40
  ## 2. Reproduce and minimise
41
41
 
42
- For a dashboard (UI) report, run the `reproduce-ui` skill on the feature first: `bash config/skills/agent/core/reproduce-ui/execute.sh --route "/tickets?tab=runs"` (route and `?tab=` from `frontend/FEATURE_MAP.md`; `--feature <word>` resolves it from the map). Read its `console.json` and `requests.json` before the screenshot — a failed `/api/...` call explains most blank or stale pages — and put the evidence folder in the ticket.
42
+ For a dashboard (UI) report, run the `reproduce-ui` skill on the feature first. A screenshot and a sentence is enough: read the screenshot, then `bash config/skills/agent/core/reproduce-ui/execute.sh --report "<the sentence> <headings, tab names and labels you see>"` matches it to a `frontend/FEATURE_MAP.md` entry and opens it (exit 3 = ambiguous: it lists the candidate routes, run again with `--route`; `--feature <word>` and `--route "/tickets?tab=runs"` work when you already know the page). Read its `console.json` and `requests.json` before the screenshot — a failed `/api/...` call explains most blank or stale pages. Put its `ticketLine` (from the JSON it prints, also in `report.md`) in the ticket; after the fix run it again and add the second line as proof.
43
43
 
44
44
  Cut inputs one at a time. Done when every remaining element is load-bearing.
45
45
 
@@ -443,6 +443,12 @@ export declare const MEMORY_CONSTANTS: {
443
443
  readonly CROSS_PROJECT_INSIGHTS: "cross_project_insights.md";
444
444
  readonly AGENTS_INDEX: "agents-index.json";
445
445
  };
446
+ readonly GOAL_STATUSES: {
447
+ readonly ACTIVE: "active";
448
+ readonly REACHED: "reached";
449
+ readonly SUPERSEDED: "superseded";
450
+ readonly CANCELLED: "cancelled";
451
+ };
446
452
  readonly AGENT_FILES: {
447
453
  readonly MEMORY: "memory.json";
448
454
  readonly ROLE_KNOWLEDGE: "role-knowledge.json";